8<\/div>";
31 | row += "<\/div>";
32 |
33 | if(wheelRef.current){
34 | for(let x = 0; x < 29; x++){
35 | wheelRef.current.innerHTML += row;
36 | }
37 | }
38 | };
39 |
40 | const spinWheel = () => {
41 | const order = [0, 11, 5, 10, 6, 9, 7, 8, 1, 14, 2, 13, 3, 12, 4];
42 | const position = Math.floor(Math.random() * order.length);
43 |
44 | const rows = 12;
45 | const card = 75 + 3 * 2;
46 | let landingPosition = (rows * 15 * card) + (position * card);
47 |
48 | const randomize = Math.floor(Math.random() * 75) - (75/2);
49 |
50 | landingPosition = landingPosition + randomize;
51 |
52 | const landingIndex = (position + Math.round(randomize / card)) % order.length;
53 |
54 | const object = {
55 | x: Math.floor(Math.random() * 50) / 100,
56 | y: Math.floor(Math.random() * 20) / 100
57 | };
58 |
59 | if (wheelRef.current) {
60 | wheelRef.current.style.transitionTimingFunction = `cubic-bezier(0,${object.x},${object.y},1)`;
61 | wheelRef.current.style.transitionDuration = '6s';
62 | wheelRef.current.style.transform = `translate3d(-${landingPosition}px, 0px, 0px)`;
63 |
64 | setTimeout(() => {
65 | if(wheelRef.current){
66 | wheelRef.current.style.transitionTimingFunction = '';
67 | wheelRef.current.style.transitionDuration = '';
68 | const resetTo = -(position * card + randomize);
69 | wheelRef.current.style.transform = `translate3d(${resetTo}px, 0px, 0px)`;
70 |
71 | const landingNumber = order[landingIndex];
72 |
73 | if (h1Ref.current) {
74 | h1Ref.current.innerHTML = `Outcome: ${landingNumber} Color: ${landingNumber % 2 === 0 ? 'Black' : 'Red'}`;
75 | }
76 | setIsSpinning(false);
77 | }
78 | }, 6000);
79 | }
80 | };
81 |
82 | useEffect(() => {
83 | initWheel();
84 | }, []);
85 |
86 | const handleClick = () => {
87 | setIsSpinning(true);
88 | spinWheel();
89 | };
90 |
91 | return (
92 |
93 |
ROULETTE
94 |
98 |
99 |
100 |
103 |
104 |
105 | );
106 | };
107 |
108 | export default Roulette;
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef
,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/app/create/item/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Button } from '@/components/Button';
4 | import React, { useState } from 'react';
5 | import Image from 'next/image';
6 | import { useToast } from '@/components/ui/use-toast';
7 |
8 | function CreateItem() {
9 | const [name, setName] = useState('');
10 | const [rarity, setRarity] = useState(1);
11 | const [price, setPrice] = useState('');
12 | const [imageURL, setImageURL] = useState('');
13 | const { toast } = useToast();
14 |
15 | const handleRarityChange = (event: React.ChangeEvent) => {
16 | setImageURL('');
17 | setRarity(Number(event.target.value));
18 | };
19 |
20 | const handleSubmit = async (e: { preventDefault: () => void; }) => {
21 | e.preventDefault();
22 |
23 | const itemData = {
24 | name,
25 | rarity,
26 | price: parseFloat(price),
27 | imageURL,
28 | };
29 |
30 | try {
31 | const response = await fetch('/api/item', {
32 | method: 'POST',
33 | headers: {
34 | 'Content-Type': 'application/json',
35 | },
36 | body: JSON.stringify(itemData),
37 | });
38 |
39 | if (!response.ok) {
40 | throw new Error(`HTTP error! status: ${response.status}`);
41 | }
42 |
43 | toast({
44 | variant: 'success',
45 | title: "Successfull Item Creation",
46 | description: "You have successfully created " + name + " with rarity " + rarity + " that costs " + price + "!",
47 | })
48 |
49 | setName('');
50 | setRarity(Number(1));
51 | setPrice('');
52 | setImageURL('');
53 | } catch (error) {
54 | console.error(error);
55 | }
56 | };
57 |
58 | return (
59 |
116 | );
117 | }
118 |
119 | export default CreateItem;
--------------------------------------------------------------------------------
/src/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type {
5 | ToastActionElement,
6 | ToastProps,
7 | } from "@/components/ui/toast"
8 |
9 | const TOAST_LIMIT = 1
10 | const TOAST_REMOVE_DELAY = 1000000
11 |
12 | type ToasterToast = ToastProps & {
13 | id: string
14 | title?: React.ReactNode
15 | description?: React.ReactNode
16 | action?: ToastActionElement
17 | }
18 |
19 | const actionTypes = {
20 | ADD_TOAST: "ADD_TOAST",
21 | UPDATE_TOAST: "UPDATE_TOAST",
22 | DISMISS_TOAST: "DISMISS_TOAST",
23 | REMOVE_TOAST: "REMOVE_TOAST",
24 | } as const
25 |
26 | let count = 0
27 |
28 | function genId() {
29 | count = (count + 1) % Number.MAX_SAFE_INTEGER
30 | return count.toString()
31 | }
32 |
33 | type ActionType = typeof actionTypes
34 |
35 | type Action =
36 | | {
37 | type: ActionType["ADD_TOAST"]
38 | toast: ToasterToast
39 | }
40 | | {
41 | type: ActionType["UPDATE_TOAST"]
42 | toast: Partial
43 | }
44 | | {
45 | type: ActionType["DISMISS_TOAST"]
46 | toastId?: ToasterToast["id"]
47 | }
48 | | {
49 | type: ActionType["REMOVE_TOAST"]
50 | toastId?: ToasterToast["id"]
51 | }
52 |
53 | interface State {
54 | toasts: ToasterToast[]
55 | }
56 |
57 | const toastTimeouts = new Map>()
58 |
59 | const addToRemoveQueue = (toastId: string) => {
60 | if (toastTimeouts.has(toastId)) {
61 | return
62 | }
63 |
64 | const timeout = setTimeout(() => {
65 | toastTimeouts.delete(toastId)
66 | dispatch({
67 | type: "REMOVE_TOAST",
68 | toastId: toastId,
69 | })
70 | }, TOAST_REMOVE_DELAY)
71 |
72 | toastTimeouts.set(toastId, timeout)
73 | }
74 |
75 | export const reducer = (state: State, action: Action): State => {
76 | switch (action.type) {
77 | case "ADD_TOAST":
78 | return {
79 | ...state,
80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81 | }
82 |
83 | case "UPDATE_TOAST":
84 | return {
85 | ...state,
86 | toasts: state.toasts.map((t) =>
87 | t.id === action.toast.id ? { ...t, ...action.toast } : t
88 | ),
89 | }
90 |
91 | case "DISMISS_TOAST": {
92 | const { toastId } = action
93 |
94 | // ! Side effects ! - This could be extracted into a dismissToast() action,
95 | // but I'll keep it here for simplicity
96 | if (toastId) {
97 | addToRemoveQueue(toastId)
98 | } else {
99 | state.toasts.forEach((toast) => {
100 | addToRemoveQueue(toast.id)
101 | })
102 | }
103 |
104 | return {
105 | ...state,
106 | toasts: state.toasts.map((t) =>
107 | t.id === toastId || toastId === undefined
108 | ? {
109 | ...t,
110 | open: false,
111 | }
112 | : t
113 | ),
114 | }
115 | }
116 | case "REMOVE_TOAST":
117 | if (action.toastId === undefined) {
118 | return {
119 | ...state,
120 | toasts: [],
121 | }
122 | }
123 | return {
124 | ...state,
125 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
126 | }
127 | }
128 | }
129 |
130 | const listeners: Array<(state: State) => void> = []
131 |
132 | let memoryState: State = { toasts: [] }
133 |
134 | function dispatch(action: Action) {
135 | memoryState = reducer(memoryState, action)
136 | listeners.forEach((listener) => {
137 | listener(memoryState)
138 | })
139 | }
140 |
141 | type Toast = Omit
142 |
143 | function toast({ ...props }: Toast) {
144 | const id = genId()
145 |
146 | const update = (props: ToasterToast) =>
147 | dispatch({
148 | type: "UPDATE_TOAST",
149 | toast: { ...props, id },
150 | })
151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152 |
153 | dispatch({
154 | type: "ADD_TOAST",
155 | toast: {
156 | ...props,
157 | id,
158 | open: true,
159 | onOpenChange: (open) => {
160 | if (!open) dismiss()
161 | },
162 | },
163 | })
164 |
165 | return {
166 | id: id,
167 | dismiss,
168 | update,
169 | }
170 | }
171 |
172 | function useToast() {
173 | const [state, setState] = React.useState(memoryState)
174 |
175 | React.useEffect(() => {
176 | listeners.push(setState)
177 | return () => {
178 | const index = listeners.indexOf(setState)
179 | if (index > -1) {
180 | listeners.splice(index, 1)
181 | }
182 | }
183 | }, [state])
184 |
185 | return {
186 | ...state,
187 | toast,
188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189 | }
190 | }
191 |
192 | export { useToast, toast }
193 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | success: "success group",
34 | },
35 | },
36 | defaultVariants: {
37 | variant: "default",
38 | },
39 | }
40 | )
41 |
42 | const Toast = React.forwardRef<
43 | React.ElementRef,
44 | React.ComponentPropsWithoutRef &
45 | VariantProps
46 | >(({ className, variant, ...props }, ref) => {
47 | return (
48 |
53 | )
54 | })
55 | Toast.displayName = ToastPrimitives.Root.displayName
56 |
57 | const ToastAction = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 | ))
70 | ToastAction.displayName = ToastPrimitives.Action.displayName
71 |
72 | const ToastClose = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >(({ className, ...props }, ref) => (
76 |
85 |
86 |
87 | ))
88 | ToastClose.displayName = ToastPrimitives.Close.displayName
89 |
90 | const ToastTitle = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, ...props }, ref) => (
94 |
99 | ))
100 | ToastTitle.displayName = ToastPrimitives.Title.displayName
101 |
102 | const ToastDescription = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | ToastDescription.displayName = ToastPrimitives.Description.displayName
113 |
114 | type ToastProps = React.ComponentPropsWithoutRef
115 |
116 | type ToastActionElement = React.ReactElement
117 |
118 | export {
119 | type ToastProps,
120 | type ToastActionElement,
121 | ToastProvider,
122 | ToastViewport,
123 | Toast,
124 | ToastTitle,
125 | ToastDescription,
126 | ToastClose,
127 | ToastAction,
128 | }
129 |
--------------------------------------------------------------------------------
/src/app/case/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useState } from 'react';
4 | import { fetchCase } from '@/lib/fetch-case';
5 | import { Button } from '@/components/Button';
6 | import Image from 'next/image';
7 | import { Skeleton } from "@/components/ui/skeleton"
8 | import useStore from '@/lib/global-store';
9 |
10 | interface PageProps {
11 | params: {
12 | slug: string
13 | }
14 | }
15 |
16 | interface Case {
17 | id: number;
18 | name: string;
19 | image: string;
20 | items: any[];
21 | history: any[];
22 | price: number;
23 | odds: Odds[];
24 | }
25 |
26 | interface Odds {
27 | id: number;
28 | caseId: number;
29 | itemId: number;
30 | Odds: number;
31 | }
32 |
33 | const CaseOpener = ({ params }: PageProps) => {
34 | const { slug } = params;
35 |
36 | const [caseData, setCaseData] = useState(null);
37 | const [error, setError] = useState(null);
38 | const [isButtonClicked, setIsButtonClicked] = useState(false);
39 | const [itemsWithOdds, setItemsWithOdds] = useState([]);
40 | const [selectedItem, setSelectedItem] = useState(null);
41 | const [animationStarted, setAnimationStarted] = useState(false);
42 | const balance = useStore((state: { balance: number } | unknown) => (state as { balance: number }).balance);
43 | const updateBalance = useStore((state: unknown) => (state as { updateBalance: (amount: number) => Promise }).updateBalance);
44 |
45 | useEffect(() => {
46 | fetchCase(slug).then(caseData => {
47 | if ('error' in caseData) {
48 | setError(caseData.error);
49 | } else {
50 | setCaseData(caseData);
51 | if (caseData && caseData.length > 0) {
52 | const items = caseData[0].items;
53 | const odds = caseData[0].odds;
54 | const itemsWithOdds = items && odds ? items.map((item: { id: any; }) => {
55 | const oddsItem = odds?.find((odds: { itemId: any; }) => odds.itemId === item.id);
56 | return { ...item, odds: oddsItem ? oddsItem.Odds : null };
57 | }) : [];
58 | setItemsWithOdds(itemsWithOdds);
59 | }
60 | }
61 | });
62 | }, [slug]);
63 |
64 | let name: string | undefined, price: number | undefined, image: string | undefined;
65 |
66 | if (caseData && caseData.length > 0) {
67 | name = caseData[0].name;
68 | price = caseData[0].price;
69 | image = caseData[0].image;
70 | }
71 |
72 | if (error) {
73 | window.location.href = '/404';
74 | }
75 |
76 | const openCase = async () => {
77 | if(!price) return;
78 | if (balance < price) {
79 | alert('Not enough balance');
80 | return;
81 | }
82 |
83 | const response = await fetch('/api/open', {
84 | method: 'POST',
85 | headers: {
86 | 'Content-Type': 'application/json',
87 | },
88 | body: JSON.stringify({ caseId: Number(slug) }),
89 | });
90 |
91 | if (!response.ok) {
92 | const errorData = await response.json();
93 | console.error('Error:', errorData.error);
94 | return;
95 | }
96 |
97 | const data = await response.json();
98 | const updatedBalance = data.leftBalance;
99 | const receivedItem = data.item;
100 |
101 | await updateBalance(updatedBalance);
102 |
103 | console.log(`You received: ${receivedItem.id}`);
104 |
105 | setSelectedItem(receivedItem);
106 | };
107 |
108 | const generateAnimationItems = () => {
109 | if (!itemsWithOdds) return <>>;
110 | return Array(30).fill(null).map((_, index) => {
111 | const item = itemsWithOdds[Math.floor(Math.random() * itemsWithOdds.length)];
112 | return (
113 |
114 |
115 |
116 | );
117 | });
118 | };
119 |
120 | return (
121 | <>
122 |
123 | {name ?
{name}
:
}
124 |
125 | {image ?
126 |
127 | :
}
128 |
129 | {isButtonClicked ?
130 |
131 | {generateAnimationItems()}
132 |
: <>>}
133 |
134 |
135 |
146 |
Items in case:
147 |
148 |
149 | {itemsWithOdds.length > 0 ? itemsWithOdds.map((item, index) => (
150 |
151 | {item.odds &&
{item.odds.toFixed(2)}%
}
152 |
153 |
{item.name}
154 |
${item.price.toFixed(2)}
155 |
156 | )) :
157 | <>
158 |
159 |
160 |
161 |
162 |
163 |
164 | >}
165 |
166 | >
167 | );
168 | };
169 |
170 | export default CaseOpener;
171 |
--------------------------------------------------------------------------------
/src/components/DropdownMenu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
11 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
12 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
13 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
14 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
15 |
16 | const DropdownMenuSubTrigger = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef & {
19 | inset?: boolean
20 | }
21 | >(({ className, inset, children, ...props }, ref) => (
22 |
31 | {children}
32 |
33 |
34 | ))
35 | DropdownMenuSubTrigger.displayName =
36 | DropdownMenuPrimitive.SubTrigger.displayName
37 |
38 | const DropdownMenuSubContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ))
51 | DropdownMenuSubContent.displayName =
52 | DropdownMenuPrimitive.SubContent.displayName
53 |
54 | const DropdownMenuContent = React.forwardRef<
55 | React.ElementRef,
56 | React.ComponentPropsWithoutRef
57 | >(({ className, sideOffset = 4, ...props }, ref) => (
58 |
59 |
68 |
69 | ))
70 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
71 |
72 | const DropdownMenuItem = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef & {
75 | inset?: boolean
76 | }
77 | >(({ className, inset, ...props }, ref) => (
78 |
87 | ))
88 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
89 |
90 | const DropdownMenuCheckboxItem = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, children, checked, ...props }, ref) => (
94 |
103 |
104 |
105 |
106 |
107 |
108 | {children}
109 |
110 | ))
111 | DropdownMenuCheckboxItem.displayName =
112 | DropdownMenuPrimitive.CheckboxItem.displayName
113 |
114 | const DropdownMenuRadioItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 | {children}
132 |
133 | ))
134 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
135 |
136 | const DropdownMenuLabel = React.forwardRef<
137 | React.ElementRef,
138 | React.ComponentPropsWithoutRef & {
139 | inset?: boolean
140 | }
141 | >(({ className, inset, ...props }, ref) => (
142 |
151 | ))
152 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
153 |
154 | const DropdownMenuSeparator = React.forwardRef<
155 | React.ElementRef,
156 | React.ComponentPropsWithoutRef
157 | >(({ className, ...props }, ref) => (
158 |
163 | ))
164 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
165 |
166 | const DropdownMenuShortcut = ({
167 | className,
168 | ...props
169 | }: React.HTMLAttributes) => {
170 | return (
171 |
175 | )
176 | }
177 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
178 |
179 | export {
180 | DropdownMenu,
181 | DropdownMenuTrigger,
182 | DropdownMenuContent,
183 | DropdownMenuItem,
184 | DropdownMenuCheckboxItem,
185 | DropdownMenuRadioItem,
186 | DropdownMenuLabel,
187 | DropdownMenuSeparator,
188 | DropdownMenuShortcut,
189 | DropdownMenuGroup,
190 | DropdownMenuPortal,
191 | DropdownMenuSub,
192 | DropdownMenuSubContent,
193 | DropdownMenuSubTrigger,
194 | DropdownMenuRadioGroup,
195 | }
196 |
--------------------------------------------------------------------------------
/src/app/create/case/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useState, useEffect } from 'react';
4 | import { Button } from '@/components/Button';
5 | import Image from 'next/image';
6 | import {
7 | Pagination,
8 | PaginationItem,
9 | PaginationLink,
10 | PaginationNext,
11 | PaginationPrevious,
12 | } from "@/components/ui/pagination"
13 | import { useToast } from '@/components/ui/use-toast';
14 |
15 | function CreateCase() {
16 | const [name, setName] = useState('');
17 | const [image, setImage] = useState('');
18 | const [allItems, setAllItems] = useState([]);
19 | const [selectedImage, setSelectedImage] = useState('');
20 | const [selectedItems, setSelectedItems] = useState([]);
21 | const [odds, setOdds] = useState<{ [key: string]: number }>({});
22 | const [price, setPrice] = useState(0);
23 |
24 | const itemsPerPage = 4;
25 | const [currentPage, setCurrentPage] = useState(1);
26 | const totalPages = Math.ceil(allItems.length / itemsPerPage);
27 | const currentItems = allItems.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
28 |
29 | const { toast } = useToast();
30 |
31 | const fetchItems = () => {
32 | fetch('/api/item')
33 | .then((response) => {
34 | if (!response.ok) {
35 | throw new Error(`HTTP error! status: ${response.status}`);
36 | }
37 | return response.json();
38 | })
39 | .then((data) => {
40 | if (Array.isArray(data.items)) {
41 | setAllItems(data.items);
42 | } else {
43 | console.error('Data.items is not an array:', data.items);
44 | }
45 | })
46 | .catch((error) => console.error(error));
47 | };
48 |
49 | useEffect(fetchItems, []);
50 | useEffect(() => {
51 | let newPrice = 0;
52 | selectedItems.forEach(itemId => {
53 | const item = allItems.find(item => item.id === itemId);
54 | if (item) {
55 | const itemOdds = odds[itemId] || 0;
56 | newPrice += item.price * (itemOdds / 100);
57 | }
58 | });
59 | setPrice(newPrice);
60 | }, [selectedItems, odds, allItems]);
61 |
62 | const handleItemClick = (item: any) => {
63 | if (selectedItems.includes(item.id)) {
64 | setSelectedItems((prevItems) => prevItems.filter((prevItem) => prevItem !== item.id));
65 | setOdds((prevOdds) => {
66 | const newOdds = { ...prevOdds };
67 | delete newOdds[item.id];
68 | return newOdds;
69 | });
70 | } else {
71 | setSelectedItems((prevItems) => [...prevItems, item.id]);
72 | setOdds((prevOdds) => ({ ...prevOdds, [item.id]: 0 }));
73 | }
74 | };
75 |
76 | const handleSubmit = async (e: { preventDefault: () => void; }) => {
77 | e.preventDefault();
78 |
79 | if (selectedItems.length < 2) {
80 | toast({
81 | variant: 'destructive',
82 | title: "Invalid selection",
83 | description: "You must select at least two items.",
84 | });
85 | return;
86 | }
87 |
88 | if (!image) {
89 | toast({
90 | variant: 'destructive',
91 | title: "Invalid image",
92 | description: "You must select an image for the case.",
93 | });
94 | return;
95 | }
96 |
97 | const totalOdds = Object.values(odds).reduce((total, odds) => total + odds, 0);
98 | if (totalOdds !== 100) {
99 | toast({
100 | variant: 'destructive',
101 | title: "Invalid odds",
102 | description: "The odds must add up to 100%.",
103 | });
104 | return;
105 | }
106 |
107 | const caseData = {
108 | name,
109 | image,
110 | price,
111 | itemIds: selectedItems,
112 | odds: Object.values(odds),
113 | };
114 |
115 | try {
116 | const response = await fetch('/api/case', {
117 | method: 'POST',
118 | headers: {
119 | 'Content-Type': 'application/json',
120 | },
121 | body: JSON.stringify(caseData),
122 | });
123 |
124 | if (!response.ok) {
125 | toast({
126 | variant: 'destructive',
127 | title: "ERROR!",
128 | description: "There was an error creating the case.",
129 | });
130 | return;
131 | }
132 |
133 | const data = await response.json();
134 |
135 | toast ({
136 | variant: 'success',
137 | title: "Case " + name + " created",
138 | description: "Case with price " + price + " has been created.",
139 | });
140 |
141 | console.log(data);
142 | } catch (error) {
143 | console.error(error);
144 | }
145 | };
146 |
147 | return (
148 | <>
149 |
150 |
173 |
174 |
175 | {currentItems.map((item, index) => (
176 |
handleItemClick(item)}
180 | >
181 |
182 |
{item.name}
183 |
${item.price.toFixed(2)}
184 |
185 | ))}
186 |
187 |
188 | setCurrentPage((old) => Math.max(old - 1, 1))} />
189 | {Array.from({length: totalPages}, (_, i) => (
190 |
191 | setCurrentPage(i + 1)}>{i + 1}
192 |
193 | ))}
194 | setCurrentPage((old) => Math.min(old + 1, totalPages))} />
195 |
196 |
197 |
198 |
199 |
Total Price: ${price.toFixed(2)}
200 |
Input the odds
201 |
The odds must add up to 100%.
202 |
203 |
204 | {selectedItems.map(itemId => {
205 | const item = allItems.find(item => item.id === itemId);
206 | if (item) {
207 | const itemOdds = odds[itemId] || 0;
208 | const cost = item.price * itemOdds / 100;
209 | return (
210 |
211 |
{`${item.name}`}
212 |
setOdds((prevOdds) => ({ ...prevOdds, [itemId]: parseFloat(e.target.value)}))}
219 | />
220 |
${cost.toFixed(2)}
221 |
222 | );
223 | }
224 | return null;
225 | })}
226 |
227 | >
228 | );
229 | }
230 |
231 | export default CreateCase;
--------------------------------------------------------------------------------
/public/swagger.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | version: "1.0.0"
4 | title: "CirovBet API"
5 | description: "API documentation for the CirovBet application"
6 |
7 | servers:
8 | - url: https://next-api-orcin-beta.vercel.app/api/
9 |
10 | paths:
11 | /api/auth/[...nextauth]:
12 | get:
13 | summary: "NextAuth Authentication"
14 | description: "NextAuth authentication route."
15 | responses:
16 | 200:
17 | description: "Successful authentication response"
18 | content:
19 | application/json:
20 | example:
21 | message: "Authentication successful"
22 | user:
23 | id: 123
24 | username: "example_user"
25 | 401:
26 | description: "Failed authentication response"
27 | content:
28 | application/json:
29 | example:
30 | error: "Authentication failed"
31 | message: "Invalid credentials"
32 |
33 | /api/balance:
34 | get:
35 | summary: "Get User Balance"
36 | description: "Gets the balance of the current user session."
37 | responses:
38 | 200:
39 | description: "Successful GET balance response"
40 | content:
41 | application/json:
42 | example:
43 | balance: 100.50
44 | post:
45 | summary: "Update User Balance"
46 | description: "Updates the balance of the current user (only server can do this request)."
47 | requestBody:
48 | required: true
49 | content:
50 | application/json:
51 | schema:
52 | type: object
53 | properties:
54 | balance:
55 | type: number
56 | responses:
57 | 200:
58 | description: "Successful POST update balance response"
59 | content:
60 | application/json:
61 | example:
62 | message: "Balance updated successfully"
63 |
64 | /api/case:
65 | get:
66 | summary: "Get All Cases"
67 | description: "Returns all the cases from the database."
68 | responses:
69 | 200:
70 | description: "Successful GET cases response"
71 | content:
72 | application/json:
73 | example:
74 | - id: 1
75 | name: "Case 1"
76 | image: "case1.jpg"
77 | price: 10.0
78 | items: []
79 | odds: []
80 | history: []
81 | post:
82 | summary: "Create New Case"
83 | description: "Creates a new case (requires admin role)."
84 | requestBody:
85 | required: true
86 | content:
87 | application/json:
88 | schema:
89 | $ref: "#/components/schemas/NewCase"
90 | responses:
91 | 200:
92 | description: "Successful POST case creation response"
93 | content:
94 | application/json:
95 | example:
96 | message: "Case created successfully"
97 | case:
98 | id: 1
99 | name: "New Case"
100 | image: "new_case.jpg"
101 | price: 15.0
102 | items: []
103 | odds: []
104 | history: []
105 | put:
106 | summary: "Update Case"
107 | description: "Updates the case with the specified ID."
108 | parameters:
109 | - in: path
110 | name: id
111 | required: true
112 | schema:
113 | type: integer
114 | requestBody:
115 | required: true
116 | content:
117 | application/json:
118 | schema:
119 | $ref: "#/components/schemas/NewCase"
120 | responses:
121 | 200:
122 | description: "Successful PUT case update response"
123 | content:
124 | application/json:
125 | example:
126 | message: "Case updated successfully"
127 | delete:
128 | summary: "Delete Case"
129 | description: "Deletes the case with the specified ID."
130 | parameters:
131 | - in: path
132 | name: id
133 | required: true
134 | schema:
135 | type: integer
136 | responses:
137 | 200:
138 | description: "Successful DELETE case response"
139 | content:
140 | application/json:
141 | example:
142 | message: "Case deleted successfully"
143 |
144 | /api/case/{id}:
145 | get:
146 | summary: "Get Case by ID"
147 | description: "Returns the case with the specified ID."
148 | parameters:
149 | - in: path
150 | name: id
151 | required: true
152 | schema:
153 | type: integer
154 | responses:
155 | 200:
156 | description: "Successful GET case by ID response"
157 | content:
158 | application/json:
159 | $ref: "#/components/schemas/Case"
160 | 404:
161 | description: "Failed GET case by ID response"
162 | content:
163 | application/json:
164 | example:
165 | error: "Case not found"
166 | message: "The requested case ID does not exist"
167 |
168 | /api/item:
169 | get:
170 | summary: "Get All Items"
171 | description: "Returns all items from the database. Optional query parameters from and to for filtering."
172 | parameters:
173 | - in: query
174 | name: from
175 | schema:
176 | type: integer
177 | - in: query
178 | name: to
179 | schema:
180 | type: integer
181 | responses:
182 | 200:
183 | description: "Successful GET items response"
184 | content:
185 | application/json:
186 | example:
187 | - id: 1
188 | name: "Item 1"
189 | rarity: 2
190 | price: 5.0
191 | imageURL: "item1.jpg"
192 | caseId: 1
193 | case: null
194 | history: []
195 | odds: []
196 | post:
197 | summary: "Create New Item"
198 | description: "Creates a new item (requires admin role)."
199 | requestBody:
200 | required: true
201 | content:
202 | application/json:
203 | schema:
204 | $ref: "#/components/schemas/NewItem"
205 | responses:
206 | 200:
207 | description: "Successful POST item creation response"
208 | content:
209 | application/json:
210 | example:
211 | message: "Item created successfully"
212 | item:
213 | id: 1
214 | name: "New Item"
215 | rarity: 3
216 | price: 8.0
217 | imageURL: "new_item.jpg"
218 | caseId: null
219 | case: null
220 | history: []
221 | odds: []
222 | put:
223 | summary: "Update Item"
224 | description: "Updates the item with the specified ID."
225 | parameters:
226 | - in: query
227 | name: id
228 | required: true
229 | schema:
230 | type: integer
231 | requestBody:
232 | required: true
233 | content:
234 | application/json:
235 | schema:
236 | $ref: "#/components/schemas/NewItem"
237 | responses:
238 | 200:
239 | description: "Successful PUT item update response"
240 | content:
241 | application/json:
242 | example:
243 | message: "Item updated successfully"
244 | delete:
245 | summary: "Delete Item"
246 | description: "Deletes the item with the specified ID."
247 | parameters:
248 | - in: query
249 | name: id
250 | required: true
251 | schema:
252 | type: integer
253 | responses:
254 | 200:
255 | description: "Successful DELETE item response"
256 | content:
257 | application/json:
258 | example:
259 | message: "Item deleted successfully"
260 |
261 | /api/items:
262 | post:
263 | summary: "Create Bulk Items"
264 | description: "Creates bulk items."
265 | requestBody:
266 | required: true
267 | content:
268 | application/json:
269 | schema:
270 | type: object
271 | properties:
272 | items:
273 | type: array
274 | items:
275 | $ref: "#/components/schemas/NewItem"
276 | responses:
277 | 200:
278 | description: "Successful bulk item creation response"
279 | content:
280 | application/json:
281 | example:
282 | message: "Bulk items created successfully"
283 | items:
284 | - id: 1
285 | name: "Bulk Item 1"
286 | rarity: 2
287 | price: 5.0
288 | imageURL: "bulk_item1.jpg"
289 | caseId: null
290 | case: null
291 | history: []
292 | odds: []
293 |
294 | /api/open:
295 | post:
296 | summary: "Open Case"
297 | description: "Opens a case and returns an item based on the odds."
298 | requestBody:
299 | required: true
300 | content:
301 | application/json:
302 | schema:
303 | type: object
304 | properties:
305 | caseId:
306 | type: integer
307 | responses:
308 | 200:
309 | description: "Successful case opening response"
310 | content:
311 | application/json:
312 | example:
313 | item:
314 | id: 1
315 | name: "Opened Item"
316 | rarity: 2
317 | price: 5.0
318 | imageURL: "opened_item.jpg"
319 | caseId: null
320 | case: null
321 | history: []
322 | odds: []
323 | 404:
324 | description: "Failed case opening response"
325 | content:
326 | application/json:
327 | example:
328 | error: "Case not found"
329 | message: "The requested case ID does not exist"
330 |
331 | /api/rmrf:
332 | delete:
333 | summary: "Delete Everything"
334 | description: "Deletes everything from the database (requires admin role)."
335 | responses:
336 | 200:
337 | description: "Successful database deletion response"
338 | content:
339 | application/json:
340 | example:
341 | message: "Database cleared successfully"
342 |
343 | /api/user:
344 | get:
345 | summary: "Get User"
346 | description: "Returns the user from the session."
347 | responses:
348 | 200:
349 | description: "Successful GET user response"
350 | content:
351 | application/json:
352 | example:
353 | user:
354 | id: 1
355 | username: "example_user"
356 | email: "example_user@example.com"
357 | image: "https://avatars.githubusercontent.com/u/79216061?v=4"
358 | balance: 123
359 | role: 0
360 |
361 | components:
362 | schemas:
363 | Case:
364 | type: object
365 | properties:
366 | id:
367 | type: integer
368 | name:
369 | type: string
370 | image:
371 | type: string
372 | price:
373 | type: number
374 | items:
375 | type: array
376 | items:
377 | $ref: "#/components/schemas/Item"
378 | odds:
379 | type: array
380 | items:
381 | $ref: "#/components/schemas/Odds"
382 | history:
383 | type: array
384 | items:
385 | $ref: "#/components/schemas/History"
386 |
387 | NewCase:
388 | type: object
389 | properties:
390 | name:
391 | type: string
392 | image:
393 | type: string
394 | price:
395 | type: number
396 | itemIds:
397 | type: array
398 | items:
399 | type: integer
400 | odds:
401 | type: array
402 | items:
403 | type: integer
404 |
405 | Item:
406 | type: object
407 | properties:
408 | id:
409 | type: integer
410 | name:
411 | type: string
412 | rarity:
413 | type: integer
414 | price:
415 | type: number
416 | imageURL:
417 | type: string
418 | caseId:
419 | type: integer
420 | case:
421 | $ref: "#/components/schemas/Case"
422 | history:
423 | type: array
424 | items:
425 | $ref: "#/components/schemas/History"
426 | odds:
427 | type: array
428 | items:
429 | $ref: "#/components/schemas/Odds"
430 |
431 | NewItem:
432 | type: object
433 | properties:
434 | name:
435 | type: string
436 | rarity:
437 | type: integer
438 | price:
439 | type: number
440 | imageURL:
441 | type: string
442 |
443 | Odds:
444 | type: object
445 | properties:
446 | id:
447 | type: integer
448 | caseId:
449 | type: integer
450 | itemId:
451 | type: integer
452 | case:
453 | $ref: "#/components/schemas/Case"
454 | item:
455 | $ref: "#/components/schemas/Item"
456 | Odds:
457 | type: number
458 |
459 | History:
460 | type: object
461 | properties:
462 | id:
463 | type: integer
464 | caseId:
465 | type: integer
466 | itemId:
467 | type: integer
468 | userId:
469 | type: string
470 | user:
471 | $ref: "#/components/schemas/User"
472 | case:
473 | $ref: "#/components/schemas/Case"
474 | item:
475 | $ref: "#/components/schemas/Item"
476 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | @layer base {
8 | :root {
9 | --background: 224 71% 4%;
10 | --foreground: 213 31% 91%;
11 |
12 | --muted: 223 47% 11%;
13 | --muted-foreground: 215.4 16.3% 56.9%;
14 |
15 | --accent: 216 34% 17%;
16 | --accent-foreground: 210 40% 98%;
17 |
18 | --popover: 224 71% 4%;
19 | --popover-foreground: 215 20.2% 65.1%;
20 |
21 | --border: 216 34% 17%;
22 | --input: 216 34% 17%;
23 |
24 | --card: 224 71% 4%;
25 | --card-foreground: 213 31% 91%;
26 |
27 | --primary: 210 40% 98%;
28 | --primary-foreground: 222.2 47.4% 1.2%;
29 |
30 | --secondary: 222.2 47.4% 11.2%;
31 | --secondary-foreground: 210 40% 98%;
32 |
33 | --destructive: 0 63% 31%;
34 | --destructive-foreground: 210 40% 98%;
35 |
36 | --success: 145 63% 31%;
37 | --success-foreground: 210 40% 98%;
38 |
39 | --ring: 216 34% 17%;
40 |
41 | --radius: 0.5rem;
42 | }
43 | }
44 |
45 | @layer base {
46 | * {
47 | @apply border-border;
48 | }
49 | body {
50 | @apply bg-background text-foreground;
51 | font-feature-settings: "rlig" 1, "calt" 1;
52 | }
53 | }
54 |
55 | :root {
56 | --foreground-rgb: 0, 0, 0;
57 | --background-start-rgb: 214, 219, 220;
58 | --background-end-rgb: 255, 255, 255;
59 | }
60 |
61 | @media (prefers-color-scheme: dark) {
62 | :root {
63 | --foreground-rgb: 255, 255, 255;
64 | --background-start-rgb: 0, 0, 0;
65 | --background-end-rgb: 0, 0, 0;
66 | --nav-col: #22033c;
67 | }
68 | }
69 |
70 | body {display: flex; flex-direction: column; color: white;background: rgb(64,26,74);background: linear-gradient(90deg, rgba(64,26,74,1) 0%, rgba(2,0,36,1) 50%, rgba(65,20,77,1) 100%);}
71 | input::-webkit-outer-spin-button,input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0;}
72 | input[type=number] {-moz-appearance: textfield;}
73 | input:focus{outline: none;}
74 | input[type="radio"] {appearance: none;cursor: pointer;}
75 | input[type="radio"] {padding: 10px;background: #000000;border-radius: 50%;transition: linear 0.2s;}
76 | input[type="radio"]:hover {background: #363636;transition: linear 0.2s;}
77 | input[type="radio"]:checked {transition: linear 0.2s;background: #bbbbbb76;}
78 | .m-15 {margin: 15px;}
79 | .popupcol {background: rgba(10, 7, 11, 0.853);}
80 | .mw-90 {max-width: 90%;}
81 | .width-60 {width: 60%;}
82 | .width-35 {width: 35%;}
83 | .success {background-color: #2fd26e84; color: white;}
84 | .accordion{min-width: 100%;list-style: none; padding: 0; margin: 0;}
85 | .accordion-title{color: white; font-weight: 900; text-transform: uppercase;font-size: larger;}
86 | .br-8 {border-radius: 8px;}
87 | @media (max-width: 640px) {.xsm\:flex-wrap {flex-wrap: wrap;}}
88 | .button-poligon {
89 | font-family: Montserrat;
90 | font-style: normal;
91 | font-weight: 900;
92 | text-transform: uppercase;
93 | height: 50px;
94 | width: 250px;
95 | font-size: large;
96 | padding: 30px;
97 | -webkit-clip-path: polygon(17px 0,calc(100% - 17px) 0,100% 50%,calc(100% - 17px) 100%,17px 100%,0 50%);
98 | clip-path: polygon(17px 0,calc(100% - 17px) 0,100% 50%,calc(100% - 17px) 100%,17px 100%,0 50%);
99 | background: #571290;
100 | box-shadow: 0px 5px 50px black;
101 | transition: 0.2s linear;
102 | }
103 | @media (max-width: 1280px) {.button-poligon {width: 200px; height: 30px; font-size: medium;}}
104 | @media (max-width: 680px) {.button-poligon {width: 100px; height: 10px; font-size: x-small;}}
105 | .avatar-hover{transition: 0.3s linear;}
106 | .avatar-hover:hover {filter: brightness(1.3);transition: 0.3s linear;}
107 | .banner{
108 | background-image: url('/banner.webp');
109 | background-position: 50%;
110 | background-repeat: no-repeat;
111 | margin: 50px 50px;
112 | border-radius: 12px;
113 | padding: 0 20px;
114 | max-width: 1920px;
115 | min-height: 460px;
116 | box-shadow: 0px 0px 500px black;
117 | box-sizing: border-box;
118 | transition: 0.5s linear;
119 | }
120 | .banner:hover{
121 | filter: brightness(1.2);
122 | box-shadow: 0px 0px 500px rgba(255, 255, 255, 0.09);
123 | transition: 0.5s linear;
124 | }
125 | .banner-content{
126 | align-items: flex-end;
127 | display: flex;
128 | font-family: Montserrat;
129 | justify-content: space-between;
130 | margin: 0 auto;
131 | max-width: 1340px;
132 | min-height: 460px;
133 | padding: 0 0 56px;
134 | }
135 | .button-poligon:hover {
136 | background: #742cb8;
137 | transition: 0.2s linear;
138 | }
139 | .profile-corner{
140 | display: flex;
141 | justify-content: flex-end;
142 | }
143 | .profile-balance-link{
144 | margin-right: 20px;
145 | color: #45d34a;
146 | font-weight: 500;
147 | padding-left: 15px;
148 | padding-right: 15px;
149 | align-self: center;
150 | border-radius: 8px;
151 | transition: 0.2s linear;
152 | }
153 | .profile-balance-link:hover{
154 | color: #369f3a;
155 | transition: 0.2s linear;
156 | }
157 | .flex-basis-25{flex-basis: 25%;}
158 | .case {
159 | background-color: #ffffff13;
160 | border-radius: 10px;
161 | padding: 20px;
162 | text-align: center;
163 | transition: 0.2s linear;
164 | }
165 | .case:hover{
166 | padding: 22px;
167 | background-color: #ffffff2f;
168 | transition: 0.2s linear;
169 | }
170 | .case p{
171 | margin-top: 15px;
172 | background-color: rgba(0, 0, 0, 0.138);
173 | padding: 10px;
174 | padding-left: 30px;
175 | padding-right: 30px;
176 | border-radius: 12px;
177 | color: #4af1b8;
178 | transition: 0.2s linear;
179 | }
180 | .case p:hover{
181 | background-color: rgba(0, 0, 0, 0.438);
182 | transition: 0.2s linear;
183 | }
184 | .case h2{
185 | font-size: large;
186 | }
187 | .case-menus{
188 | margin-bottom: 50px;
189 | }
190 | .case-page-container{
191 | display: flex;
192 | flex-direction: column;
193 | justify-content: center;
194 | align-items: center;
195 | margin-top: 100px;
196 | }
197 | .case-page-items-animation{
198 | position: absolute;
199 | left: 0;
200 | width: 60000px;
201 | transform: translateX(3000px);
202 | transition: all 5s cubic-bezier(0.49, 0.23, 0.4, 0.98);
203 | transition-delay: 1s;
204 | }
205 | .case-item-opening{
206 | position: relative;
207 | display: inline-block;
208 | background-color: #ffffff13;
209 | margin: 0px 5px;
210 | border-radius: 3px;
211 | width: 200px;
212 | height: 180px;
213 | padding: 20px;
214 | text-align: center;
215 | }
216 | .spin-case{
217 | transform: translateX(-3000px);
218 | transition: all 4s cubic-bezier(0.49, 0.23, 0.4, 0.98);
219 | }
220 | .case-page-container h1{
221 | font-size: 30px;
222 | font-weight: 900;
223 | text-transform: uppercase;
224 | color: white;
225 | margin-bottom: 20px;
226 | }
227 | .case-page-items{
228 | -webkit-clip-path: polygon(75px 0,calc(100% - 75px) 0,100% 50%,calc(100% - 75px) 100%,75px 100%,0 50%);
229 | clip-path: polygon(75px 0,calc(100% - 75px) 0,100% 50%,calc(100% - 75px) 100%,75px 100%,0 50%);
230 | background: #5712905a;
231 | width: 80%;
232 | height: 300px;
233 | border: 3px solid #571290a8;
234 | box-shadow: 0px 5px 50px black;
235 | display: flex;
236 | justify-content: center;
237 | align-items: center;
238 | margin-bottom: 50px;
239 | position: relative;
240 | overflow: hidden;
241 | }
242 | .case-page-items.rotating::after {
243 | content: "";
244 | position: absolute;
245 | top: 0;
246 | left: 50%;
247 | height: 100%;
248 | width: 2px; /* Adjust as needed */
249 | background-color: #ffffff; /* Adjust as needed */
250 | }
251 |
252 | .items-in-case{
253 | display: flex;
254 | justify-content: center;
255 | background : #5712905a;
256 | width: 90%;
257 | margin: 0px auto;
258 | margin-bottom: 100px;
259 | padding: 30px;
260 | border-radius: 16px;
261 | }
262 | .item{margin: 15px; max-height: 220px;}
263 | .items-heading{margin-top: 150px;}
264 | .case-open-button{
265 | background-color: #7a1b97;
266 | }
267 | .case-open-button:hover{
268 | background-color: #9b1fc0;
269 | }
270 | .image-transition {transition: transform 0.5s, opacity 0.5s;}
271 | .image-transition.hide {transform: translateY(200px);opacity: 0;}
272 | .nav-col{
273 | background-color: var(--nav-col);
274 | box-shadow: 0px 5px 50px black;;
275 | }
276 | .sign-in-btn{
277 | padding-left: 18px;
278 | padding-right: 18px;
279 | padding-top: 8px;
280 | padding-bottom: 8px;
281 | color: white;
282 | border: 1px solid white;
283 | border-radius: 8px;
284 | transition: 0.2s linear;
285 | }
286 | .sign-in-btn:hover{
287 | color: black;
288 | background-color: aliceblue;
289 | border: 1px solid black;
290 | transition: 0.2s linear;
291 | }
292 |
293 | .openButton {
294 | background-color: #4caf50;
295 | color: white;
296 | padding: 10px 20px;
297 | font-size: 16px;
298 | cursor: pointer;
299 | border: none;
300 | border-radius: 5px;
301 | }
302 | .openButton:hover {
303 | background-color: #45a049;
304 | }
305 | .dropDownTrans{
306 | background-color: rgba(60, 7, 95, 0.877);
307 | border: none;
308 | color: white;
309 | box-shadow: 0px 5px 50px black;
310 | min-width: 220px;
311 | }
312 | .dropDownItem{
313 | padding: 10px;
314 | cursor: pointer;
315 | border-top: 1px solid #ccc;
316 | transition: 0.2s linear;
317 | }
318 | .dropDownItem:hover{
319 | background-color: rgba(255, 255, 255, 0.304);
320 | }
321 | .footer {
322 | background-color: #020024;
323 | color: white;
324 | padding: 20px;
325 | text-align: center;
326 | font-size: 12px;
327 | width: 100%;
328 | margin-top: auto;
329 | bottom: 0;
330 | }
331 | .roulette-div{
332 | display: flex;
333 | flex-direction: column;
334 | justify-content: center;
335 | align-items: center;
336 | margin-top: 250px;
337 | }
338 | .roulette-wrapper{
339 | position:relative;
340 | display:flex;
341 | justify-content:center;
342 | width:100%;
343 | margin:0 auto;
344 | overflow:hidden;
345 | }
346 | .roulette-wrapper .selector{
347 | width:3px;
348 | background:grey;
349 | left:50%;
350 | height:100%;
351 | transform:translate(-50%,0%);
352 | position:absolute;
353 | z-index:2;
354 | }
355 | .roulette-wrapper .wheel{display:flex;}
356 | .roulette-wrapper .wheel .row{display:flex;}
357 | .roulette-wrapper .wheel .row .card{
358 | order: var(--order);
359 | height:75px;
360 | width:75px;
361 | margin:3px;
362 | border-radius:8px;
363 | border-bottom:3px solid rgba(0,0,0,0.2);
364 | display:flex;
365 | align-items:center;
366 | justify-content:center;
367 | color:white;
368 | font-size:1.5em;
369 | }
370 | .card.red{background:#F95146;}
371 | .card.black{background:#2D3035;}
372 | .card.green{background:#00C74D;}
373 | .red-rarity{background-color: #dc181877;}
374 | .red-rarity:hover{background-color: #dc1818a7;}
375 | .blue-rarity{background-color: #182fdc48;}
376 | .blue-rarity:hover{background-color: #311ec578;}
377 | .gold-rarity{background-color: #fcdf0167;}
378 | .gold-rarity:hover{background-color: #a09e1dae;}
379 | .blue-rarity-box-shadow{box-shadow: inset 0px -41px 39px -26px rgba(34,34,230,0.64); border-bottom: 3px solid rgba(34,34,230,0.64)}
380 | .red-rarity-box-shadow{box-shadow: inset 0px -41px 39px -26px rgba(230, 34, 34, 0.64); border-bottom: 3px solid rgba(230, 34, 34, 0.64)}
381 | .gold-rarity-box-shadow{box-shadow: inset 0px -41px 39px -26px rgba(230, 230, 34, 0.64); border-bottom: 8px solid rgba(230, 230, 34, 0.64)}
382 |
383 | .coin-container{
384 | display: flex;
385 | align-items: center;
386 | flex-direction: column;
387 | margin: auto;
388 | background-color: #5028a13a;
389 | width: 65%;
390 | padding: 50px;
391 | box-shadow: 15px 30px 35px rgba(0,0,0,0.1);
392 | border-radius: 10px;
393 | box-shadow: 0px 1px 150px black;
394 | }
395 | .coin-container input{
396 | width: 45%;
397 | padding: 10px;
398 | border: none;
399 | border-radius: 5px;
400 | margin-bottom: 20px;
401 | font-size: 16px;
402 | font-weight: 500;
403 | color: white;
404 | text-align: center;
405 | background-color: #5028a13a;
406 | box-shadow: 0px 5px 50px black;
407 | }
408 | .coin{
409 | margin: 100px auto;
410 | transform-style: preserve-3d;
411 | display: flex;
412 | flex-direction: column;
413 | justify-content: center;
414 | align-items: center;
415 | -webkit-transform-style: preserve-3d;
416 | transform-style: preserve-3d;
417 | }
418 | .coin-heads {transform: rotateX(0deg);}
419 | .coin-tails {transform: rotateX(180deg);}
420 | .coin img{width: 145px;}
421 | .heads,.tails{
422 | position: absolute;
423 | -webkit-backface-visibility: hidden;
424 | backface-visibility: hidden;
425 | }
426 | .tails{transform: rotateX(180deg);}
427 | @keyframes spin-tails{
428 | 0%{
429 | transform: rotateX(0);
430 | }
431 | 100%{
432 | transform: rotateX(1980deg);
433 | }
434 | }
435 | @keyframes spin-heads{
436 | 0%{
437 | transform: rotateX(0);
438 | }
439 | 100%{
440 | transform: rotateX(2160deg);
441 | }
442 | }
443 | .buttons{
444 | display: flex;
445 | justify-content: space-between;
446 | }
447 | .history{
448 | display: flex;
449 | overflow: hidden;
450 | }
451 |
452 | /* CREATE PAGE */
453 | .item-case{
454 | display: flex;
455 | justify-content: center;
456 | align-items: center;
457 | margin: auto;
458 | background-color: #5712905a;
459 | width: 80%;
460 | height: 300px;
461 | border: 3px solid #571290a8;
462 | box-shadow: 0px 5px 50px black;
463 | margin-bottom: 50px;
464 | transition: 0.5s linear;
465 | }
466 | .item-case a{
467 | background-color: #571290;
468 | color: white;
469 | padding: 30px 25px;
470 | font-size: 16px;
471 | cursor: pointer;
472 | border: none;
473 | margin: auto 20px;
474 | border-radius: 5px;
475 | transition: 0.2s linear;
476 | }
477 | .item-case a:hover{
478 | background-color: #742cb8;
479 | transition: 0.2s linear;
480 | }
481 | .case-create{
482 | display: flex;
483 | align-items: center;
484 | vertical-align: middle;
485 | flex-direction: row;
486 | margin: auto;
487 | background-color: #5712905a;
488 | width: 80%;
489 | height: 550px;
490 | box-shadow: 0px 5px 50px black;
491 | border-radius: 8px;
492 | margin-bottom: 50px;
493 | transition: 0.5s linear;
494 | }
495 | .case-create form{
496 | display: flex;
497 | flex-direction: column;
498 | align-items: center;
499 | border-radius: 8px;
500 | transition: 0.5s linear;
501 | }
502 | .case-create input{
503 | width: 80%;
504 | padding: 5px;
505 | border: none;
506 | border-radius: 5px;
507 | margin: 10px;
508 | margin-bottom: 15px;
509 | font-size: 16px;
510 | font-weight: 500;
511 | color: white;
512 | text-align: center;
513 | background-color: #5712905a;
514 | }
515 | .case-create input[type="radio"]{
516 | width: 20px;
517 | height: 20px;
518 | margin: 0 12px;
519 | }
520 | .case-create-item{
521 | width: 30%;
522 | height: 30%;
523 | font-size: medium;
524 | transition: 0.2s linear;
525 | }
526 | .case-create-item{
527 | width: 35%;
528 | height: fit-content;
529 | font-size: small;
530 | cursor: pointer;
531 | }
532 | .case-create-item p{
533 | background-color: transparent;
534 | margin-top: 0px;
535 | }
536 | .case-create-item p:hover{
537 | background-color: transparent;
538 | color:#00C74D
539 | }
540 |
541 | .balance-page{
542 | display: flex;
543 | flex-direction: column;
544 | justify-content: center;
545 | align-items: center;
546 | vertical-align: middle;
547 | margin: auto;
548 | background-color: #5712905a;
549 | width: 80%;
550 | height: 550px;
551 | box-shadow: 0px 5px 50px black;
552 | border-radius: 8px;
553 | margin-bottom: 50px;
554 | transition: 0.5s linear;
555 | }
556 | .case-create-items-images{width: 80%;}
557 | .item-create-image{
558 | background-color: rgba(255, 255, 255, 0.13);
559 | border-radius: 8px;
560 | margin: 10px 10px;
561 | transition: 0.5s linear;
562 | }
563 | .item-create-image img {transition: all 0.3s linear;}
564 | .item-create-image:hover{background-color: rgba(255, 255, 255, 0.39);transition: 0.5s linear;}
565 | .selected, .selected:hover{background-color: rgba(0, 0, 0, 0.505);}
566 | .skeleton-image {width: 160px;height: 216px; margin: 10px; background-color: rgba(116, 4, 136, 0.447);}
567 | .skeleton-case-image {width: 250px;height: 200px; margin: 10px; border-radius: 10px; background-color: rgba(116, 4, 136, 0.447);}
568 | .skeleton-color {background-color: rgba(116, 4, 136, 0.447);}
569 | .skeleton-balance {background-color: #2c973079; width: 100px; height: 24px; margin-top: 5px; margin-right: 25px;}
570 | .case-odds{
571 | display: flex;
572 | flex-direction: column;
573 | margin: 0px 10px;
574 | margin-bottom: 50px;
575 | align-items: center;
576 | background-color: #5712905a;
577 | box-shadow: 0px 5px 50px black;
578 | padding: 10px;
579 | height: 250px;
580 | border-radius: 8px;
581 | transition: 0.5s linear;
582 | }
583 | .case-odds input{
584 | width: 80%;
585 | padding: 5px;
586 | border: none;
587 | border-radius: 5px;
588 | margin: 10px;
589 | margin-bottom: 15px;
590 | font-size: 16px;
591 | font-weight: 500;
592 | color: white;
593 | text-align: center;
594 | background-color: #5712905a;
595 | }
596 | .total-price{
597 | color: #00C74D;
598 | background-color: #5712905a;
599 | margin-bottom: 30px;
600 | box-shadow: 0px 5px 50px black;
601 | padding: 20px;
602 | border-radius: 8px;
603 | }
604 | .price{color:#00C74D}
605 | .percentage{
606 | font-size: smaller;
607 | font-weight: bold;
608 | float: right;
609 | color: #ffffff;
610 | }
--------------------------------------------------------------------------------