mirror of
https://github.com/jakobkordez/ham-reserve.git
synced 2025-08-04 04:07:40 +00:00
Initial commit
This commit is contained in:
6
next-app/.eslintrc.json
Normal file
6
next-app/.eslintrc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "prettier"],
|
||||
"rules": {
|
||||
"no-unused-vars": "warn"
|
||||
}
|
||||
}
|
4
next-app/.prettierrc.json
Normal file
4
next-app/.prettierrc.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
1
next-app/README.md
Normal file
1
next-app/README.md
Normal file
@ -0,0 +1 @@
|
||||
# Ham reserve front-end
|
4
next-app/next.config.js
Normal file
4
next-app/next.config.js
Normal file
@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
34
next-app/package.json
Normal file
34
next-app/package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "next-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "20.5.8",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"autoprefixer": "10.4.15",
|
||||
"axios": "^1.5.0",
|
||||
"eslint": "8.48.0",
|
||||
"eslint-config-next": "13.4.19",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"next": "13.4.19",
|
||||
"postcss": "8.4.29",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-secure-storage": "^1.3.0",
|
||||
"tailwindcss": "3.3.3",
|
||||
"typescript": "5.2.2",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"sass": "^1.66.1"
|
||||
}
|
||||
}
|
6
next-app/postcss.config.js
Normal file
6
next-app/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
41
next-app/src/api.ts
Normal file
41
next-app/src/api.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import axios from 'axios';
|
||||
import { User } from './interfaces/user.interface';
|
||||
|
||||
const baseURL = 'http://localhost:3001/';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
interface LoginResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export const apiFunctions = {
|
||||
login: async (username: string, password: string) => {
|
||||
return await api.post<LoginResponse>('/auth/login', { username, password });
|
||||
},
|
||||
refresh: async (refreshToken: string) => {
|
||||
return await api.get<LoginResponse>('/auth/refresh', {
|
||||
headers: { Authorization: `Bearer ${refreshToken}` },
|
||||
});
|
||||
},
|
||||
getMe: async (accessToken: string) => {
|
||||
return await api.get<User>('/users/me', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
logout: async (accessToken: string) => {
|
||||
return await api.get('/auth/logout', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
BIN
next-app/src/app/favicon.ico
Normal file
BIN
next-app/src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
11
next-app/src/app/globals.scss
Normal file
11
next-app/src/app/globals.scss
Normal file
@ -0,0 +1,11 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
.text-input {
|
||||
@apply rounded border border-gray-400 px-4 py-2 dark:bg-[#343434] dark:text-white;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700;
|
||||
}
|
28
next-app/src/app/layout.tsx
Normal file
28
next-app/src/app/layout.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Header } from '@/components/header';
|
||||
import './globals.scss';
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${inter.className} dark:bg-[#232323] dark:text-[#eeeeee]`}
|
||||
>
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
59
next-app/src/app/login/page.tsx
Normal file
59
next-app/src/app/login/page.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { apiFunctions } from '@/api';
|
||||
import { useAuthState } from '@/state/auth-state';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function Login() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
const isValid = useAuthState((s) => s.isValid);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
isValid().then((r) => {
|
||||
if (r) router.replace('/');
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto my-10 flex max-w-2xl flex-col gap-4 rounded-xl bg-gray-100 p-10 dark:bg-[#454545]">
|
||||
<h1 className="text-2xl font-bold">Prijava</h1>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="username">Uporabniško ime</label>
|
||||
<input
|
||||
id="username"
|
||||
type="username"
|
||||
className="text-input"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="password">Geslo</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
className="text-input"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="button" onClick={() => login(username, password)}>
|
||||
Prijava
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
try {
|
||||
const res = (await apiFunctions.login(username, password)).data;
|
||||
useAuthState.setState(res);
|
||||
window.location.href = '/';
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
7
next-app/src/app/page.tsx
Normal file
7
next-app/src/app/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<div>Wassup</div>
|
||||
</>
|
||||
);
|
||||
}
|
62
next-app/src/components/header.tsx
Normal file
62
next-app/src/components/header.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { User } from '@/interfaces/user.interface';
|
||||
import { useAuthState } from '@/state/auth-state';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<div className="flex h-16 select-none flex-row justify-between bg-gray-100 shadow dark:bg-[#454545]">
|
||||
<Link href="/" className="my-auto ml-4 text-2xl font-semibold">
|
||||
Ham Reserve
|
||||
</Link>
|
||||
<div className="flex flex-row gap-4">
|
||||
<UserHeader />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserHeader() {
|
||||
const [user, setUser] = useState<User>();
|
||||
const [getUser, logout] = useAuthState((s) => [s.getUser, s.logout]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then((u) => setUser(u || undefined));
|
||||
}, []);
|
||||
|
||||
return user ? (
|
||||
<div className="relative h-full">
|
||||
<button
|
||||
className="flex h-full items-center p-6 hover:bg-white/20"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<div>{user.username.toUpperCase()}</div>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 z-10 mt-2 w-56 rounded-md border-gray-500 bg-white/20 py-2 shadow-md">
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
window.location.reload();
|
||||
}}
|
||||
className="w-full px-4 py-2 text-left hover:bg-white/10"
|
||||
>
|
||||
Odjava
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative h-full">
|
||||
<Link
|
||||
href="/login"
|
||||
className="flex h-full items-center p-6 hover:bg-white/20"
|
||||
>
|
||||
Prijava
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
5
next-app/src/interfaces/user.interface.ts
Normal file
5
next-app/src/interfaces/user.interface.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
name: string;
|
||||
}
|
89
next-app/src/state/auth-state.ts
Normal file
89
next-app/src/state/auth-state.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { apiFunctions } from '@/api';
|
||||
import { User } from '@/interfaces/user.interface';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import secureLocalStorage from 'react-secure-storage';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export interface AuthState {
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
user: User | null;
|
||||
|
||||
getUser: () => Promise<User | null>;
|
||||
isValid: () => Promise<boolean>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const useAuthState = create(
|
||||
persist<AuthState>(
|
||||
(set, get) => ({
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
user: null,
|
||||
|
||||
getUser: async (): Promise<User | null> => {
|
||||
const { user, isValid } = get();
|
||||
if (user) return user;
|
||||
|
||||
if (!(await isValid())) return null;
|
||||
|
||||
try {
|
||||
const user = (await apiFunctions.getMe(get().accessToken!)).data;
|
||||
set({ user });
|
||||
return user;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
isValid: async () => {
|
||||
const { accessToken, refreshToken } = get();
|
||||
|
||||
if (accessToken) {
|
||||
const { exp } = jwtDecode(accessToken) as { exp: number };
|
||||
console.log('Access token exp', exp);
|
||||
if (Date.now() < exp * 1000) return true;
|
||||
else set({ accessToken: null });
|
||||
}
|
||||
|
||||
if (refreshToken) {
|
||||
const { exp } = jwtDecode(refreshToken) as { exp: number };
|
||||
console.log('Refresh token exp', exp);
|
||||
if (Date.now() < exp * 1000) {
|
||||
// Try to refresh
|
||||
try {
|
||||
set((await apiFunctions.refresh(refreshToken)).data);
|
||||
return true;
|
||||
} catch (e) {
|
||||
set({ refreshToken: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set({ refreshToken: null, accessToken: null, user: null });
|
||||
return false;
|
||||
},
|
||||
logout: async () => {
|
||||
set({ accessToken: null, refreshToken: null, user: null });
|
||||
if (await get().isValid()) apiFunctions.logout(get().accessToken!);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
},
|
||||
// {
|
||||
// name: 'auth-storage',
|
||||
// storage: {
|
||||
// getItem: (key) => {
|
||||
// const value = secureLocalStorage.getItem(key) as string;
|
||||
// return value ? JSON.parse(value) : null;
|
||||
// },
|
||||
// setItem: (key, value) => {
|
||||
// secureLocalStorage.setItem(key, JSON.stringify(value));
|
||||
// },
|
||||
// removeItem: secureLocalStorage.removeItem,
|
||||
// },
|
||||
// },
|
||||
),
|
||||
);
|
12
next-app/tailwind.config.ts
Normal file
12
next-app/tailwind.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
27
next-app/tsconfig.json
Normal file
27
next-app/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
2584
next-app/yarn.lock
Normal file
2584
next-app/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user