feat: add design system
Some checks failed
Deploy to VPS (dist) / deploy (push) Failing after 46s

This commit is contained in:
Hewston Fox
2026-03-12 00:42:41 +02:00
parent fcb8dab8a0
commit 55bf63e215
93 changed files with 1647 additions and 86 deletions

View File

@@ -1,4 +1,4 @@
{ {
"$schema": "./node_modules/oxfmt/configuration_schema.json", "$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": ["src/routeTree.gen.ts"] "ignorePatterns": ["**/routeTree.gen.ts"]
} }

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "./node_modules/oxlint/configuration_schema.json", "$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": ["src/routeTree.gen.ts"], "ignorePatterns": ["**/routeTree.gen.ts"],
"plugins": ["react", "react-perf", "import", "jsx-a11y", "promise"], "plugins": ["react", "react-perf", "import", "jsx-a11y", "promise"],
"rules": { "rules": {
"eqeqeq": ["error", "smart"], "eqeqeq": ["error", "smart"],

View File

@@ -22,6 +22,7 @@
"@tanstack/react-router": "^1.166.3", "@tanstack/react-router": "^1.166.3",
"@tanstack/react-router-devtools": "^1.166.3", "@tanstack/react-router-devtools": "^1.166.3",
"arktype": "^2.2.0", "arktype": "^2.2.0",
"clsx": "^2.1.1",
"i18next": "^25.8.17", "i18next": "^25.8.17",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"motion": "^12.35.1", "motion": "^12.35.1",

3
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
arktype: arktype:
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.0 version: 2.2.0
clsx:
specifier: ^2.1.1
version: 2.1.1
i18next: i18next:
specifier: ^25.8.17 specifier: ^25.8.17
version: 25.8.17(typescript@5.9.3) version: 25.8.17(typescript@5.9.3)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1 +1 @@
export declare const resources: { hello: "Hello World!" }; export declare const resources: { hello: "Hello World!"; "actionModal.close": "Close" };

View File

@@ -1,3 +1,4 @@
{ {
"hello": "Hello World!" "hello": "Hello World!",
"actionModal.close": "Close"
} }

View File

@@ -1 +1 @@
export declare const resources: { hello: "Привет мир" }; export declare const resources: { hello: "Привет мир"; "actionModal.close": "Закрыть" };

View File

@@ -1,3 +1,4 @@
{ {
"hello": "Привет мир" "hello": "Привет мир",
"actionModal.close": "Закрыть"
} }

View File

@@ -0,0 +1,85 @@
@layer base {
.baseButton {
cursor: pointer;
border-radius: 9999px;
padding: 10px;
font-weight: 700;
font-size: 18px;
line-height: 18px;
width: fit-content;
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
border-width: 0.7px;
border-style: solid;
text-shadow: 0px 1px 1px #00000059;
-webkit-text-stroke-width: 0.7px;
user-select: none;
&:disabled {
pointer-events: none;
}
}
.blueButton {
background: linear-gradient(180deg, #6cc3f1 0%, #3c6bbd 100%);
border-color: #012142;
box-shadow:
0px 3px 1px 1px #bfe9ff inset,
0px 2px 1px 1px #8ccef0 inset,
0px -3px 1px 1px #8fc9e966 inset,
0px -2px 1px 1px #29497f inset,
0px 1px 2px 0px #00000073;
color: #d9edff;
-webkit-text-stroke-color: #003a73;
}
.redButton {
background: linear-gradient(180deg, #f16c6c 0%, #a73030 100%);
border-color: #420101;
box-shadow:
0px 3px 1px 1px #ffbfbf inset,
0px 2px 1px 1px #f08c8c inset,
0px -3px 1px 1px #e98f8f66 inset,
0px -2px 1px 1px #7f2929 inset,
0px 1px 2px 0px #00000073;
color: #ffd9d9;
-webkit-text-stroke-color: #730000;
}
.greenButton {
background: linear-gradient(180deg, #5bdf5b 0%, #15812e 100%);
border-color: #00450a;
box-shadow:
0px 3px 1px 1px #9bff9f inset,
0px 2px 1px 1px #8cf08e inset,
0px -3px 1px 1px #8fe99b66 inset,
0px -2px 1px 1px #1d672d inset,
0px 1px 2px 0px #00000073;
color: #cdffd2;
-webkit-text-stroke-color: #007313;
}
.yellowButton {
background: linear-gradient(180deg, #ffbd42 0%, #e59a0f 100%);
border-color: #453100;
box-shadow:
0px 3px 1px 1px #ffefb2 inset,
0px 2px 1px 1px #ffe57b inset,
0px -3px 1px 1px #ffe9ab66 inset,
0px -2px 1px 1px #a88010 inset,
0px 1px 2px 0px #00000073;
color: #fffdcd;
-webkit-text-stroke-color: #735a00;
}
}

View File

@@ -0,0 +1,29 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import classes from "./Button.module.css";
type Props = Omit<HTMLMotionProps<"button">, "className"> & {
className?: ClassValue;
variant?: "blue" | "red" | "green" | "yellow";
};
const VARIANTS_MAP = {
blue: classes.blueButton,
green: classes.greenButton,
red: classes.redButton,
yellow: classes.yellowButton,
} satisfies Record<Exclude<Props["variant"], undefined>, string>;
export default function Button({ className, variant = "blue", ...props }: Props) {
return (
<motion.button
{...props}
initial={{ scale: 0.5 }}
animate={{ scale: 1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className={clsx(VARIANTS_MAP[variant], classes.baseButton, className)}
/>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./Button";

View File

@@ -0,0 +1,24 @@
import { motion, useMotionValue, useTransform, animate } from "motion/react";
import { useEffect } from "react";
import clsx, { type ClassValue } from "clsx";
type Props = {
className?: ClassValue;
value: number;
};
export default function Counter({ className, value }: Props) {
const motionValue = useMotionValue(0);
const display = useTransform(motionValue, (v) => Math.round(v).toLocaleString("fr-FR"));
useEffect(
() =>
animate(motionValue, value, {
duration: 0.3,
ease: "easeOut",
}).stop,
[motionValue, value],
);
return <motion.span className={clsx("tabular-nums", className)}>{display}</motion.span>;
}

View File

@@ -0,0 +1 @@
export { default } from "./Counter";

View File

@@ -0,0 +1,56 @@
@layer base {
.container {
position: relative;
width: 100%;
height: 24px;
background: transparent;
border-radius: 9999px;
overflow: hidden;
}
.progressBar {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: 9999px;
}
.yellowProgress {
background: #e08b0c;
box-shadow:
0px 3px 4px 0px #ffffff73 inset,
0px -3px 4px 0px #00000040 inset;
}
.greenProgress {
background: linear-gradient(180deg, #5bdf5b 0%, #15812e 100%);
border: 0.7px solid #00450a;
box-shadow:
0px 3px 1px 1px #9bff9f inset,
0px 2px 1px 1px #8cf08e inset,
0px -3px 1px 1px #8fe99b66 inset,
0px -2px 1px 1px #1d672d inset,
0px 1px 2px 0px #00000073;
}
.text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 18px;
line-height: 18px;
font-weight: 700;
white-space: nowrap;
}
.yellowText {
color: #ffdc9d;
}
.greenText {
color: #ffffffcc;
-webkit-text-stroke: 1px #00000080;
}
}

View File

@@ -0,0 +1,40 @@
import { motion } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import Counter from "@components/atoms/Counter";
import classes from "./Progress.module.css";
type Props = {
className?: ClassValue;
value: number;
max: number;
variant: "green" | "yellow";
};
const VARIANTS_MAP = {
green: classes.greenProgress,
yellow: classes.yellowProgress,
} satisfies Record<Props["variant"], string>;
const TEXT_VARIANTS_MAP = {
green: classes.greenText,
yellow: classes.yellowText,
} satisfies Record<Props["variant"], string>;
export default function Progress({ className, value, max, variant }: Props) {
const percentage = max > 0 ? (value / max) * 100 : 0;
return (
<div className={clsx(classes.container, className)}>
<motion.div
className={clsx(classes.progressBar, VARIANTS_MAP[variant])}
initial={{ width: 0 }}
animate={{ width: `${percentage}%` }}
transition={{ type: "spring", stiffness: 60, damping: 15 }}
/>
<span className={clsx(classes.text, TEXT_VARIANTS_MAP[variant])}>
<Counter value={value} /> / {<Counter value={max} />}
</span>
</div>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./Progress";

View File

@@ -0,0 +1,45 @@
@layer base {
.container {
padding: 4px;
display: flex;
border-radius: 9999px;
}
.tabsContainer {
position: relative;
flex: 1;
display: flex;
overflow: hidden;
border-radius: 9999px;
}
.tabs {
display: flex;
width: 100%;
flex: 1;
gap: 0;
}
.tab {
flex: 1;
background: transparent;
border: none;
cursor: pointer;
padding: 8px 12px;
color: #ffffff;
-webkit-text-stroke: 0.7px #331b01;
font-size: 18px;
line-height: 18px;
font-weight: 700;
position: relative;
z-index: 1;
}
.thumb {
position: absolute;
top: 0;
height: 100%;
border-radius: 9999px;
z-index: 0;
}
}

View File

@@ -0,0 +1,54 @@
import clsx, { type ClassValue } from "clsx";
import ContentSurface from "@components/surface/ContentSurface";
import DarkSurface from "@components/surface/DarkSurface";
import { motion, type HTMLMotionProps } from "motion/react";
import classes from "./TabSelector.module.css";
type Tab = {
key: string;
title: string;
};
type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
tabs: Tab[];
value?: string | null;
onChange?: (key: string) => void;
className?: ClassValue;
};
export default function TabSelector({ tabs, value, onChange, className, ...props }: Props) {
const selectedIndex = value != null ? tabs.findIndex((tab) => tab.key === value) : -1;
return (
<ContentSurface {...props} className={clsx(classes.container, className)}>
<div className={classes.tabsContainer}>
<div className={classes.tabs}>
{tabs.map((tab) => (
<motion.button
type="button"
key={tab.key}
whileTap={{ scale: 0.95 }}
onClick={() => onChange?.(tab.key)}
className={classes.tab}
>
{tab.title}
</motion.button>
))}
</div>
{selectedIndex >= 0 && (
<DarkSurface
className={classes.thumb}
initial={{ scale: 0.5 }}
animate={{
left: `${(selectedIndex / tabs.length) * 100}%`,
width: `${100 / tabs.length}%`,
scale: 1,
}}
transition={{ type: "spring", stiffness: 500, damping: 35 }}
/>
)}
</div>
</ContentSurface>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./TabSelector";

View File

@@ -0,0 +1,82 @@
@layer base {
.container {
width: 100%;
min-height: 35px;
height: 35px;
background: #0000004d;
border-top: 1px solid #472715;
box-shadow:
0px 4px 2px 0px #00000040 inset,
0px -1.5px 0px 0px #8a582d inset;
border-radius: 9999px;
outline: none;
display: flex;
align-items: center;
padding: 0 0 0 14px;
box-sizing: border-box;
overflow: hidden;
&.error {
outline: 2px solid #af4242;
}
}
.inputWrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
flex: 1;
height: 100%;
.input {
background: transparent;
border: none;
outline: none;
color: #fbe6be;
font-size: 18px;
line-height: 18px;
font-weight: 700;
text-align: center;
padding: 0;
min-width: 20px;
field-sizing: content;
align-self: stretch;
&::placeholder {
color: #fbe6be66;
}
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
.prefix {
color: #fbe6be;
font-size: 18px;
line-height: 18px;
font-weight: 700;
}
}
.iconButton {
margin: 0px 3px;
padding: 1px;
cursor: pointer;
border-radius: 9999px;
background: radial-gradient(circle, #00000000 0%, #00000000 50%, #fbe6be 50%, #fbe6be 100%);
box-shadow:
0px 2px 0px 0px #ffffffbf inset,
-1px 0px 0px 0px #00000059 inset,
1px 0px 0px 0px #00000059 inset,
0px -1px 0px 0px #00000059 inset;
.icon {
width: 28px;
color: #fbe6be;
}
}
}

View File

@@ -0,0 +1,75 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import { type ReactNode, useRef, useState, type ChangeEvent, useId } from "react";
import KeyboardIcon from "@components/icons/KeyboardIcon";
import classes from "./NumberInput.module.css";
type Props = Omit<HTMLMotionProps<"input">, "className" | "type" | "onChange"> & {
className?: ClassValue;
error?: boolean;
prefix?: ReactNode;
value?: number;
onChange?: (value: number) => void;
};
const NUMERIC_REGEX = /[^0-9.]/g;
function filterNumericInput(input: string): string {
const filtered = input.replace(NUMERIC_REGEX, "");
const parts = filtered.split(".");
return parts.length > 2 ? `${parts[0]}.${parts.slice(1).join("")}` : filtered;
}
export default function NumberInput({
className,
error,
prefix,
value,
onChange,
...props
}: Props) {
const stableId = useId();
const inputRef = useRef<HTMLInputElement>(null);
const id = props.id ?? stableId;
const [strValue, setStrValue] = useState(() => value?.toString() ?? "");
const prevValueRef = useRef(value);
if (prevValueRef.current !== value) {
prevValueRef.current = value;
setStrValue(value?.toString() ?? "");
}
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const normalized = filterNumericInput(e.target.value);
setStrValue(normalized);
const num = parseFloat(normalized);
onChange?.(isNaN(num) ? 0 : num);
};
return (
<div className={clsx(classes.container, error && classes.error, className)}>
<label className={classes.inputWrapper} htmlFor={id}>
{prefix && <span className={classes.prefix}>{prefix}</span>}
<motion.input
placeholder="0"
{...props}
id={id}
ref={inputRef}
type="text"
inputMode="numeric"
className={classes.input}
value={strValue}
onChange={handleChange}
/>
</label>
<motion.button
whileTap={{ scale: 0.95 }}
className={classes.iconButton}
onClick={() => inputRef.current?.focus()}
>
<KeyboardIcon className={classes.icon} />
</motion.button>
</div>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./NumberInput";

View File

@@ -0,0 +1,82 @@
@layer base {
.rangeInput {
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
--thumb-size: 30px;
--track-height: 10px;
width: 100%;
padding: calc(var(--thumb-size) / 2) 0;
height: 10px;
&::-webkit-slider-runnable-track {
border-radius: 9px;
height: var(--track-height);
background: #0000004d;
border-top: 1px solid #472715;
box-shadow:
0px 4px 2px 0px #00000040 inset,
0px -1.5px 0px 0px #8a582d inset;
}
&::-moz-range-track {
border-radius: 9px;
height: var(--track-height);
background: #0000004d;
border-top: 1px solid #472715;
box-shadow:
0px 4px 2px 0px #00000040 inset,
0px -1.5px 0px 0px #8a582d inset;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: var(--thumb-size);
height: var(--thumb-size);
margin-top: calc((var(--track-height) / 2) - var(--thumb-size) / 2);
background: #895428;
border-radius: 50%;
box-shadow:
-2px 0px 1px 0px #00000040 inset,
2px 0px 1px 0px #00000040 inset,
0px 2px 1px 0px #be7f4a inset,
0px -2px 1px 0px #3a1c09 inset,
0px 1px 0px 0px #00000033;
transition: transform 0.1s ease;
}
&:active::-webkit-slider-thumb {
transform: scale(1.1);
}
::-moz-range-thumb {
width: var(--thumb-size);
height: var(--thumb-size);
margin-top: calc((var(--track-height) / 2) - var(--thumb-size) / 2);
background: #895428;
border-radius: 50%;
box-shadow:
-2px 0px 1px 0px #00000040 inset,
2px 0px 1px 0px #00000040 inset,
0px 2px 1px 0px #be7f4a inset,
0px -2px 1px 0px #3a1c09 inset,
0px 1px 0px 0px #00000033;
transition: transform 0.1s ease;
}
&:active::-moz-range-thumb {
transform: scale(1.1);
}
}
.dragging::-webkit-slider-thumb {
transform: scale(1.1);
}
.dragging::-moz-range-thumb {
transform: scale(1.1);
}
}

View File

@@ -0,0 +1,26 @@
import { motion, type HTMLMotionProps } from "motion/react";
import { useState } from "react";
import clsx, { type ClassValue } from "clsx";
import classes from "./RangeInput.module.css";
type Props = Omit<HTMLMotionProps<"input">, "className"> & {
className?: ClassValue;
};
export default function RangeInput({ className, ...props }: Props) {
const [isDragging, setIsDragging] = useState(false);
return (
<motion.input
{...props}
type="range"
className={clsx(classes.rangeInput, isDragging && classes.dragging, className)}
onMouseDown={() => setIsDragging(true)}
onMouseUp={() => setIsDragging(false)}
onMouseLeave={() => setIsDragging(false)}
onTouchStart={() => setIsDragging(true)}
onTouchEnd={() => setIsDragging(false)}
/>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./RangeInput";

View File

@@ -0,0 +1,51 @@
@layer base {
.container {
padding: 2px;
display: flex;
border-radius: 9999px;
width: 80px;
height: 30px;
}
.optionsContainer {
position: relative;
display: flex;
overflow: hidden;
border-radius: 9999px;
}
.options {
display: flex;
width: 100%;
flex: 1;
gap: 0;
z-index: 1;
}
.option {
flex: 1;
background: transparent;
border: none;
cursor: pointer;
padding: 6px 4px;
color: #fbe6be;
font-size: 18px;
line-height: 18px;
font-weight: 700;
position: relative;
z-index: 1;
transition: color 0.2s ease;
}
.selected {
color: #4b2c13;
}
.thumb {
position: absolute;
top: 0;
height: 100%;
border-radius: 9999px;
z-index: 0;
}
}

View File

@@ -0,0 +1,57 @@
import clsx, { type ClassValue } from "clsx";
import ContentSurface from "@components/surface/ContentSurface";
import LightSurface from "@components/surface/LightSurface";
import { motion, type HTMLMotionProps } from "motion/react";
import classes from "./SwitchInput.module.css";
type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
value?: boolean | null;
onChange?: (value: boolean) => void;
className?: ClassValue;
};
export default function SwitchInput({ value, onChange, className, ...props }: Props) {
const selectedIndex = value != null ? (value ? 0 : 1) : -1;
return (
<ContentSurface
{...props}
className={clsx(classes.container, className)}
whileTap={{ scale: 1.1 }}
>
<div className={classes.optionsContainer}>
<div className={classes.options}>
<motion.button
type="button"
whileTap={{ scale: 0.95 }}
onClick={() => onChange?.(true)}
className={clsx(classes.option, value === true && classes.selected)}
>
on
</motion.button>
<motion.button
type="button"
whileTap={{ scale: 0.95 }}
onClick={() => onChange?.(false)}
className={clsx(classes.option, value === false && classes.selected)}
>
off
</motion.button>
</div>
{selectedIndex >= 0 && (
<LightSurface
className={classes.thumb}
initial={{ scale: 0.5 }}
animate={{
left: `${selectedIndex * 50}%`,
width: "50%",
scale: 1,
}}
transition={{ type: "spring", stiffness: 500, damping: 35 }}
/>
)}
</div>
</ContentSurface>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./SwitchInput";

View File

@@ -0,0 +1,30 @@
@layer base {
.input {
width: 100%;
min-height: 54px;
height: 54px;
background: #0000004d;
border-top: 1px solid #472715;
box-shadow:
0px 4px 2px 0px #00000040 inset,
0px -1.5px 0px 0px #8a582d inset;
color: #fbe6be;
font-size: 18px;
line-height: 18px;
font-weight: 700;
border-radius: 14px;
border: none;
outline: none;
padding: 12px 16px;
resize: none;
box-sizing: border-box;
&::placeholder {
color: #fbe6be66;
}
}
.error {
border: 2px solid #af4242;
}
}

View File

@@ -0,0 +1,18 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import classes from "./TextAreaInput.module.css";
type Props = Omit<HTMLMotionProps<"textarea">, "className"> & {
className?: ClassValue;
error?: boolean;
};
export default function TextAreaInput({ className, error, ...props }: Props) {
return (
<motion.textarea
{...props}
className={clsx(classes.input, error && classes.error, className)}
/>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./TextAreaInput";

View File

@@ -0,0 +1,29 @@
@layer base {
.input {
width: 100%;
height: 32px;
background: #0000004d;
border-top: 1px solid #472715;
box-shadow:
0px 4px 2px 0px #00000040 inset,
0px -1.5px 0px 0px #8a582d inset;
color: #fbe6be;
font-size: 18px;
line-height: 18px;
font-weight: 700;
border-radius: 14px;
border: none;
outline: none;
padding: 7px 16px;
text-align: center;
box-sizing: border-box;
&::placeholder {
color: #fbe6be66;
}
}
.error {
border: 2px solid #af4242;
}
}

View File

@@ -0,0 +1,19 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import classes from "./TextInput.module.css";
type Props = Omit<HTMLMotionProps<"input">, "className"> & {
className?: ClassValue;
error?: boolean;
};
export default function TextInput({ className, error, ...props }: Props) {
return (
<motion.input
{...props}
type="text"
className={clsx(classes.input, error && classes.error, className)}
/>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./TextInput";

View File

@@ -0,0 +1,52 @@
import { motion, type SVGMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
type Props = Omit<SVGMotionProps<SVGElement>, "className"> & {
className?: ClassValue;
};
export default function KeyboardIcon(props: Props) {
return (
<motion.svg
viewBox="0 0 33 33"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
className={clsx(props.className)}
>
<mask
id="mask0_24_12426"
style={{ maskType: "luminance" }}
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="33"
height="33"
>
<path
d="M16.4167 31.8333C24.9313 31.8333 31.8333 24.9313 31.8333 16.4167C31.8333 7.90204 24.9313 1 16.4167 1C7.90204 1 1 7.90204 1 16.4167C1 24.9313 7.90204 31.8333 16.4167 31.8333Z"
fill="white"
stroke="white"
strokeWidth="2"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.47786 12.5625C9.98896 12.5625 10.4791 12.3594 10.8405 11.998C11.2019 11.6367 11.4049 11.1465 11.4049 10.6354C11.4049 10.1243 11.2019 9.63414 10.8405 9.27274C10.4791 8.91134 9.98896 8.70831 9.47786 8.70831C8.96677 8.70831 8.47661 8.91134 8.11521 9.27274C7.75381 9.63414 7.55078 10.1243 7.55078 10.6354C7.55078 11.1465 7.75381 11.6367 8.11521 11.998C8.47661 12.3594 8.96677 12.5625 9.47786 12.5625ZM9.47786 18.7291C9.98896 18.7291 10.4791 18.5261 10.8405 18.1647C11.2019 17.8033 11.4049 17.3132 11.4049 16.8021C11.4049 16.291 11.2019 15.8008 10.8405 15.4394C10.4791 15.078 9.98896 14.875 9.47786 14.875C8.96677 14.875 8.47661 15.078 8.11521 15.4394C7.75381 15.8008 7.55078 16.291 7.55078 16.8021C7.55078 17.3132 7.75381 17.8033 8.11521 18.1647C8.47661 18.5261 8.96677 18.7291 9.47786 18.7291ZM16.4154 12.5625C16.9265 12.5625 17.4166 12.3594 17.778 11.998C18.1394 11.6367 18.3424 11.1465 18.3424 10.6354C18.3424 10.1243 18.1394 9.63414 17.778 9.27274C17.4166 8.91134 16.9265 8.70831 16.4154 8.70831C15.9043 8.70831 15.4141 8.91134 15.0527 9.27274C14.6913 9.63414 14.4883 10.1243 14.4883 10.6354C14.4883 11.1465 14.6913 11.6367 15.0527 11.998C15.4141 12.3594 15.9043 12.5625 16.4154 12.5625ZM16.4154 18.7291C16.9265 18.7291 17.4166 18.5261 17.778 18.1647C18.1394 17.8033 18.3424 17.3132 18.3424 16.8021C18.3424 16.291 18.1394 15.8008 17.778 15.4394C17.4166 15.078 16.9265 14.875 16.4154 14.875C15.9043 14.875 15.4141 15.078 15.0527 15.4394C14.6913 15.8008 14.4883 16.291 14.4883 16.8021C14.4883 17.3132 14.6913 17.8033 15.0527 18.1647C15.4141 18.5261 15.9043 18.7291 16.4154 18.7291ZM23.3529 12.5625C23.864 12.5625 24.3541 12.3594 24.7155 11.998C25.0769 11.6367 25.2799 11.1465 25.2799 10.6354C25.2799 10.1243 25.0769 9.63414 24.7155 9.27274C24.3541 8.91134 23.864 8.70831 23.3529 8.70831C22.8418 8.70831 22.3516 8.91134 21.9902 9.27274C21.6288 9.63414 21.4258 10.1243 21.4258 10.6354C21.4258 11.1465 21.6288 11.6367 21.9902 11.998C22.3516 12.3594 22.8418 12.5625 23.3529 12.5625ZM23.3529 18.7291C23.864 18.7291 24.3541 18.5261 24.7155 18.1647C25.0769 17.8033 25.2799 17.3132 25.2799 16.8021C25.2799 16.291 25.0769 15.8008 24.7155 15.4394C24.3541 15.078 23.864 14.875 23.3529 14.875C22.8418 14.875 22.3516 15.078 21.9902 15.4394C21.6288 15.8008 21.4258 16.291 21.4258 16.8021C21.4258 17.3132 21.6288 17.8033 21.9902 18.1647C22.3516 18.5261 22.8418 18.7291 23.3529 18.7291Z"
fill="black"
/>
<path
d="M11.0195 23.3542H21.8112"
stroke="black"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</mask>
<g mask="url(#mask0_24_12426)">
<path d="M-2.08203 -2.08331H34.918V34.9167H-2.08203V-2.08331Z" fill="currentColor" />
</g>
</motion.svg>
);
}

View File

@@ -0,0 +1,38 @@
@layer base {
.overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
max-width: 320px;
padding: 13px;
.content {
border-radius: 22px;
.description {
padding: 12px;
border-radius: 18px;
text-align: center;
}
}
}
.buttons {
display: flex;
gap: 6px;
}
.button {
flex: 1;
}
}

View File

@@ -0,0 +1,62 @@
import { AnimatePresence, motion } from "motion/react";
import { useTranslation } from "react-i18next";
import SectionSurface from "../../surface/SectionSurface/SectionSurface";
import ContentSurface from "../../surface/ContentSurface/ContentSurface";
import LightSurface from "../../surface/LightSurface/LightSurface";
import Button from "../../atoms/Button/Button";
import classes from "./ActionModal.module.css";
type WithConfirm = {
onConfirm: () => void;
confirmText: string;
};
type WithoutConfirm = {
onConfirm?: never;
confirmText?: never;
};
type Props = (WithConfirm | WithoutConfirm) & {
open: boolean;
description: string;
onClose: () => void;
};
export default function ActionModal({ open, description, onClose, onConfirm, confirmText }: Props) {
const { t } = useTranslation();
return (
<AnimatePresence>
{open && (
<motion.div
className={classes.overlay}
initial={{ backdropFilter: "blur(0px)" }}
animate={{ backdropFilter: "blur(8px)" }}
exit={{ backdropFilter: "blur(0px)" }}
transition={{ duration: 0.2 }}
>
<SectionSurface
className={classes.modal}
exit={{ scale: 0 }}
transition={{ duration: 0.2, type: "spring" }}
>
<ContentSurface className={classes.content}>
<LightSurface className={classes.description}>{description}</LightSurface>
</ContentSurface>
<div className={classes.buttons}>
<Button variant="blue" onClick={onClose} className={classes.button}>
{t("actionModal.close")}
</Button>
{onConfirm != null && (
<Button variant="green" onClick={onConfirm} className={classes.button}>
{confirmText}
</Button>
)}
</div>
</SectionSurface>
</motion.div>
)}
</AnimatePresence>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./ActionModal";

View File

@@ -0,0 +1,37 @@
@reference "@/index.css";
@layer base {
.blueSectionSurface {
background: linear-gradient(180deg, #278789 0%, #206f66 100%);
border: 1px solid #123f3f;
box-shadow:
-3px 0px 1px 0px #00000059 inset,
3px 0px 1px 0px #00000059 inset,
0px 4px 1px 0px #ffffff26 inset,
0px -3px 1px 0px #00000059 inset;
padding: 16px;
border-radius: 40px;
}
.blueSurface {
background: #0c2836;
box-shadow:
0px -1.5px 0px 0px #32a39b inset,
1.5px 0px 0px 0px #32a39b inset,
-1.5px 0px 0px 0px #32a39b inset,
0px 1.5px 0px 0px #114647 inset;
color: #befbe8;
padding: 8px;
border-radius: 23px;
}
.blueSurfaceContent {
background: #0000004d;
border-top: 1px solid #0a2929;
box-shadow:
0px 4px 2px 0px #00000040 inset,
0px -1.5px 0px 0px #207475 inset;
padding: 4px;
border-radius: 9999px;
}
}

View File

@@ -0,0 +1,28 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import classes from "./BlueSectionSurface.module.css";
type Props = Omit<HTMLMotionProps<"div">, "className"> & {
className?: ClassValue;
};
export default function BlueSectionSurface(props: Props) {
return (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.2, type: "spring" }}
{...props}
className={clsx(classes.blueSectionSurface, props.className)}
/>
);
}
export function BlueSurface(props: Props) {
return <motion.div {...props} className={clsx(classes.blueSurface, props.className)} />;
}
export function BlueSurfaceContent(props: Props) {
return <motion.div {...props} className={clsx(classes.blueSurfaceContent, props.className)} />;
}

View File

@@ -0,0 +1 @@
export { default, BlueSurface, BlueSurfaceContent } from "./BlueSectionSurface";

View File

@@ -0,0 +1,14 @@
@layer base {
.contentSurface {
background: #432710;
box-shadow:
0px -1.5px 0px 0px #a17346 inset,
1.5px 0px 0px 0px #a17346 inset,
-1.5px 0px 0px 0px #a17346 inset,
0px 1.5px 0px 0px #6b4521 inset;
padding: 3px;
display: flex;
flex-direction: column;
gap: 1px;
}
}

View File

@@ -0,0 +1,11 @@
import classes from "./ContentSurface.module.css";
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
type Props = Omit<HTMLMotionProps<"div">, "className"> & {
className?: ClassValue;
};
export default function ContentSurface(props: Props) {
return <motion.div {...props} className={clsx(classes.contentSurface, props.className)} />;
}

View File

@@ -0,0 +1 @@
export { default } from "./ContentSurface";

View File

@@ -0,0 +1,11 @@
@layer base {
.darkSurface {
background: #5a391c;
box-shadow:
0px 2px 0px 0px #8a582d inset,
-1px 0px 0px 0px #00000059 inset,
1px 0px 0px 0px #00000059 inset,
0px -1px 0px 0px #00000059 inset;
color: #fbe6be;
}
}

View File

@@ -0,0 +1,11 @@
import classes from "./DarkSurface.module.css";
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
type Props = Omit<HTMLMotionProps<"div">, "className"> & {
className?: ClassValue;
};
export default function DarkSurface(props: Props) {
return <motion.div {...props} className={clsx(classes.darkSurface, props.className)} />;
}

View File

@@ -0,0 +1 @@
export { default } from "./DarkSurface";

View File

@@ -0,0 +1,9 @@
@layer base {
.dataSurface {
background: #3f2814;
box-shadow: 0px 4px 2px 0px #00000040 inset;
padding: 6px 10px;
border-radius: 99999px;
color: #fbe6be;
}
}

View File

@@ -0,0 +1,11 @@
import classes from "./DataSurface.module.css";
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
type Props = Omit<HTMLMotionProps<"div">, "className"> & {
className?: ClassValue;
};
export default function DataSurface(props: Props) {
return <motion.div {...props} className={clsx(classes.dataSurface, props.className)} />;
}

View File

@@ -0,0 +1 @@
export { default } from "./DataSurface";

View File

@@ -0,0 +1,14 @@
@layer base {
.glassSurface {
background: #ffffff1a;
backdrop-filter: blur(4px);
box-shadow:
0px 2px 1px 0px #ffffff40 inset,
0px 4px 4px 0px #00000073;
}
.glassSurfaceContent {
background: #06060666;
box-shadow: 0px 1px 4px 1px #00000040 inset;
}
}

View File

@@ -0,0 +1,16 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import classes from "./GlassSurface.module.css";
type Props = Omit<HTMLMotionProps<"div">, "className"> & {
className?: ClassValue;
};
export default function GlassSurface(props: Props) {
return <motion.div {...props} className={clsx(classes.glassSurface, props.className)} />;
}
export function GlassSurfaceContent(props: Props) {
return <motion.div {...props} className={clsx(classes.glassSurfaceContent, props.className)} />;
}

View File

@@ -0,0 +1 @@
export { default, GlassSurfaceContent } from "./GlassSurface";

View File

@@ -0,0 +1,24 @@
@reference "@/index.css";
@layer base {
.greenSurface {
background: #295a1c;
box-shadow:
0px 2px 0px 0px #318a2d inset,
-1px 0px 0px 0px #00000059 inset,
1px 0px 0px 0px #00000059 inset,
0px -1px 0px 0px #00000059 inset;
border-radius: 14px;
padding: 4px 8px;
color: #befbbe;
}
.greenSurfaceContent {
background: #2f1e0099;
box-shadow:
0px 2px 3px 1px #00000040 inset,
0px -1px 0px 0px #4fa146 inset;
padding: 4px;
border-radius: 9999px;
}
}

View File

@@ -0,0 +1,16 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import classes from "./GreenSurface.module.css";
type Props = Omit<HTMLMotionProps<"div">, "className"> & {
className?: ClassValue;
};
export default function GreenSurface(props: Props) {
return <motion.div {...props} className={clsx(classes.greenSurface, props.className)} />;
}
export function GreenSurfaceContent(props: Props) {
return <motion.div {...props} className={clsx(classes.greenSurfaceContent, props.className)} />;
}

View File

@@ -0,0 +1 @@
export { default, GreenSurfaceContent } from "./GreenSurface";

View File

@@ -0,0 +1,11 @@
@layer base {
.lightSurface {
background: #fbe6be;
box-shadow:
0px 2px 0px 0px #ffffffbf inset,
-1px 0px 0px 0px #00000059 inset,
1px 0px 0px 0px #00000059 inset,
0px -1px 0px 0px #00000059 inset;
color: #4b2c13;
}
}

View File

@@ -0,0 +1,11 @@
import classes from "./LightSurface.module.css";
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
type Props = Omit<HTMLMotionProps<"div">, "className"> & {
className?: ClassValue;
};
export default function LightSurface(props: Props) {
return <motion.div {...props} className={clsx(classes.lightSurface, props.className)} />;
}

View File

@@ -0,0 +1 @@
export { default } from "./LightSurface";

View File

@@ -0,0 +1,22 @@
@layer base {
.redSurface {
background: #5a2b1c;
box-shadow:
0px 2px 0px 0px #8a392d inset,
-1px 0px 0px 0px #00000059 inset,
1px 0px 0px 0px #00000059 inset,
0px -1px 0px 0px #00000059 inset;
border-radius: 14px;
padding: 4px 8px;
color: #fbccbe;
}
.redSurfaceContent {
background: #2f1e0099;
box-shadow:
0px 2px 3px 1px #00000040 inset,
0px -1px 0px 0px #a15746 inset;
padding: 4px;
border-radius: 9999px;
}
}

View File

@@ -0,0 +1,16 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import classes from "./RedSurface.module.css";
type Props = Omit<HTMLMotionProps<"div">, "className"> & {
className?: ClassValue;
};
export default function RedSurface(props: Props) {
return <motion.div {...props} className={clsx(classes.redSurface, props.className)} />;
}
export function RedSurfaceContent(props: Props) {
return <motion.div {...props} className={clsx(classes.redSurfaceContent, props.className)} />;
}

View File

@@ -0,0 +1 @@
export { default, RedSurfaceContent } from "./RedSurface";

View File

@@ -0,0 +1,13 @@
@layer base {
.sectionSurface {
background: linear-gradient(180deg, #935a2b 0%, #6f4420 100%);
box-shadow:
-3px 0px 1px 0px #00000059 inset,
3px 0px 1px 0px #00000059 inset,
0px 4px 1px 0px #ffffff26 inset,
0px -3px 1px 0px #00000059 inset;
border: 1px solid #511e00;
border-radius: 40px;
padding: 10px;
}
}

View File

@@ -0,0 +1,19 @@
import classes from "./SectionSurface.module.css";
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
type Props = Omit<HTMLMotionProps<"div">, "className"> & {
className?: ClassValue;
};
export default function SectionSurface(props: Props) {
return (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.2, type: "spring" }}
{...props}
className={clsx(classes.sectionSurface, props.className)}
/>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./SectionSurface";

View File

@@ -1,4 +0,0 @@
@import "tailwindcss";
@theme {
}

View File

@@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./index.css"; import "./styles/index.css";
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
import "./i18next"; import "./i18next";

View File

@@ -9,68 +9,180 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as AboutRouteImport } from './routes/about' import { Route as TasksIndexRouteImport } from './routes/tasks/index'
import { Route as IndexRouteImport } from './routes/index' import { Route as ShopIndexRouteImport } from './routes/shop/index'
import { Route as RouletteIndexRouteImport } from './routes/roulette/index'
import { Route as GameIndexRouteImport } from './routes/game/index'
import { Route as EarningsIndexRouteImport } from './routes/earnings/index'
import { Route as CashdeskIndexRouteImport } from './routes/cashdesk/index'
import { Route as ApiaryIndexRouteImport } from './routes/apiary/index'
const AboutRoute = AboutRouteImport.update({ const TasksIndexRoute = TasksIndexRouteImport.update({
id: '/about', id: '/tasks/',
path: '/about', path: '/tasks/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const IndexRoute = IndexRouteImport.update({ const ShopIndexRoute = ShopIndexRouteImport.update({
id: '/', id: '/shop/',
path: '/', path: '/shop/',
getParentRoute: () => rootRouteImport,
} as any)
const RouletteIndexRoute = RouletteIndexRouteImport.update({
id: '/roulette/',
path: '/roulette/',
getParentRoute: () => rootRouteImport,
} as any)
const GameIndexRoute = GameIndexRouteImport.update({
id: '/game/',
path: '/game/',
getParentRoute: () => rootRouteImport,
} as any)
const EarningsIndexRoute = EarningsIndexRouteImport.update({
id: '/earnings/',
path: '/earnings/',
getParentRoute: () => rootRouteImport,
} as any)
const CashdeskIndexRoute = CashdeskIndexRouteImport.update({
id: '/cashdesk/',
path: '/cashdesk/',
getParentRoute: () => rootRouteImport,
} as any)
const ApiaryIndexRoute = ApiaryIndexRouteImport.update({
id: '/apiary/',
path: '/apiary/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/apiary/': typeof ApiaryIndexRoute
'/about': typeof AboutRoute '/cashdesk/': typeof CashdeskIndexRoute
'/earnings/': typeof EarningsIndexRoute
'/game/': typeof GameIndexRoute
'/roulette/': typeof RouletteIndexRoute
'/shop/': typeof ShopIndexRoute
'/tasks/': typeof TasksIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/apiary': typeof ApiaryIndexRoute
'/about': typeof AboutRoute '/cashdesk': typeof CashdeskIndexRoute
'/earnings': typeof EarningsIndexRoute
'/game': typeof GameIndexRoute
'/roulette': typeof RouletteIndexRoute
'/shop': typeof ShopIndexRoute
'/tasks': typeof TasksIndexRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/apiary/': typeof ApiaryIndexRoute
'/about': typeof AboutRoute '/cashdesk/': typeof CashdeskIndexRoute
'/earnings/': typeof EarningsIndexRoute
'/game/': typeof GameIndexRoute
'/roulette/': typeof RouletteIndexRoute
'/shop/': typeof ShopIndexRoute
'/tasks/': typeof TasksIndexRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/about' fullPaths:
| '/apiary/'
| '/cashdesk/'
| '/earnings/'
| '/game/'
| '/roulette/'
| '/shop/'
| '/tasks/'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/about' to:
id: '__root__' | '/' | '/about' | '/apiary'
| '/cashdesk'
| '/earnings'
| '/game'
| '/roulette'
| '/shop'
| '/tasks'
id:
| '__root__'
| '/apiary/'
| '/cashdesk/'
| '/earnings/'
| '/game/'
| '/roulette/'
| '/shop/'
| '/tasks/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute ApiaryIndexRoute: typeof ApiaryIndexRoute
AboutRoute: typeof AboutRoute CashdeskIndexRoute: typeof CashdeskIndexRoute
EarningsIndexRoute: typeof EarningsIndexRoute
GameIndexRoute: typeof GameIndexRoute
RouletteIndexRoute: typeof RouletteIndexRoute
ShopIndexRoute: typeof ShopIndexRoute
TasksIndexRoute: typeof TasksIndexRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/about': { '/tasks/': {
id: '/about' id: '/tasks/'
path: '/about' path: '/tasks'
fullPath: '/about' fullPath: '/tasks/'
preLoaderRoute: typeof AboutRouteImport preLoaderRoute: typeof TasksIndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/': { '/shop/': {
id: '/' id: '/shop/'
path: '/' path: '/shop'
fullPath: '/' fullPath: '/shop/'
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof ShopIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/roulette/': {
id: '/roulette/'
path: '/roulette'
fullPath: '/roulette/'
preLoaderRoute: typeof RouletteIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/game/': {
id: '/game/'
path: '/game'
fullPath: '/game/'
preLoaderRoute: typeof GameIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/earnings/': {
id: '/earnings/'
path: '/earnings'
fullPath: '/earnings/'
preLoaderRoute: typeof EarningsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/cashdesk/': {
id: '/cashdesk/'
path: '/cashdesk'
fullPath: '/cashdesk/'
preLoaderRoute: typeof CashdeskIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/apiary/': {
id: '/apiary/'
path: '/apiary'
fullPath: '/apiary/'
preLoaderRoute: typeof ApiaryIndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
} }
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, ApiaryIndexRoute: ApiaryIndexRoute,
AboutRoute: AboutRoute, CashdeskIndexRoute: CashdeskIndexRoute,
EarningsIndexRoute: EarningsIndexRoute,
GameIndexRoute: GameIndexRoute,
RouletteIndexRoute: RouletteIndexRoute,
ShopIndexRoute: ShopIndexRoute,
TasksIndexRoute: TasksIndexRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)

View File

@@ -1,20 +1,18 @@
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; import { createRootRoute, Outlet, useNavigate } from "@tanstack/react-router";
import { TanStackDevtools } from "@tanstack/react-devtools"; import { TanStackDevtools } from "@tanstack/react-devtools";
import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { useEffect } from "react";
import { Route as GameRoute } from "./game";
export const Route = createRootRoute({ export const Route = createRootRoute({
notFoundComponent: () => {
const navigate = useNavigate();
useEffect(() => void navigate({ to: GameRoute.to }), [navigate]);
},
component: () => ( component: () => (
<> <>
<div className="p-2 flex gap-2">
<Link to="/" className="[&.active]:font-bold">
Home
</Link>{" "}
<Link to="/about" className="[&.active]:font-bold">
About
</Link>
</div>
<hr />
<Outlet /> <Outlet />
<TanStackDevtools <TanStackDevtools
config={{ config={{

View File

@@ -1,5 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/about")({
component: () => <div className="p-2">Hello from About!</div>,
});

View File

@@ -0,0 +1,5 @@
import SectionSurface from "@components/surface/SectionSurface";
export default function ApiaryRoute() {
return <SectionSurface>Hello "/apiary"!</SectionSurface>;
}

View File

@@ -0,0 +1,3 @@
import { createFileRoute } from "@tanstack/react-router";
import component from "./-/ApiaryRoute";
export const Route = createFileRoute("/apiary/")({ component });

View File

@@ -0,0 +1,5 @@
import SectionSurface from "@components/surface/SectionSurface";
export default function CashdeskRoute() {
return <SectionSurface>Hello "/cashdesk"!</SectionSurface>;
}

View File

@@ -0,0 +1,3 @@
import { createFileRoute } from "@tanstack/react-router";
import component from "./-/CashdeskRoute";
export const Route = createFileRoute("/cashdesk/")({ component });

View File

@@ -0,0 +1,5 @@
import SectionSurface from "@components/surface/SectionSurface";
export default function EarningsRoute() {
return <SectionSurface>Hello "/earnings"!</SectionSurface>;
}

View File

@@ -0,0 +1,3 @@
import { createFileRoute } from "@tanstack/react-router";
import component from "./-/EarningsRoute";
export const Route = createFileRoute("/earnings/")({ component });

View File

@@ -0,0 +1,94 @@
import { useState } from "react";
import SectionSurface from "@components/surface/SectionSurface";
import { Link } from "@tanstack/react-router";
import DarkSurface from "@components/surface/DarkSurface";
import ContentSurface from "@components/surface/ContentSurface";
import LightSurface from "@components/surface/LightSurface";
import DataSurface from "@components/surface/DataSurface";
import GlassSurface, { GlassSurfaceContent } from "@components/surface/GlassSurface";
import Button from "@components/atoms/Button";
import TabSelector from "@components/atoms/TabSelector";
import Progress from "@components/atoms/Progress";
import RangeInput from "@components/form/RangeInput";
import SwitchInput from "@components/form/SwitchInput";
import TextInput from "@components/form/TextInput";
import NumberInput from "@components/form/NumberInput";
import TextAreaInput from "@components/form/TextAreaInput";
import ActionModal from "@components/modal/ActionModal";
const TABS = [
{ key: "tab1", title: "Tab 1" },
{ key: "tab2", title: "Tab 2" },
{ key: "tab3", title: "Tab 3" },
];
export default function GameRoute() {
const [activeTab, setActiveTab] = useState<string | null>(TABS[0].key);
const [progressValue, setProgressValue] = useState(0);
const [switchValue, setSwitchValue] = useState<boolean | null>(false);
const [modalOpen, setModalOpen] = useState(false);
return (
<SectionSurface className="relative flex flex-col gap-4 w-full overflow-auto max-h-dvh">
<TabSelector tabs={TABS} value={activeTab} onChange={setActiveTab} />
<SwitchInput value={switchValue} onChange={setSwitchValue} />
<ContentSurface className="rounded-2xl">
<LightSurface className="rounded-2xl rounded-b-sm p-2">
Hello "/game"!
<DataSurface>100$</DataSurface>
</LightSurface>
<DarkSurface className="rounded-2xl rounded-t-sm p-2 flex flex-col gap-2">
<Link to="/roulette">Roulette</Link>
<DataSurface>100$</DataSurface>
</DarkSurface>
</ContentSurface>
<Button onClick={() => setModalOpen(true)}>Open modal</Button>
<Button>Click me</Button>
<Button variant="green">Click me</Button>
<Button variant="red">Click me</Button>
<Button variant="yellow">Click me</Button>
<Button variant="blue">Click me</Button>
<ContentSurface className="rounded-full">
<Progress value={progressValue} max={10000} variant="green" />
</ContentSurface>
<ContentSurface className="rounded-full">
<Progress value={progressValue} max={10000} variant="yellow" />
</ContentSurface>
<RangeInput
value={progressValue}
onChange={(e) => setProgressValue(Number(e.target.value))}
min={0}
max={10000}
/>
<TextInput placeholder="Text Input" />
<TextInput placeholder="Text Input Error" error />
<NumberInput placeholder="Number Input" />
<NumberInput error prefix="$" value={progressValue} onChange={setProgressValue} />
<TextAreaInput placeholder="Text Area Input" rows={3} />
<TextAreaInput placeholder="Text Area Error" error rows={3} />
<div className="flex gap-2">
{[1, 2, 3, 4, 5].map((i) => (
<GlassSurface
key={i}
className="rounded-2xl w-12.5 h-12.5 p-3"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring" }}
>
<GlassSurfaceContent className="rounded-2xl flex items-center justify-center">
10
</GlassSurfaceContent>
</GlassSurface>
))}
</div>
<ActionModal
open={modalOpen}
description="Are you sure you want to proceed with this action?"
onClose={() => setModalOpen(false)}
onConfirm={() => setModalOpen(false)}
confirmText="Confirm"
/>
</SectionSurface>
);
}

View File

@@ -0,0 +1,3 @@
import { createFileRoute } from "@tanstack/react-router";
import component from "./-/GameRoute";
export const Route = createFileRoute("/game/")({ component });

View File

@@ -1,26 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import { languages } from "../i18next";
export const Route = createFileRoute("/")({
component: () => {
const { t, i18n } = useTranslation();
const [langIdx, setLangIdx] = useState(0);
useEffect(() => {
i18n.changeLanguage(languages[langIdx].key);
}, [langIdx, i18n]);
return (
<div className="p-2">
<h3>
{t("hello")}
<button onClick={() => setLangIdx((langIdx + 1) % languages.length)}>
{i18n.language}
</button>
</h3>
</div>
);
},
});

View File

@@ -0,0 +1,11 @@
import SectionSurface from "@components/surface/SectionSurface";
import { Link } from "@tanstack/react-router";
export default function RouletteRoute() {
return (
<SectionSurface>
Hello "/roulette"!
<Link to="/game">Game</Link>
</SectionSurface>
);
}

View File

@@ -0,0 +1,3 @@
import { createFileRoute } from "@tanstack/react-router";
import component from "./-/RouletteRoute";
export const Route = createFileRoute("/roulette/")({ component });

View File

@@ -0,0 +1,5 @@
import SectionSurface from "@components/surface/SectionSurface";
export default function ShopRoute() {
return <SectionSurface>Shop</SectionSurface>;
}

View File

@@ -0,0 +1,3 @@
import { createFileRoute } from "@tanstack/react-router";
import component from "./-/ShopRoute";
export const Route = createFileRoute("/shop/")({ component });

View File

@@ -0,0 +1,5 @@
import SectionSurface from "@components/surface/SectionSurface";
export default function TasksRoute() {
return <SectionSurface>Hello "/tasks"!</SectionSurface>;
}

View File

@@ -0,0 +1,3 @@
import { createFileRoute } from "@tanstack/react-router";
import component from "./-/TasksRoute";
export const Route = createFileRoute("/tasks/")({ component });

View File

@@ -0,0 +1,31 @@
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-Italic.ttf") format("truetype");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-Bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "BalsamiqSans";
src: url("/fonts/BalsamicSans/BalsamiqSans-BoldItalic.ttf") format("truetype");
font-weight: 700;
font-style: italic;
font-display: swap;
}

22
src/styles/index.css Normal file
View File

@@ -0,0 +1,22 @@
@import "tailwindcss";
@import "./fonts/BalsamiqSans.css";
@theme {
}
html,
body {
@apply w-dvw h-dvh bg-top-left bg-green-300;
font-family: "BalsamiqSans", sans-serif;
}
#root {
@apply w-full h-full max-w-150 m-auto overflow-hidden relative;
}
button {
&:focus-visible {
outline: none;
}
}

View File

@@ -7,7 +7,6 @@
"module": "ESNext", "module": "ESNext",
"types": ["vite/client"], "types": ["vite/client"],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@@ -15,12 +14,15 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
"strict": true, "strict": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true,
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
}
}, },
"include": ["src"] "include": ["src"]
} }

View File

@@ -9,6 +9,7 @@ import i18nextConfig, {
LOCALES_PATH, LOCALES_PATH,
LOCAL_LOCALES_PATH, LOCAL_LOCALES_PATH,
} from "./i18next.config"; } from "./i18next.config";
import path from "node:path";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@@ -46,4 +47,10 @@ export default defineConfig({
__LOCALES_PATH__: JSON.stringify(LOCALES_PATH), __LOCALES_PATH__: JSON.stringify(LOCALES_PATH),
__DEFAULT_LANG__: JSON.stringify(DEFAULT_LANGUAGE), __DEFAULT_LANG__: JSON.stringify(DEFAULT_LANGUAGE),
}, },
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@components": path.resolve(__dirname, "./src/components"),
},
},
}); });