mirror of
https://github.com/mudabbir-ahmad/peworkshop.git
synced 2026-10-07 19:50:20 +00:00
redo of entire app
This commit is contained in:
1 parent
7dee4879a7
commit
093dd7ea9e
45 files changed
+686
-5659
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
@@ -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' });
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
+46
-24
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import './globals.css';
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function Home() {
|
||||
redirect('/login'); // Redirect to the login page
|
||||
}
|
||||
-103
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user