redo of entire app

This commit is contained in:
bobbert committed 2025-03-11 16:22:47 +00:00
1 parent 7dee4879a7
commit 093dd7ea9e
45 files changed
+686 -5659

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
'use client'; // Mark as a Client Component
import { useState } from 'react';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
export default function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleLogin = async (e) => {
e.preventDefault();
const result = await signIn('credentials', {
redirect: false,
username,
password,
});
if (result.error) {
setError(result.error);
} else {
router.push('/team-selection'); // Redirect to the team selection page after login
}
};
return (
<div>
<h1>Login</h1>
<form onSubmit={handleLogin}>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">Log In</button>
</form>
{error && <p className="error">{error}</p>}
<p>
Don't have an account? <a href="/register">Register</a>
</p>
</div>
);
}
+54
View File
@@ -0,0 +1,54 @@
'use client'; // Mark as a Client Component
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { query } from '../../../lib/db';
import bcrypt from 'bcryptjs';
export default function Register() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleRegister = async (e) => {
e.preventDefault();
const hashedPassword = bcrypt.hashSync(password, 10);
try {
await query(
'INSERT INTO users (username, password, role) VALUES (?, ?, ?)',
[username, hashedPassword, 'user']
);
router.push('/login'); // Redirect to the login page after registration
} catch (err) {
setError('Registration failed. Username may already exist.');
}
};
return (
<div>
<h1>Register</h1>
<form onSubmit={handleRegister}>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">Register</button>
</form>
{error && <p className="error">{error}</p>}
<p>
Already have an account? <a href="/login">Login</a>
</p>
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
'use client'; // Mark as a Client Component
import { useState } from 'react';
import { useSession } from 'next-auth/react';
export default function Admin() {
const { data: session } = useSession();
const [timerStarted, setTimerStarted] = useState(false);
if (session?.user?.role !== 'admin') {
return <p>Access denied</p>;
}
const startTimer = async () => {
// Implement timer logic (e.g., update database or state)
setTimerStarted(true);
};
return (
<div>
<h1>Admin Console</h1>
<button onClick={startTimer} disabled={timerStarted}>
Start Timer
</button>
</div>
);
}
+21
View File
@@ -0,0 +1,21 @@
import { query } from '../../../lib/db';
import { useSession } from 'next-auth/react';
export default async function Progress() {
const { data: session } = useSession();
// Fetch progress from the database
const progress = await query(
'SELECT COUNT(*) as found FROM clues WHERE found = TRUE AND team_id = ?',
[session.user.teamId]
);
const remaining = 20 - progress.found;
return (
<div>
<h1>Progress</h1>
<p>Clues Found: {progress.found}</p>
<p>Clues Remaining: {remaining}</p>
</div>
);
}
+33
View File
@@ -0,0 +1,33 @@
'use client'; // Mark as a Client Component
import { useState } from 'react';
import QRScanner from '../../../components/QRScanner';
import { useSession } from 'next-auth/react';
export default function Scan() {
const { data: session } = useSession();
const [message, setMessage] = useState('');
const handleScan = async (clueId) => {
const response = await fetch('/api/mark-clue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ clueId, teamId: session.user.teamId }),
});
if (response.ok) {
setMessage('Clue marked as found!');
} else {
setMessage('Error marking clue.');
}
};
return (
<div>
<h1>Scan QR Code</h1>
<QRScanner onScan={handleScan} />
{message && <p>{message}</p>}
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
import { query } from '../../../lib/db';
export default async function Stats({ params }) {
const { teamId } = params;
// Fetch team and clues from the database
const team = await query('SELECT * FROM teams WHERE id = ?', [teamId]);
const clues = await query('SELECT * FROM clues WHERE team_id = ?', [teamId]);
return (
<div>
<h1>Stats for {team.name}</h1>
<p>Team Code: {team.code}</p>
<h2>Clues</h2>
<ul>
{clues.map((clue) => (
<li key={clue.id}>
{clue.description} - {clue.found ? 'Found' : 'Not Found'}
</li>
))}
</ul>
</div>
);
}
+40
View File
@@ -0,0 +1,40 @@
'use client'; // Mark as a Client Component
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { query } from '../../../lib/db';
export default function TeamSelection() {
const [teamCode, setTeamCode] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleJoinTeam = async (e) => {
e.preventDefault();
const team = await query('SELECT * FROM teams WHERE code = ?', [teamCode]);
if (team) {
router.push(`/stats/${team.id}`);
} else {
setError('Team not found');
}
};
return (
<div>
<h1>Team Selection</h1>
<form onSubmit={handleJoinTeam}>
<input
type="text"
placeholder="Team Code"
value={teamCode}
onChange={(e) => setTeamCode(e.target.value)}
required
/>
<button type="submit">Join Team</button>
</form>
{error && <p style={{ color: 'red' }}>{error}</p>}
<p>
<a href="/create-team">Create a new team</a>
</p>
</div>
);
}
+38
View File
@@ -0,0 +1,38 @@
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import bcrypt from 'bcryptjs';
import { query } from '../../../lib/db';
export default NextAuth({
providers: [
Providers.Credentials({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
const user = await query('SELECT * FROM users WHERE username = ?', [credentials.username]);
if (user && bcrypt.compareSync(credentials.password, user.password)) {
return { id: user.id, name: user.username, role: user.role };
}
return null;
},
}),
],
callbacks: {
jwt: async (token, user) => {
if (user) {
token.role = user.role;
}
return token;
},
session: async (session, token) => {
session.user.role = token.role;
return session;
},
},
pages: {
signIn: '/login', // Redirect to the login page
},
});
+12
View File
@@ -0,0 +1,12 @@
import { query } from '../../../lib/db';
export async function POST(request) {
const { clueId, teamId } = await request.json();
await query('UPDATE clues SET found = TRUE WHERE id = ? AND team_id = ?', [
clueId,
teamId,
]);
return Response.json({ message: 'Clue marked as found' });
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+46 -24
View File
@@ -1,26 +1,48 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
/* Global Styles */
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
h1 {
color: #333;
}
form {
margin: 20px 0;
}
input {
display: block;
margin: 10px 0;
padding: 8px;
width: 100%;
max-width: 300px;
}
button {
padding: 10px 20px;
background-color: #0070f3;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #005bb5;
}
a {
color: #0070f3;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.error {
color: red;
}
+9
View File
@@ -0,0 +1,9 @@
import './globals.css';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
-34
View File
@@ -1,34 +0,0 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';
export default function Home() {
redirect('/login'); // Redirect to the login page
}
-103
View File
@@ -1,103 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
);
}