);
}
\ No newline at end of file
diff --git a/app/(main)/team-selection/page.js b/app/(main)/team-selection/page.js
index 39c6867..dce8e03 100644
--- a/app/(main)/team-selection/page.js
+++ b/app/(main)/team-selection/page.js
@@ -1,40 +1,81 @@
'use client'; // Mark as a Client Component
import { useState } from 'react';
+import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
-import { query } from '../../../lib/db';
+import { joinTeam } from '../actions/auth';
export default function TeamSelection() {
+ const { data: session, status } = useSession();
const [teamCode, setTeamCode] = useState('');
const [error, setError] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
+ // Redirect if not authenticated
+ if (status === 'unauthenticated') {
+ router.push('/login');
+ return null;
+ }
+
+ // Redirect if user is admin
+ if (status === 'authenticated' && session?.user?.role === 'admin') {
+ router.push('/admin');
+ return null;
+ }
+
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');
+ setIsLoading(true);
+ setError('');
+
+ try {
+ const result = await joinTeam(session.user.id, teamCode);
+
+ if (result.success) {
+ router.push(`/team/${result.teamId}`);
+ } else {
+ setError(result.error || 'Team not found');
+ }
+ } catch (err) {
+ setError('An unexpected error occurred');
+ console.error(err);
+ } finally {
+ setIsLoading(false);
}
};
+ if (status === 'loading') {
+ return
Loading...
-
Team Selection
-
- {error &&
{error}
}
-
- Create a new team
-
+
+
+
Team Selection
+
+ {error &&
{error}
}
+
+
);
}
\ No newline at end of file
diff --git a/app/actions/auth.js b/app/actions/auth.js
new file mode 100644
index 0000000..53b61e1
--- /dev/null
+++ b/app/actions/auth.js
@@ -0,0 +1,140 @@
+// app/actions/auth.js
+'use server'
+
+import { queryOne, run } from '../../lib/db';
+import bcrypt from 'bcryptjs';
+
+// Register a new user
+export async function registerUser(formData) {
+ try {
+ const username = formData.get('username');
+ const password = formData.get('password');
+
+ // Check if user already exists
+ const existingUser = await queryOne(
+ 'SELECT * FROM users WHERE username = ?',
+ [username]
+ );
+
+ if (existingUser) {
+ return { success: false, error: 'Username already exists' };
+ }
+
+ // Hash the password
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ // Insert the new user
+ const result = await run(
+ 'INSERT INTO users (username, password, role) VALUES (?, ?, ?)',
+ [username, hashedPassword, 'user']
+ );
+
+ return {
+ success: true,
+ userId: result.lastID
+ };
+ } catch (error) {
+ console.error('Registration error:', error);
+ return {
+ success: false,
+ error: 'Registration failed: ' + error.message
+ };
+ }
+}
+
+// Login function (to be used with NextAuth)
+export async function validateCredentials(username, password) {
+ try {
+ const user = await queryOne(
+ 'SELECT * FROM users WHERE username = ?',
+ [username]
+ );
+
+ if (!user) {
+ return null;
+ }
+
+ const isValid = await bcrypt.compare(password, user.password);
+
+ if (!isValid) {
+ return null;
+ }
+
+ return {
+ id: user.id,
+ username: user.username,
+ role: user.role
+ };
+ } catch (error) {
+ console.error('Login error:', error);
+ return null;
+ }
+}
+
+// Start hunt timer (admin only)
+export async function startHuntTimer(active) {
+ try {
+ // First check if a record exists
+ const currentTimer = await queryOne('SELECT * FROM game_settings WHERE id = 1');
+
+ if (currentTimer) {
+ // Update existing record
+ await run(
+ 'UPDATE game_settings SET hunt_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
+ [active ? 1 : 0]
+ );
+ } else {
+ // Create new record
+ await run(
+ 'INSERT INTO game_settings (id, hunt_active, created_at, updated_at) VALUES (1, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)',
+ [active ? 1 : 0]
+ );
+ }
+
+ return { success: true };
+ } catch (error) {
+ console.error('Timer update error:', error);
+ return { success: false, error: error.message };
+ }
+}
+
+// Get hunt timer status
+export async function getHuntStatus() {
+ try {
+ const settings = await queryOne('SELECT * FROM game_settings WHERE id = 1');
+ return {
+ active: settings?.hunt_active === 1,
+ updatedAt: settings?.updated_at
+ };
+ } catch (error) {
+ console.error('Get hunt status error:', error);
+ return { active: false };
+ }
+}
+
+// Join a team
+export async function joinTeam(userId, teamCode) {
+ try {
+ // Find the team by code
+ const team = await queryOne('SELECT * FROM teams WHERE code = ?', [teamCode]);
+
+ if (!team) {
+ return { success: false, error: 'Team not found' };
+ }
+
+ // Update user's team
+ await run(
+ 'UPDATE users SET team_id = ? WHERE id = ?',
+ [team.id, userId]
+ );
+
+ return {
+ success: true,
+ teamId: team.id,
+ teamName: team.name
+ };
+ } catch (error) {
+ console.error('Join team error:', error);
+ return { success: false, error: error.message };
+ }
+}
\ No newline at end of file
diff --git a/app/api/auth/[...nextauth]/route.js b/app/api/auth/[...nextauth]/route.js
new file mode 100644
index 0000000..d2cd90c
--- /dev/null
+++ b/app/api/auth/[...nextauth]/route.js
@@ -0,0 +1,55 @@
+// app/api/auth/[...nextauth]/route.js
+import NextAuth from 'next-auth';
+import CredentialsProvider from 'next-auth/providers/credentials';
+import { validateCredentials } from '../../../actions/auth';
+
+export const authOptions = {
+ providers: [
+ CredentialsProvider({
+ name: 'Credentials',
+ credentials: {
+ username: { label: "Username", type: "text" },
+ password: { label: "Password", type: "password" }
+ },
+ async authorize(credentials) {
+ if (!credentials?.username || !credentials?.password) {
+ return null;
+ }
+
+ const user = await validateCredentials(
+ credentials.username,
+ credentials.password
+ );
+
+ return user;
+ }
+ })
+ ],
+ callbacks: {
+ async jwt({ token, user }) {
+ if (user) {
+ token.id = user.id;
+ token.role = user.role;
+ token.username = user.username;
+ }
+ return token;
+ },
+ async session({ session, token }) {
+ if (token) {
+ session.user.id = token.id;
+ session.user.role = token.role;
+ session.user.username = token.username;
+ }
+ return session;
+ }
+ },
+ pages: {
+ signIn: '/login',
+ },
+ session: {
+ strategy: "jwt",
+ }
+};
+
+const handler = NextAuth(authOptions);
+export { handler as GET, handler as POST };
\ No newline at end of file
diff --git a/app/api/auth/[nextuath].js b/app/api/auth/[nextuath].js
deleted file mode 100644
index b7cff8b..0000000
--- a/app/api/auth/[nextuath].js
+++ /dev/null
@@ -1,38 +0,0 @@
-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
- },
-});
\ No newline at end of file
diff --git a/app/api/user/route.js b/app/api/user/route.js
new file mode 100644
index 0000000..0d34b8b
--- /dev/null
+++ b/app/api/user/route.js
@@ -0,0 +1,17 @@
+// app/api/user/route.js
+import { getServerSession } from 'next-auth/next';
+import { authOptions } from '../auth/[...nextauth]/route';
+
+export async function GET() {
+ const session = await getServerSession(authOptions);
+
+ if (!session) {
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ return Response.json({
+ id: session.user.id,
+ username: session.user.username,
+ role: session.user.role
+ });
+}
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
index fe94fe6..0a235ee 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -45,4 +45,342 @@ a:hover {
.error {
color: red;
+}
+
+
+/* Reset & Base Styles */
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ background-color: #f4f4f4;
+ color: #333;
+ min-height: 100vh;
+}
+
+/* Typography */
+h1 {
+ color: #333;
+ margin-bottom: 1rem;
+ font-size: 1.5rem;
+ text-align: center;
+}
+
+h2 {
+ color: #444;
+ margin-bottom: 0.75rem;
+ font-size: 1.25rem;
+}
+
+p {
+ margin-bottom: 1rem;
+}
+
+/* Layout Components */
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 1rem;
+}
+
+/* Card Styles */
+.auth-container,
+.team-selection-container,
+.admin-page,
+.stats-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ padding: 1rem;
+}
+
+.auth-card,
+.team-selection-card,
+.admin-card,
+.stats-card {
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 2rem;
+ width: 100%;
+ max-width: 400px;
+}
+
+/* Form Elements */
+.form-group {
+ margin-bottom: 1rem;
+}
+
+input[type="text"],
+input[type="password"],
+.auth-input,
+.team-input {
+ display: block;
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 1rem;
+ margin-bottom: 1rem;
+}
+
+button,
+.auth-button,
+.team-button {
+ display: block;
+ width: 100%;
+ padding: 0.75rem;
+ background-color: #0070f3;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+button:hover,
+.auth-button:hover,
+.team-button:hover {
+ background-color: #005bb5;
+}
+
+button:disabled {
+ background-color: #ccc;
+ cursor: not-allowed;
+}
+
+/* Admin Panel Styles */
+.timer-section {
+ margin-top: 1.5rem;
+ padding: 1rem;
+ border: 1px solid #eee;
+ border-radius: 4px;
+}
+
+.active {
+ color: #22c55e;
+ font-weight: bold;
+}
+
+.inactive {
+ color: #ef4444;
+ font-weight: bold;
+}
+
+.start-button {
+ background-color: #22c55e;
+}
+
+.start-button:hover {
+ background-color: #16a34a;
+}
+
+.stop-button {
+ background-color: #ef4444;
+}
+
+.stop-button:hover {
+ background-color: #dc2626;
+}
+
+.control-buttons {
+ margin-top: 1rem;
+}
+
+/* QR Scanner */
+.qr-scanner {
+ margin: 1rem 0;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+/* Links */
+a {
+ color: #0070f3;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+.auth-link {
+ margin-top: 1rem;
+ text-align: center;
+}
+
+.create-team-link {
+ display: block;
+ margin-top: 1rem;
+ text-align: center;
+}
+
+/* Error messages */
+.error {
+ color: #ef4444;
+ margin-top: 0.5rem;
+ font-size: 0.875rem;
+}
+
+/* Loading state */
+.loading {
+ text-align: center;
+ font-size: 1rem;
+ padding: 2rem;
+}
+
+/* Team page styled as in Figma */
+.team-page {
+ background-color: #f8e9a1;
+ padding: 1rem;
+ border-radius: 8px;
+}
+
+.user-box {
+ background-color: #d99e6a;
+ border-radius: 8px;
+ padding: 0.75rem;
+ margin-bottom: 0.75rem;
+ color: #fff;
+}
+
+.add-users {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: #f3cf55;
+ border-radius: 8px;
+ padding: 0.5rem 1rem;
+ margin-bottom: 1rem;
+}
+
+/* Stats page styled as in Figma */
+.stats-user-box {
+ background-color: #d99e6a;
+ border-radius: 4px;
+ padding: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.clue-box {
+ margin: 0.75rem 0;
+}
+
+.clue-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 0.5rem;
+ border-bottom: 1px solid #eee;
+}
+
+.found {
+ color: #22c55e;
+}
+
+.not-found {
+ color: #ef4444;
+}
+
+/* Progress page styled as in Figma */
+.progress-page {
+ background-color: #8fbc8f; /* Green background */
+ padding: 1rem;
+ border-radius: 8px;
+}
+
+.progress-map {
+ position: relative;
+ margin-top: 1rem;
+ min-height: 200px;
+}
+
+.progress-point {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: #fff;
+ position: absolute;
+}
+
+.progress-point.active {
+ background-color: #ff6b6b;
+}
+
+/* Navigation bar at bottom of screens as in Figma */
+.nav-bar {
+ display: flex;
+ justify-content: space-around;
+ background-color: #f8f8f8;
+ border-top: 1px solid #ddd;
+ padding: 0.75rem 0;
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+}
+
+.nav-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ color: #777;
+}
+
+.nav-item.active {
+ color: #0070f3;
+}
+
+.nav-icon {
+ font-size: 1.5rem;
+ margin-bottom: 0.25rem;
+}
+
+/* Leaderboard */
+.leaderboard {
+ padding: 1rem;
+ background-color: #f8e9a1;
+ border-radius: 8px;
+}
+
+.team-row {
+ background-color: #f3cf55;
+ padding: 0.75rem;
+ margin-bottom: 0.75rem;
+ border-radius: 4px;
+ display: flex;
+ justify-content: space-between;
+}
+
+/* Clue page as in Figma */
+.clue-page {
+ background-color: #f3cf55;
+ padding: 1rem;
+ border-radius: 8px;
+}
+
+.clue-box {
+ background-color: #fff8dc;
+ padding: 1rem;
+ border-radius: 8px;
+ margin-bottom: 1rem;
+}
+
+.clue-description {
+ margin-bottom: 1rem;
+}
+
+.clue-buttons {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 1rem;
+}
+
+.clue-button {
+ width: 48%;
}
\ No newline at end of file
diff --git a/clue_hunt.db b/clue_hunt.db
index 66124d9..7ba33dd 100644
Binary files a/clue_hunt.db and b/clue_hunt.db differ
diff --git a/init-schema.sql b/init-schema.sql
new file mode 100644
index 0000000..dc42d54
--- /dev/null
+++ b/init-schema.sql
@@ -0,0 +1,60 @@
+-- Schema for clue_hunt.db
+
+-- Drop tables if they exist
+DROP TABLE IF EXISTS clues;
+DROP TABLE IF EXISTS users;
+DROP TABLE IF EXISTS teams;
+DROP TABLE IF EXISTS game_settings;
+
+-- Users table
+CREATE TABLE users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ password TEXT NOT NULL,
+ role TEXT NOT NULL DEFAULT 'user',
+ team_id INTEGER,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (team_id) REFERENCES teams(id)
+);
+
+-- Teams table
+CREATE TABLE teams (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ code TEXT UNIQUE NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Clues table
+CREATE TABLE clues (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ description TEXT NOT NULL,
+ team_id INTEGER NOT NULL,
+ found BOOLEAN DEFAULT FALSE,
+ found_at TIMESTAMP,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (team_id) REFERENCES teams(id)
+);
+
+-- Game settings table
+CREATE TABLE game_settings (
+ id INTEGER PRIMARY KEY,
+ hunt_active BOOLEAN DEFAULT FALSE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Insert initial admin user
+INSERT INTO users (username, password, role)
+VALUES ('admin', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', 'admin');
+-- Password is 'password123' - change this in production!
+
+-- Insert sample teams
+INSERT INTO teams (name, code) VALUES ('Team Alpha', 'ALPHA123');
+INSERT INTO teams (name, code) VALUES ('Team Beta', 'BETA456');
+INSERT INTO teams (name, code) VALUES ('Team Gamma', 'GAMMA789');
+
+-- Insert sample clues for Team Alpha
+INSERT INTO clues (description, team_id) VALUES ('Find the red door', 1);
+INSERT INTO clues (description, team_id) VALUES ('Look under the bridge', 1);
+INSERT INTO clues (description, team_id) VALUES ('Check the library',
\ No newline at end of file
diff --git a/lib/db.js b/lib/db.js
index 608a89f..ba58bd2 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -1,18 +1,69 @@
-const Database = require('better-sqlite3');
+// lib/db.js
+let 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;
- }
+// Only import sqlite3 on server-side
+if (typeof window === 'undefined') {
+ sqlite3 = require('sqlite3').verbose();
}
+// Initialize the database connection
+function getDb() {
+ if (!sqlite3) {
+ console.error('Cannot access SQLite on the client side');
+ return null;
+ }
-module.exports = { query };
\ No newline at end of file
+ return new sqlite3.Database('./clue_hunt.db', (err) => {
+ if (err) {
+ console.error('Error opening database:', err.message);
+ } else {
+ console.log('Connected to the SQLite database.');
+ }
+ });
+}
+
+// Helper function to run queries with results
+async function query(sql, params = []) {
+ if (typeof window !== 'undefined') {
+ throw new Error('Database queries cannot be executed on the client side');
+ }
+
+ const db = getDb();
+ return new Promise((resolve, reject) => {
+ db.all(sql, params, (err, rows) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(rows);
+ }
+ db.close(); // Close the database connection
+ });
+ });
+}
+
+// Helper for single row queries
+async function queryOne(sql, params = []) {
+ const rows = await query(sql, params);
+ return rows && rows.length > 0 ? rows[0] : null;
+}
+
+// Helper for insert/update/delete operations
+async function run(sql, params = []) {
+ if (typeof window !== 'undefined') {
+ throw new Error('Database operations cannot be executed on the client side');
+ }
+
+ const db = getDb();
+ return new Promise((resolve, reject) => {
+ db.run(sql, params, function(err) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve({ lastID: this.lastID, changes: this.changes });
+ }
+ db.close(); // Close the database connection
+ });
+ });
+}
+
+module.exports = { query, queryOne, run };
\ No newline at end of file
diff --git a/scripts/test-sqlite-fallback.js b/scripts/test-sqlite-fallback.js
new file mode 100644
index 0000000..2e70b0f
--- /dev/null
+++ b/scripts/test-sqlite-fallback.js
@@ -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();
\ No newline at end of file