Compare commits
14 Commits
f065c31fc3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 39e3c38aa0 | |||
| 268f404cbe | |||
| c2c387648f | |||
| 967211565b | |||
| ec9d9ad1c5 | |||
| 983a63ce0b | |||
| 46625b03a8 | |||
| 83eb5656c1 | |||
| ca7fd3edfa | |||
| 5699a4a60b | |||
| 8ee097b774 | |||
| aee597acf2 | |||
| 04b2536aac | |||
| 0647d5f90e |
144
app/api/client.ts
Normal file
144
app/api/client.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { useUserStore } from "~/hooks/user";
|
||||||
|
|
||||||
|
interface get {
|
||||||
|
"/item": {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
uuid: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return: Item;
|
||||||
|
};
|
||||||
|
"/items": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
};
|
||||||
|
return?: Array<Item>;
|
||||||
|
};
|
||||||
|
"/case": {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
uuid: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return: Case;
|
||||||
|
};
|
||||||
|
"/cases": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
};
|
||||||
|
return?: Array<Case>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface post {
|
||||||
|
"/login": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
body: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
token?: never;
|
||||||
|
};
|
||||||
|
return: User;
|
||||||
|
};
|
||||||
|
"/logout": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
body?: never;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
return?: never;
|
||||||
|
};
|
||||||
|
"/register": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
body: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
token?: never;
|
||||||
|
};
|
||||||
|
return?: never;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
rarity: number;
|
||||||
|
price: number;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Case {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
uuid: string;
|
||||||
|
username: string;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
baseUrl: string;
|
||||||
|
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async GET<T extends keyof get>(
|
||||||
|
path: T,
|
||||||
|
parameters: get[T]["parameters"]
|
||||||
|
): Promise<[get[T]["return"], number]> {
|
||||||
|
let query = parameters["query"]
|
||||||
|
? "?" + new URLSearchParams(parameters["query"])
|
||||||
|
: "";
|
||||||
|
let response = await fetch(this.baseUrl + path + query, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let data = await response.json();
|
||||||
|
return [data, response.status];
|
||||||
|
}
|
||||||
|
|
||||||
|
async POST<T extends keyof post>(
|
||||||
|
path: T,
|
||||||
|
parameters: post[T]["parameters"]
|
||||||
|
): Promise<[post[T]["return"], number]> {
|
||||||
|
let query = parameters["query"]
|
||||||
|
? "?" + new URLSearchParams(parameters["query"])
|
||||||
|
: "";
|
||||||
|
let response = await fetch(this.baseUrl + path + query, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(parameters["body"]),
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: parameters["token"]
|
||||||
|
? "Bearer " + parameters["token"]
|
||||||
|
: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await response.json();
|
||||||
|
} catch (_) {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
return [data, response.status];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new Client("http://127.0.0.1:8000");
|
||||||
|
|
||||||
|
export default client;
|
||||||
76
app/components/navbar.tsx
Normal file
76
app/components/navbar.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useLocation, useNavigate } from "react-router";
|
||||||
|
import client from "~/api/client";
|
||||||
|
import { useUserStore } from "~/hooks/user";
|
||||||
|
|
||||||
|
export default function NavBar() {
|
||||||
|
const location = useLocation().pathname;
|
||||||
|
const user = useUserStore((state) => state.user);
|
||||||
|
const setUser = useUserStore((state) => state.setUser);
|
||||||
|
const [popupOpen, setPopupOpen] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function handleProfileClick() {
|
||||||
|
if (user) {
|
||||||
|
setPopupOpen(!popupOpen);
|
||||||
|
} else {
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
const [_, status] = await client.POST("/logout", {
|
||||||
|
token: user ? user.token : "",
|
||||||
|
});
|
||||||
|
setUser(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between p-2">
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<a
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
className={
|
||||||
|
"border-2 white rounded-md px-2 py-1" +
|
||||||
|
(location === "/" ? " bg-gray-500" : "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleProfileClick}
|
||||||
|
className="border-2 white rounded-md px-2 py-1"
|
||||||
|
>
|
||||||
|
{user ? user.username : "Login"}
|
||||||
|
{popupOpen ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="w-screen h-screen absolute top-0 left-0 z-[99]"
|
||||||
|
onClick={() => {
|
||||||
|
setPopupOpen(!popupOpen);
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div className="absolute mt-3 -ml-14 bg-red-600 flex flex-col rounded-md justify-center items-center z-[100] space-y-0.5 w-25 py-1">
|
||||||
|
<a className="px-2 py-1 hover:bg-blue-400 w-[90%] rounded-md">
|
||||||
|
Profile
|
||||||
|
</a>
|
||||||
|
<a className="px-2 py-1 hover:bg-blue-400 w-[90%] rounded-md">
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
<div className="bg-amber-300 w-[90%] h-[2px] rounded-md"></div>
|
||||||
|
<a
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="px-2 py-1 hover:bg-blue-400 w-[90%] rounded-md"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
app/hooks/user.tsx
Normal file
20
app/hooks/user.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
import type { User } from "../api/client";
|
||||||
|
|
||||||
|
interface UserStore {
|
||||||
|
user: User | null;
|
||||||
|
setUser: (user: User | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = create<UserStore>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
user: null,
|
||||||
|
setUser: (user: User | null) => set({ user }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import "./app.css";
|
import "../app.css";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
|
|
||||||
export function Welcome() {
|
export function HomePage() {
|
||||||
const [rolling, setRolling] = useState(false);
|
const [rolling, setRolling] = useState(false);
|
||||||
const [showResetPopup, setShowResetPopup] = useState(false);
|
const [showResetPopup, setShowResetPopup] = useState(false);
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export function Welcome() {
|
|||||||
<div className="w-full overflow-hidden mt-4">
|
<div className="w-full overflow-hidden mt-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex space-x-3"
|
className="flex space-x-3"
|
||||||
animate={rolling ? { x: [15000, -15110] } : null}
|
animate={rolling ? { x: [15000, -15110] } : undefined}
|
||||||
transition={
|
transition={
|
||||||
rolling
|
rolling
|
||||||
? {
|
? {
|
||||||
58
app/pages/login.tsx
Normal file
58
app/pages/login.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import client from "~/api/client";
|
||||||
|
import { useUserStore } from "~/hooks/user";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const user = useUserStore((state) => state.user);
|
||||||
|
const setUser = useUserStore((state) => state.setUser);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
const navigator = useNavigate();
|
||||||
|
|
||||||
|
async function login(event: React.FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
let username = formData.get("username");
|
||||||
|
let password = formData.get("password");
|
||||||
|
let [newUser, status] = await client.POST("/login", {
|
||||||
|
body: {
|
||||||
|
username: username as string,
|
||||||
|
password: password as string,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (status === 200) {
|
||||||
|
setUser(newUser);
|
||||||
|
navigator("/");
|
||||||
|
} else {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{user !== null ? (
|
||||||
|
<h1>You are already logged in.</h1>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h1>Login</h1>
|
||||||
|
{error && (
|
||||||
|
<h2 style={{ color: "red" }}>
|
||||||
|
An error occurred. Please try again.
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
<form onSubmit={login}>
|
||||||
|
<label>
|
||||||
|
Username:
|
||||||
|
<input type="text" name="username" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Password:
|
||||||
|
<input type="password" name="password" />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
app/pages/register.tsx
Normal file
54
app/pages/register.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import client from "~/api/client";
|
||||||
|
import { useUserStore } from "~/hooks/user";
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const user = useUserStore((state) => state.user);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
const navigator = useNavigate();
|
||||||
|
|
||||||
|
async function login(formData: FormData) {
|
||||||
|
let username = formData.get("username");
|
||||||
|
let password = formData.get("password");
|
||||||
|
let email = formData.get("email");
|
||||||
|
let [_, status] = await client.POST("/register", {
|
||||||
|
body: {
|
||||||
|
username: username as string,
|
||||||
|
password: password as string,
|
||||||
|
email: email as string,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (status === 200) {
|
||||||
|
navigator("/");
|
||||||
|
} else {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return user ? (
|
||||||
|
<h1>You are already logged in.</h1>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<h1>Register</h1>
|
||||||
|
{error && (
|
||||||
|
<h2 style={{ color: "red" }}>An error occurred. Please try again.</h2>
|
||||||
|
)}
|
||||||
|
<form action={login}>
|
||||||
|
<label>
|
||||||
|
Username:
|
||||||
|
<input type="text" name="username" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Password:
|
||||||
|
<input type="password" name="password" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Email:
|
||||||
|
<input type="email" name="email" />
|
||||||
|
</label>
|
||||||
|
<button type="submit">Register</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
|
|
||||||
import type { Route } from "./+types/root";
|
import type { Route } from "./+types/root";
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
|
import NavBar from "./components/navbar";
|
||||||
|
|
||||||
export const links: Route.LinksFunction = () => [
|
export const links: Route.LinksFunction = () => [
|
||||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||||
@@ -33,6 +34,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<NavBar />
|
||||||
{children}
|
{children}
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
import { type RouteConfig, index } from "@react-router/dev/routes";
|
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
||||||
|
|
||||||
export default [index("routes/home.tsx")] satisfies RouteConfig;
|
export default [
|
||||||
|
index("routes/home.tsx"),
|
||||||
|
route("/login", "routes/login.tsx"),
|
||||||
|
route("/register", "routes/register.tsx"),
|
||||||
|
] satisfies RouteConfig;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Route } from "./+types/home";
|
import type { Route } from "./+types/home";
|
||||||
import { Welcome } from "../index";
|
import { HomePage } from "../pages/home";
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
@@ -9,5 +9,5 @@ export function meta({}: Route.MetaArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <Welcome />;
|
return <HomePage />;
|
||||||
}
|
}
|
||||||
|
|||||||
17
app/routes/login.tsx
Normal file
17
app/routes/login.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Route } from "./+types/login";
|
||||||
|
import LoginPage from "../pages/login";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: "Aindustries' casino" },
|
||||||
|
{ name: "description", content: "Welcome to React Router!" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clientLoader() {}
|
||||||
|
|
||||||
|
export function HydrateFallback() {}
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
return <LoginPage />;
|
||||||
|
}
|
||||||
17
app/routes/register.tsx
Normal file
17
app/routes/register.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Route } from "./+types/login";
|
||||||
|
import RegisterPage from "../pages/register";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: "Aindustries' casino" },
|
||||||
|
{ name: "description", content: "Welcome to React Router!" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clientLoader() {}
|
||||||
|
|
||||||
|
export function HydrateFallback() {}
|
||||||
|
|
||||||
|
export default function Register() {
|
||||||
|
return <RegisterPage />;
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"motion": "^12.5.0",
|
"motion": "^12.5.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router": "^7.3.0"
|
"react-router": "^7.3.0",
|
||||||
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-router/dev": "^7.3.0",
|
"@react-router/dev": "^7.3.0",
|
||||||
|
|||||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
|||||||
react-router:
|
react-router:
|
||||||
specifier: ^7.3.0
|
specifier: ^7.3.0
|
||||||
version: 7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
zustand:
|
||||||
|
specifier: ^5.0.3
|
||||||
|
version: 5.0.3(@types/react@19.0.10)(react@19.0.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@react-router/dev':
|
'@react-router/dev':
|
||||||
specifier: ^7.3.0
|
specifier: ^7.3.0
|
||||||
@@ -2296,6 +2299,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
zustand@5.0.3:
|
||||||
|
resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=18.0.0'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=18.0.0'
|
||||||
|
use-sync-external-store: '>=1.2.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
use-sync-external-store:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
@@ -4474,3 +4495,8 @@ snapshots:
|
|||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
yaml@1.10.2: {}
|
yaml@1.10.2: {}
|
||||||
|
|
||||||
|
zustand@5.0.3(@types/react@19.0.10)(react@19.0.0):
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.0.10
|
||||||
|
react: 19.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user