created LoginPage, RegisterPage, next fonts, authService
This commit is contained in:
parent
9c45a0f227
commit
044c2870e7
@ -1 +1 @@
|
|||||||
# Nyanberg - Paradox Games Save Viewer
|
# paradox-save-parser-frontend
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@ -1,11 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { AgGridReact } from 'ag-grid-react'
|
import { AgGridReact } from 'ag-grid-react'
|
||||||
import styles from './page.module.css'
|
|
||||||
import { AllCommunityModule, themeQuartz } from 'ag-grid-community'
|
import { AllCommunityModule, themeQuartz } from 'ag-grid-community'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { colorSchemeDark } from 'ag-grid-community'
|
import { colorSchemeDark } from 'ag-grid-community'
|
||||||
|
|
||||||
export default function Home() {
|
export default function BrowsePage() {
|
||||||
const [agTheme, setAgTheme] = useState(themeQuartz)
|
const [agTheme, setAgTheme] = useState(themeQuartz)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -23,7 +23,7 @@ export default function Home() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page} style={{ height: '500px' }}>
|
<div style={{ height: '500px' }}>
|
||||||
<AgGridReact
|
<AgGridReact
|
||||||
theme={agTheme}
|
theme={agTheme}
|
||||||
modules={[AllCommunityModule]}
|
modules={[AllCommunityModule]}
|
||||||
@ -1,12 +1,13 @@
|
|||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
|
font-family: var(--font-ibm-plex-sans), monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: #0a0a0a;
|
--background: #0a0a0a;
|
||||||
--foreground: #ededed;
|
--foreground: #ffffff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,35 +18,9 @@ body {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Hack';
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
src: url('/fonts/hack-v3.003/Hack-Regular.ttf');
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Hack';
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: normal;
|
|
||||||
src: url('/fonts/hack-v3.003/Hack-Bold.ttf');
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Hack';
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: italic;
|
|
||||||
src: url('/fonts/hack-v3.003/Hack-Italic.ttf');
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Hack';
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: italic;
|
|
||||||
src: url('/fonts/hack-v3.003/Hack-BoldItalic.ttf');
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
font-family: 'Hack';
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
import type { Metadata } from 'next'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
|
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
|
||||||
|
import Navbar from '@/components/Navbar'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Create Next App',
|
title: 'Home',
|
||||||
description: 'Generated by create next app',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode
|
|
||||||
}>) {
|
|
||||||
ModuleRegistry.registerModules([AllCommunityModule])
|
ModuleRegistry.registerModules([AllCommunityModule])
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Nyanberg - Paradox Games Save Viewer</title>
|
<title>paradox-save-parser-frontend</title>
|
||||||
</head>
|
</head>
|
||||||
<body>{children}</body>
|
<body>
|
||||||
|
<Navbar />
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
98
src/app/login/page.tsx
Normal file
98
src/app/login/page.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { authService } from '@/services/authService'
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
stayLoggedIn: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value, type, checked } = e.target
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: type === 'checkbox' ? checked : value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
try {
|
||||||
|
console.log('Log in:', formData)
|
||||||
|
const result = await authService.login(formData.email, formData.password)
|
||||||
|
console.log('Logged in:', result)
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err.message)
|
||||||
|
setError(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-5" style={{ maxWidth: '500px' }}>
|
||||||
|
<h2 className="mb-4">Login</h2>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-danger" role="alert">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="email" className="form-label">
|
||||||
|
Email address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-control"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
autoComplete="email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="password" className="form-label">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-check mb-3">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="stayLoggedIn"
|
||||||
|
name="stayLoggedIn"
|
||||||
|
checked={formData.stayLoggedIn}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor="stayLoggedIn">
|
||||||
|
Stay logged in
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="btn btn-primary w-100">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
121
src/app/register/page.tsx
Normal file
121
src/app/register/page.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { authService } from '@/services/authService'
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
try {
|
||||||
|
if (formData.password !== formData.confirmPassword) {
|
||||||
|
throw new Error('Passwords do not match.')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Register', formData)
|
||||||
|
const result = await authService.register(formData.name, formData.email, formData.password)
|
||||||
|
console.log('Registered:', result)
|
||||||
|
|
||||||
|
router.push('/login') // Redirect after success
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err.message)
|
||||||
|
setError(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-5" style={{ maxWidth: '400px' }}>
|
||||||
|
<h2 className="mb-4 text-center">Register</h2>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-danger" role="alert">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="name" className="form-label">
|
||||||
|
Nickname
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="email" className="form-label">
|
||||||
|
Email address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-control"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="password" className="form-label">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="confirmPassword" className="form-label">
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="btn btn-primary w-100">
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
27
src/components/Navbar.tsx
Normal file
27
src/components/Navbar.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { font_Hack } from '@/utils/myFonts'
|
||||||
|
|
||||||
|
export default function Navbar() {
|
||||||
|
return (
|
||||||
|
<nav className={`navbar navbar-expand-lg navbar-dark bg-dark px-3 ${font_Hack.className}`}>
|
||||||
|
<div className="container-fluid">
|
||||||
|
{/* Left: Home Link */}
|
||||||
|
<Link href="/" className="navbar-brand">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Right: Login/Register Buttons */}
|
||||||
|
<div className="d-flex gap-2 ms-auto">
|
||||||
|
<Link href="/login" className="btn btn-outline-light rounded-pill px-4">
|
||||||
|
Log in
|
||||||
|
</Link>
|
||||||
|
<Link href="/register" className="btn btn-primary rounded-pill px-4">
|
||||||
|
Register
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
46
src/services/authService.ts
Normal file
46
src/services/authService.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
class AuthService {
|
||||||
|
private API_BASE = '/api/auth'
|
||||||
|
|
||||||
|
private hashPassword(password: string) {
|
||||||
|
// TODO: password hashing
|
||||||
|
return password
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(email: string, password: string) {
|
||||||
|
const response = await fetch(`${this.API_BASE}/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password: this.hashPassword(password),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Login failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(name: string, email: string, password: string) {
|
||||||
|
const response = await fetch(`${this.API_BASE}/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password: this.hashPassword(password),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Registration failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export a singleton instance
|
||||||
|
export const authService = new AuthService()
|
||||||
36
src/utils/myFonts.ts
Normal file
36
src/utils/myFonts.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import localFont from 'next/font/local'
|
||||||
|
import { IBM_Plex_Sans } from 'next/font/google'
|
||||||
|
|
||||||
|
export const font_IbmPlexSans = IBM_Plex_Sans({
|
||||||
|
subsets: ['latin'],
|
||||||
|
weight: ['400', '500', '700'],
|
||||||
|
display: 'swap',
|
||||||
|
variable: '--font-ibm-plex-sans',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const font_Hack = localFont({
|
||||||
|
src: [
|
||||||
|
{
|
||||||
|
path: '../../public/fonts/hack-v3.003/Hack-Regular.ttf',
|
||||||
|
weight: '400',
|
||||||
|
style: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '../../public/fonts/hack-v3.003/Hack-Bold.ttf',
|
||||||
|
weight: '700',
|
||||||
|
style: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '../../public/fonts/hack-v3.003/Hack-Italic.ttf',
|
||||||
|
weight: '400',
|
||||||
|
style: 'italic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '../../public/fonts/hack-v3.003/Hack-BoldItalic.ttf',
|
||||||
|
weight: '700',
|
||||||
|
style: 'italic',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
display: 'swap',
|
||||||
|
variable: '--font-hack',
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user