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

+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="identifier.sqlite" uuid="c7434bd6-ec43-49a7-bbac-7cea6b408619">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:identifier.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxBlameSettings">
<option name="version" value="2" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/clue-hunt-app.iml" filepath="$PROJECT_DIR$/.idea/clue-hunt-app.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+31 -20
View File
@@ -1,36 +1,47 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Next.js Workshop Project
## Getting Started
## running it locally
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Open [http://localhost:3000](http://localhost:3000) with your browser.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
## Figma Design page:
[The figma design](https://www.figma.com/design/aD2uqt6nOnxRnH8LhOzztD/PE-Treasure-Hunt-Design?node-id=0-1&t=qVGjYO9k4xWNhjuw-1)
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Expected features for the app.
- Record team names – Must have.
- Record names of team members – Could have.
- A start button to initiate the treasure hunt, which remains disabled until activated by an
admin or a specific time – Could have.
- Display treasure hunt clues sequentially once started – Must have.
- Store a minimum of 10 and a maximum of 20 clues centrally, to be displayed in the app –
Could have.
- For each clue, provide options to mark as "Found" (QR code located) or "Pass" (QR code not
found) - Should have.
- Allow QR code scanning, retrieve and display the associated value – Must have.
- Store the captured value with a timestamp – Could have.
- Display the number of clues completed, remaining clues, and time left throughout the hunt
– Could have.
- Show a summary of completed clues, QR codes, and total time taken at the end of the hunt
– Could have.
- Display an alert and prompt users to return to the venue if time runs out – Should have.
- Disable all interactions after the allotted time expires – Could have.
- Enable the CSM induction team to monitor the progress of all teams centrally – Could have.
- Allow the CSM induction team to send messages to the winning teams, including rankings
and congratulatory notes for the top three teams – Could have. -- thinking of not actually having it... way too much work.
- Allow the CSM induction team to configure the treasure hunt parameters centrally – Could
have. -> no clue what this means???
## Learn More
To learn more about Next.js, take a look at the following resources:
## To learn next.js
Next.js documentation:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+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>
);
}
BIN
View File
Binary file not shown.
+30
View File
@@ -0,0 +1,30 @@
'use client'; // Mark as a Client Component
import { useState } from 'react';
import QrReader from 'react-qr-reader';
export default function QRScanner({ onScan }) {
const [result, setResult] = useState('');
const handleScan = (data) => {
if (data) {
setResult(data);
onScan(parseInt(data, 10));
}
};
const handleError = (err) => {
console.error(err);
};
return (
<div>
<QrReader
delay={300}
onError={handleError}
onScan={handleScan}
style={{ width: '100%' }}
/>
<p>{result}</p>
</div>
);
}
+3
View File
@@ -0,0 +1,3 @@
INSERT INTO users (username, password, role) VALUES ('admin', 'hashed_password', 'admin');
INSERT INTO teams (name, code) VALUES ('Team Alpha', 'ALPHA123');
INSERT INTO clues (description, team_id) VALUES ('Find the red door', 1);
+3
View File
@@ -0,0 +1,3 @@
INSERT INTO users (username, password, role) VALUES ('admin', 'hashed_password', 'admin');
INSERT INTO teams (name, code) VALUES ('Team Alpha', 'ALPHA123');
INSERT INTO clues (description, team_id) VALUES ('Find the red door', 1);
+17
View File
@@ -0,0 +1,17 @@
const Database = require('better-sqlite3');
// Initialize the database connection
const db = new Database('./clue_hunt.db');
// Helper function to run queries
function query(sql, params) {
try {
const stmt = db.prepare(sql);
return params ? stmt.all(params) : stmt.all();
} catch (err) {
console.error('Error executing query:', err);
throw err;
}
}
module.exports = { query };
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
webpack5: true,
webpack: (config) => {
config.resolve.fallback = { fs: false };
return config;
},
};
+9
View File
@@ -4,4 +4,13 @@ const nextConfig: NextConfig = {
/* config options here */
};
module.exports = {
webpack5: true,
webpack: (config: { resolve: { fallback: { fs: boolean; }; }; }) => {
config.resolve.fallback = { fs: false };
return config;
},
};
export default nextConfig;
-5468
View File
File diff suppressed because it is too large. Load diff
+13 -5
View File
@@ -9,19 +9,27 @@
"lint": "next lint"
},
"dependencies": {
"bcryptjs": "^3.0.2",
"better-sqlite3": "^11.8.1",
"jsonwebtoken": "^9.0.2",
"next": "15.2.2",
"next-auth": "^4.24.11",
"qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.2.2"
"react-qr-reader": "^3.0.0-beta-1",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.2.2",
"@eslint/eslintrc": "^3"
"tailwindcss": "^4",
"typescript": "^5"
}
}
-1
View File
@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

