sanity
This commit is contained in:
parent
a756a40572
commit
6bad98efd1
18 changed files with 540 additions and 209 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -7,13 +7,15 @@
|
||||||
"dev": "bunx --bun vite",
|
"dev": "bunx --bun vite",
|
||||||
"build": "tsc -b && bunx --bun vite build",
|
"build": "tsc -b && bunx --bun vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"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": {
|
"dependencies": {
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"eruda": "^3.0.1",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"lucide-react": "^0.396.0",
|
"lucide-react": "^0.396.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
html, body, #root {
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
@apply h-full w-full;
|
@apply h-full w-full;
|
||||||
}
|
}
|
312
src/App.tsx
312
src/App.tsx
|
@ -2,25 +2,49 @@ import { create } from 'zustand'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import doska from './assets/doska.svg'
|
import doska from './assets/doska.svg'
|
||||||
import { useState } from 'react'
|
import { ReactNode, useEffect, useState } from 'react'
|
||||||
import { Button } from './components/ui/button'
|
import { Button } from './components/ui/button'
|
||||||
import { Input } from './components/ui/input'
|
import { Input } from './components/ui/input'
|
||||||
import { Label } from './components/ui/label'
|
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 {
|
interface Player {
|
||||||
points: number
|
throws: Throw[]
|
||||||
throws: number
|
pts: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Game {
|
interface Game {
|
||||||
players: Player[]
|
players: Player[]
|
||||||
currentPlayerIdx: number
|
currentPlayerIdx: number
|
||||||
round: number
|
round: number
|
||||||
|
maxThrows: number
|
||||||
started: boolean
|
started: boolean
|
||||||
startedAt: number
|
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) => ({
|
const useGame = create<Game>((set) => ({
|
||||||
|
@ -29,59 +53,287 @@ const useGame = create<Game>((set) => ({
|
||||||
round: 0,
|
round: 0,
|
||||||
started: false,
|
started: false,
|
||||||
startedAt: -1,
|
startedAt: -1,
|
||||||
start(nplayers) {
|
maxThrows: 3,
|
||||||
set(state => produce(state, state => {
|
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 }, () => ({
|
state.players = Array.from({ length: nplayers }, () => ({
|
||||||
points: 0,
|
pts: 0,
|
||||||
throws: 0
|
throws: [],
|
||||||
}))
|
}))
|
||||||
state.currentPlayerIdx = 0
|
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() {
|
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 (
|
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-between items-center w-full">
|
||||||
<div className="flex flex-row justify-start">
|
<div className="flex flex-col justify-start">
|
||||||
<Label htmlFor="round">Раунд</Label>
|
<Label htmlFor="round">Раунд</Label>
|
||||||
<p id="round">{started ? `#${round}` : "Игра не начата"}</p></div>
|
<p id="round">#{round}</p>
|
||||||
<p>{started ? new Date(Date.now() - startedAt).toLocaleTimeString() : ""}</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>
|
</div>
|
||||||
|
|
||||||
<Board />
|
<Board setPoints={setPoints} setDouble={setDouble} />
|
||||||
</div>
|
</div>
|
||||||
{/* Нижняя часть: управление игрой */}
|
{/* Нижняя часть: управление игрой */}
|
||||||
<Control />
|
<Control {...{winner,setWinner}} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Board() {
|
function Board({
|
||||||
const [points, setPoints] = useState(0)
|
setPoints,
|
||||||
|
setDouble,
|
||||||
|
}: {
|
||||||
|
setPoints: (w: number) => void
|
||||||
|
setDouble: (d: boolean) => void
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<img src={doska} onMouseMove={e => {}} />
|
<img
|
||||||
|
src={doska}
|
||||||
|
onClick={(e) => {
|
||||||
|
const { points, double } = getPoints(e)
|
||||||
|
setPoints(points)
|
||||||
|
setDouble(double)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Control() {
|
function Control({ winner, setWinner }: {winner: number | undefined, setWinner: (w: number | undefined) => void}) {
|
||||||
const [started, round, start, players] = useGame(game => [game.started, game.round, game.start, game.players])
|
const [started, start, players, finish] = useGame((game) => [
|
||||||
|
game.started,
|
||||||
|
game.start,
|
||||||
|
game.players,
|
||||||
|
game.finish,
|
||||||
|
])
|
||||||
const [nplayers, setNplayers] = useState(2)
|
const [nplayers, setNplayers] = useState(2)
|
||||||
|
const [mthrows, setMthrows] = useState(3)
|
||||||
|
const [o1, setO1] = useState(501)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col h-full justify-start gap-2'>
|
<div className="flex flex-col h-full justify-start gap-2">
|
||||||
<Label htmlFor="nplayers">Количество игроков</Label>
|
<div className="flex flex-row w-full justify-between items-center">
|
||||||
<Input id="nplayers" onChange={ev => setNplayers(parseInt(ev.target.value))} type="number" value={nplayers} disabled={started} />
|
<div className="flex flex-col">
|
||||||
|
<Label htmlFor="nplayers">Игроки</Label>
|
||||||
<Button onClick={() => start(nplayers)} disabled={started}>
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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 * as React from 'react'
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const badgeVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||||
secondary:
|
secondary:
|
||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||||
outline: "text-foreground",
|
outline: 'text-foreground',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
|
|
|
@ -1,36 +1,36 @@
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
outline:
|
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:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: 'h-10 px-4 py-2',
|
||||||
sm: "h-9 rounded-md px-3",
|
sm: 'h-9 rounded-md px-3',
|
||||||
lg: "h-11 rounded-md px-8",
|
lg: 'h-11 rounded-md px-8',
|
||||||
icon: "h-10 w-10",
|
icon: 'h-10 w-10',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
size: "default",
|
size: 'default',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
|
@ -41,7 +41,7 @@ export interface ButtonProps
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : 'button'
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
@ -49,8 +49,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
Button.displayName = "Button"
|
Button.displayName = 'Button'
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
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<
|
const Card = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -9,13 +9,13 @@ const Card = React.forwardRef<
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
Card.displayName = "Card"
|
Card.displayName = 'Card'
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
const CardHeader = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -23,11 +23,11 @@ const CardHeader = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
CardHeader.displayName = "CardHeader"
|
CardHeader.displayName = 'CardHeader'
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
const CardTitle = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
|
@ -36,13 +36,13 @@ const CardTitle = React.forwardRef<
|
||||||
<h3
|
<h3
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-2xl font-semibold leading-none tracking-tight",
|
'text-2xl font-semibold leading-none tracking-tight',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
CardTitle.displayName = "CardTitle"
|
CardTitle.displayName = 'CardTitle'
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
const CardDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
|
@ -50,19 +50,19 @@ const CardDescription = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn('text-sm text-muted-foreground', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
CardDescription.displayName = "CardDescription"
|
CardDescription.displayName = 'CardDescription'
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
const CardContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ 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<
|
const CardFooter = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -70,10 +70,10 @@ const CardFooter = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
className={cn('flex items-center p-6 pt-0', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
CardFooter.displayName = "CardFooter"
|
CardFooter.displayName = 'CardFooter'
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
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
|
export interface InputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
@ -11,15 +11,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
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",
|
'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
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
Input.displayName = "Input"
|
Input.displayName = 'Input'
|
||||||
|
|
||||||
export { Input }
|
export { Input }
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const labelVariants = cva(
|
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<
|
const Label = React.forwardRef<
|
||||||
|
|
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 { type ClassValue, clsx } from 'clsx'
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
import('eruda').then((eru) => eru.default.init())
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|
|
@ -1,80 +1,80 @@
|
||||||
import type { Config } from "tailwindcss"
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
darkMode: ["class"],
|
darkMode: ['class'],
|
||||||
content: [
|
content: [
|
||||||
'./pages/**/*.{ts,tsx}',
|
'./pages/**/*.{ts,tsx}',
|
||||||
'./components/**/*.{ts,tsx}',
|
'./components/**/*.{ts,tsx}',
|
||||||
'./app/**/*.{ts,tsx}',
|
'./app/**/*.{ts,tsx}',
|
||||||
'./src/**/*.{ts,tsx}',
|
'./src/**/*.{ts,tsx}',
|
||||||
],
|
],
|
||||||
prefix: "",
|
prefix: '',
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
padding: "2rem",
|
padding: '2rem',
|
||||||
screens: {
|
screens: {
|
||||||
"2xl": "1400px",
|
'2xl': '1400px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border))",
|
border: 'hsl(var(--border))',
|
||||||
input: "hsl(var(--input))",
|
input: 'hsl(var(--input))',
|
||||||
ring: "hsl(var(--ring))",
|
ring: 'hsl(var(--ring))',
|
||||||
background: "hsl(var(--background))",
|
background: 'hsl(var(--background))',
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: 'hsl(var(--foreground))',
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: "hsl(var(--accent))",
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: "hsl(var(--popover))",
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: 'var(--radius)',
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: 'calc(var(--radius) - 2px)',
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
"accordion-down": {
|
'accordion-down': {
|
||||||
from: { height: "0" },
|
from: { height: '0' },
|
||||||
to: { height: "var(--radix-accordion-content-height)" },
|
to: { height: 'var(--radix-accordion-content-height)' },
|
||||||
},
|
},
|
||||||
"accordion-up": {
|
'accordion-up': {
|
||||||
from: { height: "var(--radix-accordion-content-height)" },
|
from: { height: 'var(--radix-accordion-content-height)' },
|
||||||
to: { height: "0" },
|
to: { height: '0' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [require('tailwindcss-animate')],
|
||||||
} satisfies Config
|
} satisfies Config
|
||||||
|
|
||||||
export default config
|
export default config
|
|
@ -3,10 +3,8 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./src/*"]
|
||||||
"./src/*"
|
}
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
"@": path.resolve(__dirname, "./src"),
|
'@': path.resolve(__dirname, './src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue