This commit is contained in:
85
src/components/atoms/Button/Button.module.css
Normal file
85
src/components/atoms/Button/Button.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
29
src/components/atoms/Button/Button.tsx
Normal file
29
src/components/atoms/Button/Button.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1
src/components/atoms/Button/index.ts
Normal file
1
src/components/atoms/Button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./Button";
|
||||
24
src/components/atoms/Counter/Counter.tsx
Normal file
24
src/components/atoms/Counter/Counter.tsx
Normal 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>;
|
||||
}
|
||||
1
src/components/atoms/Counter/index.ts
Normal file
1
src/components/atoms/Counter/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./Counter";
|
||||
56
src/components/atoms/Progress/Progress.module.css
Normal file
56
src/components/atoms/Progress/Progress.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
40
src/components/atoms/Progress/Progress.tsx
Normal file
40
src/components/atoms/Progress/Progress.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/components/atoms/Progress/index.ts
Normal file
1
src/components/atoms/Progress/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./Progress";
|
||||
45
src/components/atoms/TabSelector/TabSelector.module.css
Normal file
45
src/components/atoms/TabSelector/TabSelector.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
54
src/components/atoms/TabSelector/TabSelector.tsx
Normal file
54
src/components/atoms/TabSelector/TabSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/components/atoms/TabSelector/index.ts
Normal file
1
src/components/atoms/TabSelector/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./TabSelector";
|
||||
82
src/components/form/NumberInput/NumberInput.module.css
Normal file
82
src/components/form/NumberInput/NumberInput.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/components/form/NumberInput/NumberInput.tsx
Normal file
75
src/components/form/NumberInput/NumberInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/components/form/NumberInput/index.ts
Normal file
1
src/components/form/NumberInput/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./NumberInput";
|
||||
82
src/components/form/RangeInput/RangeInput.module.css
Normal file
82
src/components/form/RangeInput/RangeInput.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
26
src/components/form/RangeInput/RangeInput.tsx
Normal file
26
src/components/form/RangeInput/RangeInput.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1
src/components/form/RangeInput/index.ts
Normal file
1
src/components/form/RangeInput/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./RangeInput";
|
||||
51
src/components/form/SwitchInput/SwitchInput.module.css
Normal file
51
src/components/form/SwitchInput/SwitchInput.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
57
src/components/form/SwitchInput/SwitchInput.tsx
Normal file
57
src/components/form/SwitchInput/SwitchInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/components/form/SwitchInput/index.ts
Normal file
1
src/components/form/SwitchInput/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./SwitchInput";
|
||||
30
src/components/form/TextAreaInput/TextAreaInput.module.css
Normal file
30
src/components/form/TextAreaInput/TextAreaInput.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
18
src/components/form/TextAreaInput/TextAreaInput.tsx
Normal file
18
src/components/form/TextAreaInput/TextAreaInput.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1
src/components/form/TextAreaInput/index.ts
Normal file
1
src/components/form/TextAreaInput/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./TextAreaInput";
|
||||
29
src/components/form/TextInput/TextInput.module.css
Normal file
29
src/components/form/TextInput/TextInput.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
19
src/components/form/TextInput/TextInput.tsx
Normal file
19
src/components/form/TextInput/TextInput.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1
src/components/form/TextInput/index.ts
Normal file
1
src/components/form/TextInput/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./TextInput";
|
||||
52
src/components/icons/KeyboardIcon.tsx
Normal file
52
src/components/icons/KeyboardIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
src/components/modal/ActionModal/ActionModal.module.css
Normal file
38
src/components/modal/ActionModal/ActionModal.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
62
src/components/modal/ActionModal/ActionModal.tsx
Normal file
62
src/components/modal/ActionModal/ActionModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/components/modal/ActionModal/index.ts
Normal file
1
src/components/modal/ActionModal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./ActionModal";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)} />;
|
||||
}
|
||||
1
src/components/surface/BlueSectionSurface/index.ts
Normal file
1
src/components/surface/BlueSectionSurface/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default, BlueSurface, BlueSurfaceContent } from "./BlueSectionSurface";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
11
src/components/surface/ContentSurface/ContentSurface.tsx
Normal file
11
src/components/surface/ContentSurface/ContentSurface.tsx
Normal 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)} />;
|
||||
}
|
||||
1
src/components/surface/ContentSurface/index.ts
Normal file
1
src/components/surface/ContentSurface/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./ContentSurface";
|
||||
11
src/components/surface/DarkSurface/DarkSurface.module.css
Normal file
11
src/components/surface/DarkSurface/DarkSurface.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
11
src/components/surface/DarkSurface/DarkSurface.tsx
Normal file
11
src/components/surface/DarkSurface/DarkSurface.tsx
Normal 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)} />;
|
||||
}
|
||||
1
src/components/surface/DarkSurface/index.ts
Normal file
1
src/components/surface/DarkSurface/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./DarkSurface";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
11
src/components/surface/DataSurface/DataSurface.tsx
Normal file
11
src/components/surface/DataSurface/DataSurface.tsx
Normal 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)} />;
|
||||
}
|
||||
1
src/components/surface/DataSurface/index.ts
Normal file
1
src/components/surface/DataSurface/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./DataSurface";
|
||||
14
src/components/surface/GlassSurface/GlassSurface.module.css
Normal file
14
src/components/surface/GlassSurface/GlassSurface.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
16
src/components/surface/GlassSurface/GlassSurface.tsx
Normal file
16
src/components/surface/GlassSurface/GlassSurface.tsx
Normal 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)} />;
|
||||
}
|
||||
1
src/components/surface/GlassSurface/index.ts
Normal file
1
src/components/surface/GlassSurface/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default, GlassSurfaceContent } from "./GlassSurface";
|
||||
24
src/components/surface/GreenSurface/GreenSurface.module.css
Normal file
24
src/components/surface/GreenSurface/GreenSurface.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
16
src/components/surface/GreenSurface/GreenSurface.tsx
Normal file
16
src/components/surface/GreenSurface/GreenSurface.tsx
Normal 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)} />;
|
||||
}
|
||||
1
src/components/surface/GreenSurface/index.ts
Normal file
1
src/components/surface/GreenSurface/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default, GreenSurfaceContent } from "./GreenSurface";
|
||||
11
src/components/surface/LightSurface/LightSurface.module.css
Normal file
11
src/components/surface/LightSurface/LightSurface.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
11
src/components/surface/LightSurface/LightSurface.tsx
Normal file
11
src/components/surface/LightSurface/LightSurface.tsx
Normal 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)} />;
|
||||
}
|
||||
1
src/components/surface/LightSurface/index.ts
Normal file
1
src/components/surface/LightSurface/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./LightSurface";
|
||||
22
src/components/surface/RedSurface/RedSurface.module.css
Normal file
22
src/components/surface/RedSurface/RedSurface.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
16
src/components/surface/RedSurface/RedSurface.tsx
Normal file
16
src/components/surface/RedSurface/RedSurface.tsx
Normal 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)} />;
|
||||
}
|
||||
1
src/components/surface/RedSurface/index.ts
Normal file
1
src/components/surface/RedSurface/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default, RedSurfaceContent } from "./RedSurface";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
19
src/components/surface/SectionSurface/SectionSurface.tsx
Normal file
19
src/components/surface/SectionSurface/SectionSurface.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1
src/components/surface/SectionSurface/index.ts
Normal file
1
src/components/surface/SectionSurface/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./SectionSurface";
|
||||
@@ -1,4 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import "./index.css";
|
||||
import "./styles/index.css";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import "./i18next";
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as AboutRouteImport } from './routes/about'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as TasksIndexRouteImport } from './routes/tasks/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({
|
||||
id: '/about',
|
||||
path: '/about',
|
||||
const TasksIndexRoute = TasksIndexRouteImport.update({
|
||||
id: '/tasks/',
|
||||
path: '/tasks/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
const ShopIndexRoute = ShopIndexRouteImport.update({
|
||||
id: '/shop/',
|
||||
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,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/apiary/': typeof ApiaryIndexRoute
|
||||
'/cashdesk/': typeof CashdeskIndexRoute
|
||||
'/earnings/': typeof EarningsIndexRoute
|
||||
'/game/': typeof GameIndexRoute
|
||||
'/roulette/': typeof RouletteIndexRoute
|
||||
'/shop/': typeof ShopIndexRoute
|
||||
'/tasks/': typeof TasksIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/apiary': typeof ApiaryIndexRoute
|
||||
'/cashdesk': typeof CashdeskIndexRoute
|
||||
'/earnings': typeof EarningsIndexRoute
|
||||
'/game': typeof GameIndexRoute
|
||||
'/roulette': typeof RouletteIndexRoute
|
||||
'/shop': typeof ShopIndexRoute
|
||||
'/tasks': typeof TasksIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/about': typeof AboutRoute
|
||||
'/apiary/': typeof ApiaryIndexRoute
|
||||
'/cashdesk/': typeof CashdeskIndexRoute
|
||||
'/earnings/': typeof EarningsIndexRoute
|
||||
'/game/': typeof GameIndexRoute
|
||||
'/roulette/': typeof RouletteIndexRoute
|
||||
'/shop/': typeof ShopIndexRoute
|
||||
'/tasks/': typeof TasksIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/about'
|
||||
fullPaths:
|
||||
| '/apiary/'
|
||||
| '/cashdesk/'
|
||||
| '/earnings/'
|
||||
| '/game/'
|
||||
| '/roulette/'
|
||||
| '/shop/'
|
||||
| '/tasks/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/about'
|
||||
id: '__root__' | '/' | '/about'
|
||||
to:
|
||||
| '/apiary'
|
||||
| '/cashdesk'
|
||||
| '/earnings'
|
||||
| '/game'
|
||||
| '/roulette'
|
||||
| '/shop'
|
||||
| '/tasks'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/apiary/'
|
||||
| '/cashdesk/'
|
||||
| '/earnings/'
|
||||
| '/game/'
|
||||
| '/roulette/'
|
||||
| '/shop/'
|
||||
| '/tasks/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AboutRoute: typeof AboutRoute
|
||||
ApiaryIndexRoute: typeof ApiaryIndexRoute
|
||||
CashdeskIndexRoute: typeof CashdeskIndexRoute
|
||||
EarningsIndexRoute: typeof EarningsIndexRoute
|
||||
GameIndexRoute: typeof GameIndexRoute
|
||||
RouletteIndexRoute: typeof RouletteIndexRoute
|
||||
ShopIndexRoute: typeof ShopIndexRoute
|
||||
TasksIndexRoute: typeof TasksIndexRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/about': {
|
||||
id: '/about'
|
||||
path: '/about'
|
||||
fullPath: '/about'
|
||||
preLoaderRoute: typeof AboutRouteImport
|
||||
'/tasks/': {
|
||||
id: '/tasks/'
|
||||
path: '/tasks'
|
||||
fullPath: '/tasks/'
|
||||
preLoaderRoute: typeof TasksIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
'/shop/': {
|
||||
id: '/shop/'
|
||||
path: '/shop'
|
||||
fullPath: '/shop/'
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AboutRoute: AboutRoute,
|
||||
ApiaryIndexRoute: ApiaryIndexRoute,
|
||||
CashdeskIndexRoute: CashdeskIndexRoute,
|
||||
EarningsIndexRoute: EarningsIndexRoute,
|
||||
GameIndexRoute: GameIndexRoute,
|
||||
RouletteIndexRoute: RouletteIndexRoute,
|
||||
ShopIndexRoute: ShopIndexRoute,
|
||||
TasksIndexRoute: TasksIndexRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -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 { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools";
|
||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { Route as GameRoute } from "./game";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
notFoundComponent: () => {
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => void navigate({ to: GameRoute.to }), [navigate]);
|
||||
},
|
||||
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 />
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/about")({
|
||||
component: () => <div className="p-2">Hello from About!</div>,
|
||||
});
|
||||
5
src/routes/apiary/-/ApiaryRoute.tsx
Normal file
5
src/routes/apiary/-/ApiaryRoute.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import SectionSurface from "@components/surface/SectionSurface";
|
||||
|
||||
export default function ApiaryRoute() {
|
||||
return <SectionSurface>Hello "/apiary"!</SectionSurface>;
|
||||
}
|
||||
3
src/routes/apiary/index.tsx
Normal file
3
src/routes/apiary/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import component from "./-/ApiaryRoute";
|
||||
export const Route = createFileRoute("/apiary/")({ component });
|
||||
5
src/routes/cashdesk/-/CashdeskRoute.tsx
Normal file
5
src/routes/cashdesk/-/CashdeskRoute.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import SectionSurface from "@components/surface/SectionSurface";
|
||||
|
||||
export default function CashdeskRoute() {
|
||||
return <SectionSurface>Hello "/cashdesk"!</SectionSurface>;
|
||||
}
|
||||
3
src/routes/cashdesk/index.tsx
Normal file
3
src/routes/cashdesk/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import component from "./-/CashdeskRoute";
|
||||
export const Route = createFileRoute("/cashdesk/")({ component });
|
||||
5
src/routes/earnings/-/EarningsRoute.tsx
Normal file
5
src/routes/earnings/-/EarningsRoute.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import SectionSurface from "@components/surface/SectionSurface";
|
||||
|
||||
export default function EarningsRoute() {
|
||||
return <SectionSurface>Hello "/earnings"!</SectionSurface>;
|
||||
}
|
||||
3
src/routes/earnings/index.tsx
Normal file
3
src/routes/earnings/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import component from "./-/EarningsRoute";
|
||||
export const Route = createFileRoute("/earnings/")({ component });
|
||||
94
src/routes/game/-/GameRoute.tsx
Normal file
94
src/routes/game/-/GameRoute.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
3
src/routes/game/index.tsx
Normal file
3
src/routes/game/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import component from "./-/GameRoute";
|
||||
export const Route = createFileRoute("/game/")({ component });
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
11
src/routes/roulette/-/RouletteRoute.tsx
Normal file
11
src/routes/roulette/-/RouletteRoute.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
3
src/routes/roulette/index.tsx
Normal file
3
src/routes/roulette/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import component from "./-/RouletteRoute";
|
||||
export const Route = createFileRoute("/roulette/")({ component });
|
||||
5
src/routes/shop/-/ShopRoute.tsx
Normal file
5
src/routes/shop/-/ShopRoute.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import SectionSurface from "@components/surface/SectionSurface";
|
||||
|
||||
export default function ShopRoute() {
|
||||
return <SectionSurface>Shop</SectionSurface>;
|
||||
}
|
||||
3
src/routes/shop/index.tsx
Normal file
3
src/routes/shop/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import component from "./-/ShopRoute";
|
||||
export const Route = createFileRoute("/shop/")({ component });
|
||||
5
src/routes/tasks/-/TasksRoute.tsx
Normal file
5
src/routes/tasks/-/TasksRoute.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import SectionSurface from "@components/surface/SectionSurface";
|
||||
|
||||
export default function TasksRoute() {
|
||||
return <SectionSurface>Hello "/tasks"!</SectionSurface>;
|
||||
}
|
||||
3
src/routes/tasks/index.tsx
Normal file
3
src/routes/tasks/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import component from "./-/TasksRoute";
|
||||
export const Route = createFileRoute("/tasks/")({ component });
|
||||
31
src/styles/fonts/BalsamiqSans.css
Normal file
31
src/styles/fonts/BalsamiqSans.css
Normal 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
22
src/styles/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user