-1
View File
@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

+19
View File
@@ -0,0 +1,19 @@
<svg width="402" height="874" viewBox="0 0 402 874" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="402" height="874" fill="#A1887F"/>
<line x1="200.5" x2="200.5" y2="874" stroke="#917A72" stroke-width="3"/>
<line x1="306.5" x2="306.5" y2="874" stroke="#917A72" stroke-width="3"/>
<line x1="98.5" x2="98.5" y2="874" stroke="#917A72" stroke-width="3"/>
<line x1="400.5" x2="400.5" y2="874" stroke="#917A72" stroke-width="3"/>
<path d="M0 721H402V784H0V721Z" fill="#917A72"/>
<circle cx="49.5" cy="752.5" r="14" stroke="black" stroke-width="3"/>
<circle cx="200.5" cy="752.5" r="14" stroke="black" stroke-width="3"/>
<circle cx="351.5" cy="752.5" r="14" stroke="black" stroke-width="3"/>
<path d="M0 107H402V170H0V107Z" fill="#917A72"/>
<circle cx="49.5" cy="138.5" r="14" stroke="black" stroke-width="3"/>
<circle cx="200.5" cy="138.5" r="14" stroke="black" stroke-width="3"/>
<circle cx="351.5" cy="138.5" r="14" stroke="black" stroke-width="3"/>
<line y1="168.5" x2="402" y2="168.5" stroke="black" stroke-width="3"/>
<line y1="105.5" x2="402" y2="105.5" stroke="black" stroke-width="3"/>
<line y1="782.5" x2="402" y2="782.5" stroke="black" stroke-width="3"/>
<line y1="719.5" x2="402" y2="719.5" stroke="black" stroke-width="3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="125" height="122" viewBox="0 0 125 122" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="3" width="122" height="122" fill="url(#pattern0_68_91)"/>
<defs>
<pattern id="pattern0_68_91" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_68_91" transform="scale(0.00195312)"/>
</pattern>
<image id="image0_68_91" width="512" height="512" preserveAspectRatio="none" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 46 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="122" height="122" viewBox="0 0 122 122" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="122" height="122" fill="url(#pattern0_68_98)"/>
<defs>
<pattern id="pattern0_68_98" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_68_98" transform="scale(0.00195312)"/>
</pattern>
<image id="image0_68_98" width="512" height="512" preserveAspectRatio="none" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 47 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="132" height="132" viewBox="0 0 132 132" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="52.3086" width="95" height="95" transform="rotate(33.4094 52.3086 0)" fill="url(#pattern0_0_343)"/>
<defs>
<pattern id="pattern0_0_343" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_0_343" transform="scale(0.00195312)"/>
</pattern>
<image id="image0_0_343" width="512" height="512" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAIABJREFUeJzs3XdYFNfeB/DvbGXZpSMIggJKs4EgAgqK0Rg1mmJNTKLGGnuqXZPcFE0vajRoElNMojHtppl2o1hRUbGi2AsoINJhF3bn/WP0vrmGcmZ3Z1l2f5/n4Xnv8+acmZ+wO+fMKb8DEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEELMxzV3AIQ0o1AAaQB63Pzfvjf//9UA8gEcBrADwE4Atc0QHzGfEkAKgF4AugIIAKC5+d8KAZwHkAlgG4BzzRAfIYQQG+sG4HkA2QB4xp9iAO8C6NAM8RJxOkD4WxWD/e+bDeEz0a0Z4iWEECIROYS3/LchvPWxNgr1/RgArAHgacP4CRtPAKsh/I0s+Rufh/BZSYPw2SGEENLCxEJ4EyyEZQ1CfT95APra7p9CmnAHhL+Jtf/OhQDeARBju38KIYQQc3gBmAEgC9ZvDG7/qQUw0Tb/LNKIyRD+FlL/vfdD+Gx52eafRQghpCkcgP4APoeweE/qhuDvPyZQJ6A5TYHwN7Dl37wawAYA/UCLqAkhpFkoAYyFsFLflg3A7T8G0HRAc+gH27z5N/aTDeBhCJ9FQgghEtMBmAPgApr34f/3nzzQ0LAteQK4jOb/u9/6yQfwHAAPCf/NhBDitPwAvARx27ts+bNKun86uc37aP6/d30/1wG8AOGzSgghxEJaAM8CKEfzP+Ab+zEACJPod0D+Xziaf+i/qZ9yAEshfHYJsVu0iIXYKzmACRAStARIdRM/nzb59/Qfn9sr4S4XDzcfP4Vcoa2qqSo4dGxH4bqNL3UsKr4q5m3uTQBPSRUrASBsy5vNWtjPp03+Y488d6pLeA9/tYumtclkqi4pKbyyY/+Wqn//+XF4QdFlyT5bEKaGngXwEQCjhPchxCzUASD26G4ArwDoJMXFw9p2PHP/gAmXenTr5+/qootCw9+Dyg3fvX3wyx9WpTBeughAa9DDXioKANcAeLMUfvCeWTvG3Ds7DoBrA0X4qpqKnMyDfxR8+9tHwecuHpdqBOcogLkAfpHo+oSYhToAxJ6EQ8jm1s/aF24XGHH+keFPno/rnBqiVKhCRFTlN3z39k4RnYAUCGcHEOtLBZDBUvCBoTN2PHTf470g4hlXW2c4f+BoxoVPvn6z3cW83BAzY2zMHwCmATgtwbUJEY06AMQeKAA8AWG4X9NEWWZqlabq/gETDtxz56PubjqPLjD7885XPvpM74qi4qv+DIVfgDD/S6zvRQCLmirUyifg6oevbnMDOHPn4PnyytLD3/36QcX3v6/vpjdUNzSCYI5qCJ+Pt0AjRaSZUQeANLeuAD4A0N1qF4xKPDZuxDPF4SFdYzmOc7PGNXcd+H3rslXT01iKQjiBjljfHgCJTRVaNHP11qRu/dOscUOe58tzzx8+tH7zaz5HcjI7WuOaN+2FkETqqBWvSYgo1AEgzUUN4W1uPqyQSIXjZKY7U4bvnzBqvkbr6t7F4uhuY6jTnx4+tTPLaYB1AHwAlFk7BifnCWGNRZMH9Hy95uhZlVJt9fn8yqrSIx9sXF79x86vE3iet8az0wBgGYCXb/5vQmyKOgCkOUQB2ATA4oZaIVcYhg+akjlq6PQAlUIt6XG9E+f2uVZwPY9lGuAeAD9IGYsTug/At00VauUTmP/hq9ukXNmPujrDxR/+/PTaZ9++FWuo1VsjC2A2gFEATlnhWoQwo+Muia09AODfAIItuYhK5VI9dczSXQtmrHKN7dizk1ymYFoZbomy8hvZx3L3t2UoWghgi9TxOJmZAHo0Veie/mOzu0YlsfyNzCaTyT2iO8S1GTl46jVPd5+Dh3P2+BtNRoUFl2wNYByAMwCOWydKQpoma+4AiNNQAVgB4AsAZs/LcxxnuvfO8RmbVh0qGpT2YB+5TC7p297f9e5xN89Y9E5JA3FOA1gK9U4carNnmkwmD7z7jod7b3ov+8bD9z2eyXEy1s9HfdwhjIq9A+G7QojkaAqA2EIwgI0Aki25SGzHXkfnT3sXWlf3ztYJSxyTyXTtvilRfozzv1EATkodk5PoDOBIU4U4juO/S88plMlkzZKKt1pfdeKN9Kf0mYf+iLXwUvshTAmcs0JYhDSIRgCI1PoBOAgLGv92QRHnPnh1294XnlrfubkafwCQyWT+ocHRZxmL3y9pMM7lPpZC4SFdc5ur8QcAjdo1evGs1bEfvrots11QhCWNd3cIuwTolEkiKeoAECk9BOBnCKviRVPIFbVPT35z68rnfwr08wlscv7XFgakjLjMWJQ6ANYzjKXQgNQR+VIHwqKVT2Diyud/Cnxq8ptb5TJ5nZmX8YWQOfABK4ZGyP+gDgCRyhwAn8DM+cyI0JhTn6/IOt0naWgahC2DdiE1cQjrka8JACRdjOYkQgAwDamnJAySfCGoCOq0pKFpm947eLZTePcT5l4DwOcA5lkxLkL+izoAxNpkELKcvQ0zPl8KuUL/9OQ3fntj8eYQjdo12urRWchd59XV29O/iKEoB8aha9KoYWBYq+Tj6X9N6+ohydkRllApNRHL538RvmR2+nalQmXOXn8OwHIAr4PWbBErow4AsSY1hFX+j5tTOSI05tSmVYcu90m6ZwDsdyW0bGi/R1jf6B6WNBLnwPQ7vHfAoydhv88zRY+YvqlfrMi62KFdZ3PPAXgKwGew3+8FaYHs9QtDWh41gO8hrF4WbcSgKTveWLy5jVKpbm/dsKyvf8pw1hzzCRBWsBPzxALoxlLwjp73u0sci8XUKpcOby39ts0DQ2fuMfMSYyAkQ6JOALEK6gAQa5AD+BTAXWIrKhUq/bK5GzLGjXgmBYC5h7fYlKe7b1cPN+9ixuLjpYzFwT3KUsjDzbvYw827pXS0NA/dNyfpjcWbtysUSnOmBAZD2FJrSeIhQgBQB4BYTg5haHKk2IoBrdpe2fB25pnOkT16Wz8sSSnu7vswa8a2h2GFsw6ckArCG2+T7uk//hhaWIMYERqT+tnbe3P9fIPyzKh+H4CPQc9vYiH6ABFLcADWwoytSj1i+manL/tDo9HorHnCms0M6T+WNZuhP4S3NiLOPRC2wjXp7jvGeEociyS0Gl2ndcv/dInr3LvJJEf1GAMgHbQwkFiAOgDEEq+DcZj27wb0Hp25ZHZ6BDjOnrZtieKm9YgJCY5kTQo0S9JgHNNslkLtgiLOaV09Wsrw/z9wnMz7+Sc+iBjS72Fz1gVMBPCutWMizoMOAyLmeh7CUb6iPHTfnK0TR81Pgh3t7TdXK5/Aw9v2/BDCUDQMwHcArkkbkcOIB/ASS8GnJr1+OMCvbYi04UhO0b1LWoBSodydfWK32NwRtxJkbbN2UMTx0QgAMcdwAEvEVnr80WXbHhg6Mw0tbL62Id079+6okCtqGYs/IWkwjuVJlkIKhdIQG93T7vb+m0k+8u5pvZY+vtachvxZUMZAYgbqABCx4iFk+GOee+Q4jn990eaMfikj+kgXlu1xnKxVv14jDjAWfxCAzU4ubMHagHFB6cA+ow9wMhnTOoGWIqFLWp9l8zZkcBwn5mRBDsAHAOIkCos4KOoAEDECIAxlu4qp9PLcz7ZHhsW0tJX+TMbcO5N1Gk0FxnltJ/c4GHdNjB4ywyH3w3eO6NH7xac+zhBZzRXCd7O1BCERB0UdAMLKBcA3AILEVFo8a83WzhEtbpsfM29P//jIsJhTjMVnQdgVQOoXAGA6S8GI0JhTnu6+TEmCWqKu0cl9FkxbuVVktWAI39EWv76G2AZ1AAirdQCSxFSYMfZffybG9kuTJhy7wc0Y92IBY1ktgAVSBtPCLQLj6NLjE18pgoNvgevZ/a60OeOXiR0JSAbwvhTxEMdDHQDCYhKEo32ZTRg1P2Ngnwf7SRSPXQkNikr09w1mTegyFcKbGvlf7SB8zprk5xuUFxzQPkHieOxC/9QRqRNGzRPbCRh384eQRtE2QNKUcIjMP37vgEd3P3TvnJ5w8De0v5EHB7Y/8Nfu70MYyioA6AD8IG1ILc7bALqzFFw0c/UBf982YRLHYy+46A5xwRVVZXtOnc0W03HsB2ATgBsSxUUcAI0AkMYoAWyA0GAx6RgenzNp9MJYONlnK7Zjr3id1qOUsfgE0Irtv+sBYCxLQXed540ukQlMHQUHIpvy4OKYqA5xOSLquEFI0e0QW26JNJzqIU1Eex7CiXZMvDz8CpfN3eAOQCNdSPaJ4zjd4xNeOchYXA5gDej7Bwi/gxVg/F08NfnNwwDXIg6NsjLX5fM2uHt6Line truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 51 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-1
View File
@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

-1
View File
@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

+61
View File
@@ -0,0 +1,61 @@
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import bcrypt from 'bcryptjs';
async function initDb() {
const db = await open({
filename: './clue_hunt.db',
driver: sqlite3.Database,
});
// Create tables
await db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user'
);
CREATE TABLE IF NOT EXISTS teams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS clues (
id INTEGER PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL,
found BOOLEAN NOT NULL DEFAULT FALSE,
skipped BOOLEAN NOT NULL DEFAULT FALSE,
team_id INTEGER,
FOREIGN KEY (team_id) REFERENCES teams(id)
);
CREATE TABLE IF NOT EXISTS progress (
id INTEGER PRIMARY KEY AUTOINCREMENT,
team_id INTEGER,
clue_id INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (team_id) REFERENCES teams(id),
FOREIGN KEY (clue_id) REFERENCES clues(id)
);
`);
// Seed initial data
const hashedPassword = bcrypt.hashSync('admin', 10);
await db.run(
'INSERT OR IGNORE INTO users (username, password, role) VALUES (?, ?, ?)',
['admin', hashedPassword, 'admin']
);
await db.run(
'INSERT OR IGNORE INTO teams (name, code) VALUES (?, ?)',
['Team Alpha', 'ALPHA123']
);
console.log('Database initialized successfully.');
await db.close();
}
initDb().catch((err) => console.error('Error initializing database:', err));
+18
View File
@@ -0,0 +1,18 @@
const { query } = require('../lib/db');
const bcrypt = require('bcryptjs');
async function seedDb() {
try {
const hashedPassword = bcrypt.hashSync('admin', 10);
await query(
'INSERT OR IGNORE INTO users (username, password, role) VALUES (?, ?, ?)',
['admin', hashedPassword, 'admin']
);
console.log('Database seeded successfully.');
} catch (err) {
console.error('Error seeding database:', err);
}
}
seedDb();
+12
View File
@@ -0,0 +1,12 @@
const { query } = require('../lib/db');
async function testDb() {
try {
const rows = await query('SELECT 1 + 1 AS result');
console.log('Database test successful:', rows);
} catch (err) {
console.error('Database test failed:', err);
}
}
testDb();
+16
View File
@@ -0,0 +1,16 @@
const Database = require('better-sqlite3');
// Use an in-memory database for testing
const db = new Database(':memory:');
// Create a table
db.exec('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
// Insert data
const insert = db.prepare('INSERT INTO test (name) VALUES (?)');
insert.run('Alice');
insert.run('Bob');
// Query data
const rows = db.prepare('SELECT * FROM test').all();
console.log(rows); // Should output: [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]