sanity
This commit is contained in:
parent
a756a40572
commit
6bad98efd1
18 changed files with 540 additions and 209 deletions
|
@ -1,2 +1,2 @@
|
|||
semi: false
|
||||
singleQuote: true
|
||||
singleQuote: true
|
||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -14,4 +14,4 @@
|
|||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,15 @@
|
|||
"dev": "bunx --bun vite",
|
||||
"build": "tsc -b && bunx --bun vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "bunx --bun vite preview"
|
||||
"preview": "bunx --bun vite preview",
|
||||
"fmt": "bunx --bun prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eruda": "^3.0.1",
|
||||
"immer": "^10.1.1",
|
||||
"lucide-react": "^0.396.0",
|
||||
"react": "^18.3.1",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
html, body, #root {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
@apply h-full w-full;
|
||||
}
|
||||
}
|
||||
|
|
324
src/App.tsx
324
src/App.tsx
|
@ -2,25 +2,49 @@ import { create } from 'zustand'
|
|||
import './App.css'
|
||||
import { produce } from 'immer'
|
||||
import doska from './assets/doska.svg'
|
||||
import { useState } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { Button } from './components/ui/button'
|
||||
import { Input } from './components/ui/input'
|
||||
import { Label } from './components/ui/label'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from './components/ui/card'
|
||||
import { getPoints } from './lib/math'
|
||||
|
||||
type Throw =
|
||||
| {
|
||||
type: 'real'
|
||||
pts: number
|
||||
}
|
||||
| {
|
||||
type: 'bust'
|
||||
remaining: number
|
||||
}
|
||||
| {
|
||||
type: 'finish'
|
||||
}
|
||||
|
||||
interface Player {
|
||||
points: number
|
||||
throws: number
|
||||
throws: Throw[]
|
||||
pts: number
|
||||
}
|
||||
|
||||
interface Game {
|
||||
players: Player[]
|
||||
currentPlayerIdx: number
|
||||
round: number
|
||||
|
||||
maxThrows: number
|
||||
started: boolean
|
||||
startedAt: number
|
||||
o1: number
|
||||
|
||||
start(nplayers: number): void
|
||||
start(nplayers: number, mthrows: number, o1: number): void
|
||||
doThrow(pts: number, double: boolean): Throw
|
||||
finish(): number
|
||||
}
|
||||
|
||||
const useGame = create<Game>((set) => ({
|
||||
|
@ -29,59 +53,287 @@ const useGame = create<Game>((set) => ({
|
|||
round: 0,
|
||||
started: false,
|
||||
startedAt: -1,
|
||||
start(nplayers) {
|
||||
set(state => produce(state, state => {
|
||||
state.players = Array.from({ length: nplayers }, () => ({
|
||||
points: 0,
|
||||
throws: 0
|
||||
}))
|
||||
state.currentPlayerIdx = 0
|
||||
}))
|
||||
}
|
||||
maxThrows: 3,
|
||||
o1: 501,
|
||||
start(nplayers, mthrows, o1) {
|
||||
console.log('start', nplayers)
|
||||
set((state) =>
|
||||
produce(state, (state) => {
|
||||
state.o1 = o1
|
||||
state.round = 1
|
||||
state.started = true
|
||||
state.startedAt = new Date().getTime()
|
||||
state.maxThrows = mthrows
|
||||
state.players = Array.from({ length: nplayers }, () => ({
|
||||
pts: 0,
|
||||
throws: [],
|
||||
}))
|
||||
state.currentPlayerIdx = 0
|
||||
}),
|
||||
)
|
||||
},
|
||||
doThrow(pts, double): Throw {
|
||||
let th: Throw = { type: 'real', pts }
|
||||
set((state) =>
|
||||
produce(state, (state) => {
|
||||
if (
|
||||
state.players[state.currentPlayerIdx].throws.length >= state.maxThrows
|
||||
) {
|
||||
state.currentPlayerIdx =
|
||||
(state.currentPlayerIdx + 1) % state.players.length
|
||||
if (state.currentPlayerIdx === 0) {
|
||||
state.round++
|
||||
state.players.forEach(p => p.throws = [])
|
||||
}
|
||||
}
|
||||
const player = state.players[state.currentPlayerIdx]
|
||||
if (player.pts + pts === state.o1 && double) th = { type: 'finish' }
|
||||
else if (Math.abs(player.pts + pts - state.o1) <= 1) th = { type: 'bust', remaining: player.pts + pts - state.o1 }
|
||||
player.throws.push(th)
|
||||
if (th.type === 'real') player.pts += th.pts
|
||||
}),
|
||||
)
|
||||
return th
|
||||
},
|
||||
finish() {
|
||||
let idx: number = 0
|
||||
set((state) => {
|
||||
state.players.forEach((player, ix) => {
|
||||
console.log(player, ix)
|
||||
if (player.pts > state.players[idx].pts) {
|
||||
idx = ix
|
||||
console.log('winner', player, ix)
|
||||
}
|
||||
})
|
||||
return produce(state, (state) => {
|
||||
state.started = false
|
||||
state.currentPlayerIdx = -1
|
||||
})
|
||||
})
|
||||
console.log('winner', idx)
|
||||
return idx
|
||||
},
|
||||
}))
|
||||
|
||||
function fuckingGameTime(ms: number) {
|
||||
const duration = ms / 1000
|
||||
const hours = Math.floor(duration / 3600)
|
||||
const minutes = Math.floor((duration % 3600) / 60)
|
||||
const seconds = Math.floor(duration % 60)
|
||||
return `${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}:${seconds < 10 ? '0' + seconds : seconds}`
|
||||
}
|
||||
|
||||
function useTimer(startedAt: number) {
|
||||
const [time, setTime] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(
|
||||
() => setTime(new Date().getTime() - startedAt),
|
||||
1000,
|
||||
)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [startedAt])
|
||||
|
||||
return time
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [startedAt, started, round] = useGame(game => [game.startedAt, game.started, game.round])
|
||||
const [startedAt, started, round, doThrow, finish] = useGame((game) => [
|
||||
game.startedAt,
|
||||
game.started,
|
||||
game.round,
|
||||
game.doThrow,
|
||||
game.finish
|
||||
])
|
||||
const gameTime = useTimer(startedAt)
|
||||
const [points, setPoints] = useState(0)
|
||||
const [double, setDouble] = useState(false)
|
||||
const [winner, setWinner] = useState<number | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<div className='flex flex-col h-full w-full'>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
{/* Верхняя часть: доска */}
|
||||
<div className='flex flex-col h-full justify-start items-center'>
|
||||
<div className="flex flex-col h-full justify-between items-center">
|
||||
<div className="flex flex-row justify-between items-center w-full">
|
||||
<div className="flex flex-row justify-start">
|
||||
<Label htmlFor="round">Раунд</Label>
|
||||
<p id="round">{started ? `#${round}` : "Игра не начата"}</p></div>
|
||||
<p>{started ? new Date(Date.now() - startedAt).toLocaleTimeString() : ""}</p>
|
||||
<div className="flex flex-col justify-start">
|
||||
<Label htmlFor="round">Раунд</Label>
|
||||
<p id="round">#{round}</p>
|
||||
</div>
|
||||
{points > 0 ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (doThrow(points, double).type === 'finish')
|
||||
setWinner(finish())
|
||||
setPoints(0)
|
||||
}}
|
||||
>
|
||||
{points}
|
||||
</Button>
|
||||
) : null}
|
||||
<div className="flex flex-col justify-start">
|
||||
<Label htmlFor="time">Время</Label>
|
||||
<p id="time">{started ? fuckingGameTime(gameTime) : '00:00:00'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Board />
|
||||
<Board setPoints={setPoints} setDouble={setDouble} />
|
||||
</div>
|
||||
{/* Нижняя часть: управление игрой */}
|
||||
<Control />
|
||||
<Control {...{winner,setWinner}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Board() {
|
||||
const [points, setPoints] = useState(0)
|
||||
|
||||
function Board({
|
||||
setPoints,
|
||||
setDouble,
|
||||
}: {
|
||||
setPoints: (w: number) => void
|
||||
setDouble: (d: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<img src={doska} onMouseMove={e => {}} />
|
||||
<img
|
||||
src={doska}
|
||||
onClick={(e) => {
|
||||
const { points, double } = getPoints(e)
|
||||
setPoints(points)
|
||||
setDouble(double)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Control() {
|
||||
const [started, round, start, players] = useGame(game => [game.started, game.round, game.start, game.players])
|
||||
function Control({ winner, setWinner }: {winner: number | undefined, setWinner: (w: number | undefined) => void}) {
|
||||
const [started, start, players, finish] = useGame((game) => [
|
||||
game.started,
|
||||
game.start,
|
||||
game.players,
|
||||
game.finish,
|
||||
])
|
||||
const [nplayers, setNplayers] = useState(2)
|
||||
const [mthrows, setMthrows] = useState(3)
|
||||
const [o1, setO1] = useState(501)
|
||||
|
||||
return (
|
||||
<div className='flex flex-col h-full justify-start gap-2'>
|
||||
<Label htmlFor="nplayers">Количество игроков</Label>
|
||||
<Input id="nplayers" onChange={ev => setNplayers(parseInt(ev.target.value))} type="number" value={nplayers} disabled={started} />
|
||||
<div className="flex flex-col h-full justify-start gap-2">
|
||||
<div className="flex flex-row w-full justify-between items-center">
|
||||
<div className="flex flex-col">
|
||||
<Label htmlFor="nplayers">Игроки</Label>
|
||||
<Input
|
||||
id="nplayers"
|
||||
onChange={(ev) => setNplayers(parseInt(ev.target.value))}
|
||||
type="number"
|
||||
value={nplayers}
|
||||
disabled={started}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setWinner(undefined)
|
||||
start(nplayers, mthrows, o1)
|
||||
}}
|
||||
disabled={started}
|
||||
>
|
||||
{'Старт'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={() => start(nplayers)} disabled={started}>
|
||||
{"Начать игру"}
|
||||
</Button>
|
||||
<div className="flex flex-col">
|
||||
<Label htmlFor="mthrows">Дротики</Label>
|
||||
<Input
|
||||
id="mthrows"
|
||||
onChange={(ev) => setMthrows(parseInt(ev.target.value))}
|
||||
type="number"
|
||||
value={mthrows}
|
||||
disabled={started}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setWinner(undefined)
|
||||
start(nplayers, mthrows, o1)
|
||||
}}
|
||||
disabled={!started}
|
||||
>
|
||||
{'Рестарт'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Label htmlFor="o1">Макс. очков</Label>
|
||||
<Input
|
||||
id="o1"
|
||||
onChange={(ev) => setO1(parseInt(ev.target.value))}
|
||||
type="number"
|
||||
value={o1}
|
||||
disabled={started}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setWinner(finish())}
|
||||
disabled={!started}
|
||||
variant="destructive"
|
||||
>
|
||||
{'Стоп'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{started
|
||||
? players.map((player, idx) => (
|
||||
<Player player={player} idx={idx} key={idx}>
|
||||
{Array.from({ length: mthrows }, (_, i) => {
|
||||
const th = player.throws[i]
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex flex-row justify-start items-center gap-2"
|
||||
>
|
||||
<Label htmlFor={`throw-${idx}-${i}`}>Бросок #{i + 1}</Label>
|
||||
<p id={`throw-${idx}-${i}`}>
|
||||
{(() => {
|
||||
switch (th?.type) {
|
||||
case 'real':
|
||||
return th.pts
|
||||
case 'bust':
|
||||
return `не хватило ${th.remaining === 0 ? 'двойного' : `${th.remaining} очка`}`
|
||||
case 'finish':
|
||||
return ''
|
||||
default:
|
||||
return '0'
|
||||
}
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Player>
|
||||
))
|
||||
: null}
|
||||
{winner !== undefined ? (
|
||||
<Player idx={winner} player={players[winner]}>
|
||||
🎉 Игрок {winner + 1} выиграл!
|
||||
</Player>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function Player({
|
||||
player,
|
||||
children,
|
||||
idx,
|
||||
}: {
|
||||
player: Player
|
||||
children: ReactNode
|
||||
idx: number
|
||||
}) {
|
||||
const cidx = useGame(game => game.currentPlayerIdx)
|
||||
return (
|
||||
<Card key={idx} className={cidx === idx ? "bg-neutral-300" : ""}>
|
||||
<CardHeader>
|
||||
<CardTitle>Игрок #{idx + 1}</CardTitle>
|
||||
<CardDescription>Всего очков: {player.pts}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
|
@ -41,7 +41,7 @@ export interface ButtonProps
|
|||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
|
@ -49,8 +49,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
@ -9,13 +9,13 @@ const Card = React.forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
Card.displayName = 'Card'
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
@ -23,11 +23,11 @@ const CardHeader = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
CardHeader.displayName = 'CardHeader'
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
|
@ -36,13 +36,13 @@ const CardTitle = React.forwardRef<
|
|||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
'text-2xl font-semibold leading-none tracking-tight',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
CardTitle.displayName = 'CardTitle'
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
|
@ -50,19 +50,19 @@ const CardDescription = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
CardDescription.displayName = 'CardDescription'
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
CardContent.displayName = 'CardContent'
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
@ -70,10 +70,10 @@ const CardFooter = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
CardFooter.displayName = 'CardFooter'
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
@ -11,15 +11,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
|
|
126
src/index.css
126
src/index.css
|
@ -1,76 +1,76 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
|
77
src/lib/math.ts
Normal file
77
src/lib/math.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import React from 'react'
|
||||
|
||||
/* 9 27 45 63 81 99 117 135 153 171 189 207 225 243 261 279 297 315 333 351 */
|
||||
const ptSlice = [
|
||||
[1, 153, 171],
|
||||
[2, 27, 45],
|
||||
[3, 351, 360],
|
||||
[3, 0, 9],
|
||||
[4, 117, 135],
|
||||
[5, 189, 207],
|
||||
[6, 81, 99],
|
||||
[7, 315, 333],
|
||||
[8, 279, 297],
|
||||
[9, 225, 243],
|
||||
[10, 63, 81],
|
||||
[11, 261, 279],
|
||||
[12, 207, 225],
|
||||
[13, 99, 117],
|
||||
[14, 243, 261],
|
||||
[15, 45, 63],
|
||||
[16, 297, 315],
|
||||
[17, 9, 27],
|
||||
[18, 135, 153],
|
||||
[19, 333, 351],
|
||||
[20, 171, 189],
|
||||
]
|
||||
|
||||
/* Проверка дистанции */
|
||||
/* 3% 7.5% 44% 47% 71,5 75,5 */
|
||||
const prRanges = [
|
||||
[0, 50, 0.0, 3.0],
|
||||
[0, 25, 3.0, 7.5],
|
||||
[1, 0, 7.5, 38.0],
|
||||
[3, 0, 38.0, 47.3],
|
||||
[1, 0, 47.3, 65.2],
|
||||
[2, 0, 65.2, 75.5],
|
||||
[0, 0, 75.5, 150.0],
|
||||
]
|
||||
|
||||
export function getPoints(e: React.MouseEvent<HTMLImageElement>): {
|
||||
points: number
|
||||
double: boolean
|
||||
} {
|
||||
let points = 0
|
||||
let double = false
|
||||
let lims = e.currentTarget.getBoundingClientRect()
|
||||
|
||||
let cxval = e.clientX - lims.left - lims.width / 2
|
||||
let cyval = e.clientY - lims.top - lims.height / 2
|
||||
|
||||
let angleval = (Math.atan2(cxval, cyval) * 180) / Math.PI
|
||||
if (angleval < 0) angleval += 360
|
||||
|
||||
let distval = Math.sqrt(cxval * cxval + cyval * cyval)
|
||||
distval = (distval / (lims.width / 2)) * 100 /* in prercent */
|
||||
|
||||
for (let index = 0; index < ptSlice.length; index++) {
|
||||
if (angleval > ptSlice[index][1] && angleval <= ptSlice[index][2]) {
|
||||
points = ptSlice[index][0]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < prRanges.length; index++) {
|
||||
if (distval > prRanges[index][2] && distval <= prRanges[index][3]) {
|
||||
points = points * prRanges[index][0] + prRanges[index][1]
|
||||
if (prRanges[index][0] == 2) {
|
||||
double = true
|
||||
} else {
|
||||
double = false
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return { points, double }
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
import('eruda').then((eru) => eru.default.init())
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
|
|
@ -1,80 +1,80 @@
|
|||
import type { Config } from "tailwindcss"
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
darkMode: ['class'],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: "",
|
||||
],
|
||||
prefix: '',
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
'2xl': '1400px',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-accordion-content-height)' },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: '0' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
plugins: [require('tailwindcss-animate')],
|
||||
} satisfies Config
|
||||
|
||||
export default config
|
||||
export default config
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
{
|
||||
"files": [],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
|
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
|||
resolve: {
|
||||
alias: {
|
||||
// @ts-ignore
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue