Compare commits

...

11 Commits

Author SHA1 Message Date
39e3c38aa0 Removed console log 2025-03-17 22:30:21 +01:00
268f404cbe Added navbar 2025-03-17 22:29:43 +01:00
c2c387648f Created navbar 2025-03-17 22:29:37 +01:00
967211565b Changed client to add token to logout call 2025-03-17 22:29:05 +01:00
ec9d9ad1c5 Added pages to routes 2025-03-17 20:01:43 +01:00
983a63ce0b Created UserStore hook 2025-03-17 20:01:37 +01:00
46625b03a8 Created register page 2025-03-17 20:01:23 +01:00
83eb5656c1 Created login page 2025-03-17 20:01:13 +01:00
ca7fd3edfa Added POST routes to client 2025-03-17 20:00:59 +01:00
5699a4a60b removed test 2025-03-17 20:00:42 +01:00
8ee097b774 Added zustand 2025-03-17 20:00:14 +01:00
13 changed files with 373 additions and 37 deletions

View File

@@ -1,4 +1,6 @@
interface paths {
import { useUserStore } from "~/hooks/user";
interface get {
"/item": {
parameters: {
query: {
@@ -11,12 +13,59 @@ interface paths {
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 {
id: number;
uuid: string;
name: string;
rarity: number;
@@ -24,6 +73,19 @@ export interface Item {
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;
@@ -31,20 +93,49 @@ class Client {
this.baseUrl = baseUrl;
}
GET<T extends keyof paths>(
async GET<T extends keyof get>(
path: T,
parameters: paths[T]["parameters"]
): Promise<paths[T]["return"]> {
parameters: get[T]["parameters"]
): Promise<[get[T]["return"], number]> {
let query = parameters["query"]
? "?" + new URLSearchParams(parameters["query"])
: "";
return fetch(this.baseUrl + path + query, {
let response = await fetch(this.baseUrl + path + query, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
// body: parameters ? JSON.stringify(parameters) : undefined,
}).then((response) => response.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];
}
}

76
app/components/navbar.tsx Normal file
View 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
View 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",
}
)
);

58
app/pages/login.tsx Normal file
View 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
View 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>
);
}

View File

@@ -1,5 +0,0 @@
import type { Item } from "~/api/client";
export default function Test({ item }: { item: Item }) {
return <div>{item.name}</div>;
}

View File

@@ -9,6 +9,7 @@ import {
import type { Route } from "./+types/root";
import "./app.css";
import NavBar from "./components/navbar";
export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
@@ -33,6 +34,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
<Links />
</head>
<body>
<NavBar />
{children}
<ScrollRestoration />
<Scripts />

View File

@@ -2,5 +2,6 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("/test", "routes/test.tsx"),
route("/login", "routes/login.tsx"),
route("/register", "routes/register.tsx"),
] satisfies RouteConfig;

17
app/routes/login.tsx Normal file
View 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
View 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 />;
}

View File

@@ -1,22 +0,0 @@
import type { Route } from "./+types/home";
import Test from "../pages/test";
import client from "~/api/client";
import type { Item } from "~/api/client";
export function meta({}: Route.MetaArgs) {
return [
{ title: "Aindustries' casino" },
{ name: "description", content: "Welcome to React Router!" },
];
}
export async function clientLoader() {
let item = await client.GET("/item", {
query: { uuid: "eee91ea1-1827-482b-b298-63bd6eda0221" },
});
return item;
}
export default function TestPage({ loaderData: item }: { loaderData: Item }) {
return <Test item={item} />;
}

View File

@@ -15,7 +15,8 @@
"motion": "^12.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.3.0"
"react-router": "^7.3.0",
"zustand": "^5.0.3"
},
"devDependencies": {
"@react-router/dev": "^7.3.0",

26
pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ importers:
react-router:
specifier: ^7.3.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:
'@react-router/dev':
specifier: ^7.3.0
@@ -2296,6 +2299,24 @@ packages:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
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:
'@ampproject/remapping@2.3.0':
@@ -4474,3 +4495,8 @@ snapshots:
yallist@3.1.1: {}
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