diff --git a/apps/api/migrations/0001_initial.sql b/apps/api/migrations/0001_initial.sql index ec526c2..28b73cd 100644 --- a/apps/api/migrations/0001_initial.sql +++ b/apps/api/migrations/0001_initial.sql @@ -1,31 +1,40 @@ -- 1. Create Users Table CREATE TABLE users ( id TEXT PRIMARY KEY, + email TEXT UNIQUE, name TEXT, - email TEXT, - is_completed INTEGER DEFAULT 0, + completed INTEGER DEFAULT 0, created_at INTEGER DEFAULT (strftime('%s', 'now')) ); --- 2. Create Stalls Table -CREATE TABLE stalls ( - id INTEGER PRIMARY KEY, - name TEXT, - qr_slug TEXT UNIQUE, +-- 2. Create Items Table +CREATE TABLE items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + logo TEXT, + qr_slug TEXT UNIQUE NOT NULL, + total_voters INTEGER DEFAULT 0, + qualified_voters INTEGER DEFAULT 0, + qualified_rating_sum INTEGER DEFAULT 0, + non_qualified_rating_sum INTEGER DEFAULT 0, + qualified_avg_rating REAL DEFAULT 0, created_at INTEGER DEFAULT (strftime('%s', 'now')) ); -- 3. Create Ratings Table CREATE TABLE ratings ( - id INTEGER PRIMARY KEY, - user_id TEXT, - stall_id INTEGER, - rating INTEGER, + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + item_id INTEGER NOT NULL, + rating INTEGER NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')), - UNIQUE(user_id, stall_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (stall_id) REFERENCES stalls(id) ON DELETE CASCADE + FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE ); --- 4.Adding an index for email lookups -CREATE UNIQUE INDEX idx_users_email ON users(email); \ No newline at end of file +-- 4. One rating per user per item +CREATE UNIQUE INDEX unique_vote ON ratings(user_id, item_id); + +-- 5. Index for email lookups +CREATE UNIQUE INDEX idx_users_email ON users(email); diff --git a/apps/api/src/controllers/items.Controller.ts b/apps/api/src/controllers/items.Controller.ts new file mode 100644 index 0000000..f390a30 --- /dev/null +++ b/apps/api/src/controllers/items.Controller.ts @@ -0,0 +1,22 @@ +import type { Context } from 'hono' +import type { AppEnv } from '../types' +import { fetchItem } from '../services/items.Service' + +export const getItemBySlug = async (c: Context) => { + const slug = c.req.param('slug') + + if (!slug) { + return c.json({ success: false, message: 'Slug is required' }, 400) + } + + try { + const item = await fetchItem(c.env.DB, slug) + if (!item) { + return c.json({ success: false, message: 'Item not found' }, 404) + } + return c.json({ success: true, data: item }) + } catch (e: any) { + console.error('Item lookup error:', e) + return c.json({ success: false, message: 'Internal Server Error' }, 500) + } +} diff --git a/apps/api/src/controllers/stalls.Controller.ts b/apps/api/src/controllers/stalls.Controller.ts deleted file mode 100644 index 47ffbd3..0000000 --- a/apps/api/src/controllers/stalls.Controller.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Context } from 'hono' -import type { AppEnv } from '../types' -import { fetchStall } from '../services/stalls.Service' - -export const getStallBySlug = async (c: Context) => { - const slug = c.req.param('slug') - - if (!slug) { - return c.json({ success: false, message: 'Slug is required' }, 400) - } - - try { - const stall = await fetchStall(c.env.DB, slug) - if (!stall) { - return c.json({ success: false, message: 'Stall not found' }, 404) - } - return c.json({ success: true, data: stall }) - } catch (e: any) { - console.error('Stall lookup error:', e) - return c.json({ success: false, message: 'Internal Server Error' }, 500) - } -} diff --git a/apps/api/src/controllers/vote.Controller.ts b/apps/api/src/controllers/vote.Controller.ts index 5d0e7e5..25b0c8e 100644 --- a/apps/api/src/controllers/vote.Controller.ts +++ b/apps/api/src/controllers/vote.Controller.ts @@ -13,10 +13,10 @@ export const createVote = async (c: Context) => { return c.json({ success: false, message: 'Invalid JSON body' }, 400) } - const { stallId, rating } = body + const { itemId, rating } = body - if (typeof stallId !== 'number' || typeof rating !== 'number') { - return c.json({ success: false, message: 'stallId and rating must be numbers' }, 400) + if (typeof itemId !== 'number' || typeof rating !== 'number') { + return c.json({ success: false, message: 'itemId and rating must be numbers' }, 400) } if (rating < 0 || rating > 10) { diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 54f6690..68bfe06 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -8,7 +8,7 @@ export const users = sqliteTable('users', { createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), }); -export const stalls = sqliteTable('stalls', { +export const items = sqliteTable('items', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), description: text('description'), @@ -25,11 +25,11 @@ export const stalls = sqliteTable('stalls', { export const ratings = sqliteTable('ratings', { id: integer('id').primaryKey({ autoIncrement: true }), userId: text('user_id').notNull().references(() => users.id), - stallId: integer('stall_id').notNull().references(() => stalls.id), + itemId: integer('item_id').notNull().references(() => items.id), rating: integer('rating').notNull(), createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), }, (table) => ({ - unq: uniqueIndex('unique_vote').on(table.userId, table.stallId), + unq: uniqueIndex('unique_vote').on(table.userId, table.itemId), }) ); \ No newline at end of file diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 28436b6..bebd0d3 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -5,7 +5,7 @@ import type { AppEnv } from './types' import voteRoutes from './routes/vote.route' import healthRoutes from './routes/health.route' import progressRoutes from './routes/progress.route' -import stallsRoutes from './routes/stalls.route' +import itemsRoutes from './routes/items.route' import resultsRoutes from './routes/results.route' import userRoutes from './routes/user.route' import webhookRoutes from './routes/webhook.route' @@ -80,7 +80,7 @@ const api = app.basePath('/api/v1') api.route('/vote', voteRoutes) api.route('/health', healthRoutes) api.route('/progress', progressRoutes) -api.route('/stalls', stallsRoutes) +api.route('/items', itemsRoutes) api.route('/results', resultsRoutes) api.route('/user', userRoutes) diff --git a/apps/api/src/routes/items.route.ts b/apps/api/src/routes/items.route.ts new file mode 100644 index 0000000..52bbf89 --- /dev/null +++ b/apps/api/src/routes/items.route.ts @@ -0,0 +1,9 @@ +import { Hono } from 'hono' +import type { AppEnv } from '../types' +import { getItemBySlug } from '../controllers/items.Controller' + +const items = new Hono() + +items.get('/:slug', getItemBySlug) + +export default items diff --git a/apps/api/src/routes/results.route.ts b/apps/api/src/routes/results.route.ts index 541cf46..e7014a2 100644 --- a/apps/api/src/routes/results.route.ts +++ b/apps/api/src/routes/results.route.ts @@ -1,23 +1,23 @@ import { Hono } from 'hono'; import { getDb } from '../db/client'; -import { stalls, ratings } from '../db/schema'; +import { items, ratings } from '../db/schema'; import { eq } from 'drizzle-orm'; import type { AppEnv } from '../types'; -import { refreshStallAggregates } from '../services/stalls.Service'; +import { refreshItemAggregates } from '../services/items.Service'; const resultsRoutes = new Hono(); resultsRoutes.get('/', async (c) => { try { const db = getDb(c.env.DB); - const allStalls = await db.select().from(stalls); - const stallIds = allStalls.map(s => s.id); + const allItems = await db.select().from(items); + const itemIds = allItems.map(i => i.id); - // Refresh all stalls to ensure consistency (optional but good for a "Results" view) + // Refresh all items to ensure consistency (optional but good for a "Results" view) // In a high-traffic app, we might skip this and just read, but for this event, real-time-consistent view is best. - await refreshStallAggregates(c.env.DB, stallIds); + await refreshItemAggregates(c.env.DB, itemIds); // Fetch the updated data - const finalResults = await db.select().from(stalls); + const finalResults = await db.select().from(items); // Sort by qualifiedRatingSum descending for the leaderboard finalResults.sort((a, b) => (b.qualifiedRatingSum || 0) - (a.qualifiedRatingSum || 0)); diff --git a/apps/api/src/routes/stalls.route.ts b/apps/api/src/routes/stalls.route.ts deleted file mode 100644 index 4e81d3f..0000000 --- a/apps/api/src/routes/stalls.route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Hono } from 'hono' -import type { AppEnv } from '../types' -import { getStallBySlug } from '../controllers/stalls.Controller' - -const stalls = new Hono() - -stalls.get('/:slug', getStallBySlug) - -export default stalls diff --git a/apps/api/src/services/stalls.Service.ts b/apps/api/src/services/items.Service.ts similarity index 67% rename from apps/api/src/services/stalls.Service.ts rename to apps/api/src/services/items.Service.ts index 88a4350..7ab07ec 100644 --- a/apps/api/src/services/stalls.Service.ts +++ b/apps/api/src/services/items.Service.ts @@ -1,29 +1,29 @@ import { getDb } from '../db/client' -import { stalls, ratings } from '../db/schema' +import { items, ratings } from '../db/schema' import { eq, inArray, count } from 'drizzle-orm' -export const fetchStall = async (db: D1Database, slug: string) => { +export const fetchItem = async (db: D1Database, slug: string) => { const ormDb = getDb(db) - const result = await ormDb.select().from(stalls).where(eq(stalls.qrSlug, slug)).limit(1) + const result = await ormDb.select().from(items).where(eq(items.qrSlug, slug)).limit(1) return result[0] || null } -export const refreshStallAggregates = async (db: D1Database, stallIds: number[]) => { - if (stallIds.length === 0) return; +export const refreshItemAggregates = async (db: D1Database, itemIds: number[]) => { + if (itemIds.length === 0) return; const ormDb = getDb(db); - // 1. Get all ratings for these stalls + // 1. Get all ratings for these items const targetRatings = await ormDb .select() .from(ratings) - .where(inArray(ratings.stallId, stallIds)); + .where(inArray(ratings.itemId, itemIds)); - // 2. Get qualification status for all users who voted for these stalls + // 2. Get qualification status for all users who voted for these items const userIds = [...new Set(targetRatings.map(r => r.userId))]; if (userIds.length === 0) { - // If no ratings left, reset stalls (unlikely in this context but good for robustness) - for (const id of stallIds) { - await ormDb.update(stalls) + // If no ratings left, reset items (unlikely in this context but good for robustness) + for (const id of itemIds) { + await ormDb.update(items) .set({ totalVoters: 0, qualifiedVoters: 0, @@ -31,7 +31,7 @@ export const refreshStallAggregates = async (db: D1Database, stallIds: number[]) nonQualifiedRatingSum: 0, qualifiedAvgRating: 0 }) - .where(eq(stalls.id, id)); + .where(eq(items.id, id)); } return; } @@ -51,7 +51,7 @@ export const refreshStallAggregates = async (db: D1Database, stallIds: number[]) .map(u => u.userId) ); - // 3. Aggregate per stall + // 3. Aggregate per item const aggregates = new Map(); - for (const id of stallIds) { + for (const id of itemIds) { aggregates.set(id, { totalVoters: 0, qualifiedVoters: 0, @@ -69,8 +69,8 @@ export const refreshStallAggregates = async (db: D1Database, stallIds: number[]) } for (const rating of targetRatings) { - if (!rating.stallId) continue; - const agg = aggregates.get(rating.stallId); + if (!rating.itemId) continue; + const agg = aggregates.get(rating.itemId); if (!agg) continue; agg.totalVoters += 1; @@ -83,13 +83,13 @@ export const refreshStallAggregates = async (db: D1Database, stallIds: number[]) } // 4. Update the database - await Promise.all(stallIds.map(async (id) => { + await Promise.all(itemIds.map(async (id) => { const agg = aggregates.get(id)!; - const qualifiedAvgRating = agg.qualifiedVoters > 0 - ? Math.round((agg.qualifiedRatingSum / agg.qualifiedVoters) * 100) / 100 + const qualifiedAvgRating = agg.qualifiedVoters > 0 + ? Math.round((agg.qualifiedRatingSum / agg.qualifiedVoters) * 100) / 100 : 0; - await ormDb.update(stalls) + await ormDb.update(items) .set({ totalVoters: agg.totalVoters, qualifiedVoters: agg.qualifiedVoters, @@ -97,6 +97,6 @@ export const refreshStallAggregates = async (db: D1Database, stallIds: number[]) nonQualifiedRatingSum: agg.nonQualifiedRatingSum, qualifiedAvgRating: qualifiedAvgRating, }) - .where(eq(stalls.id, id)); + .where(eq(items.id, id)); })); } diff --git a/apps/api/src/services/progress.Service.ts b/apps/api/src/services/progress.Service.ts index 842b7b1..a75a57d 100644 --- a/apps/api/src/services/progress.Service.ts +++ b/apps/api/src/services/progress.Service.ts @@ -1,5 +1,5 @@ import { getDb } from '../db/client' -import { ratings, users, stalls } from '../db/schema' +import { ratings, users, items } from '../db/schema' import { eq, count } from 'drizzle-orm' export const fetchProgress = async (db: D1Database, userId: string) => { @@ -13,12 +13,12 @@ export const fetchProgress = async (db: D1Database, userId: string) => { const userRatings = await ormDb .select({ - stallId: stalls.id, - stallName: stalls.name, + itemId: items.id, + itemName: items.name, rating: ratings.rating }) .from(ratings) - .innerJoin(stalls, eq(ratings.stallId, stalls.id)) + .innerJoin(items, eq(ratings.itemId, items.id)) .where(eq(ratings.userId, userId)) return { userId, progress: progressCount, isCompleted, ratings: userRatings } diff --git a/apps/api/src/services/vote.Service.ts b/apps/api/src/services/vote.Service.ts index c50e33c..0a9af3d 100644 --- a/apps/api/src/services/vote.Service.ts +++ b/apps/api/src/services/vote.Service.ts @@ -4,7 +4,7 @@ import { ratings, users } from '../db/schema' import { eq, count } from 'drizzle-orm' import type { AppEnv } from '../types' import { ensureUserExists } from './user.Service' -import { refreshStallAggregates } from './stalls.Service' +import { refreshItemAggregates } from './items.Service' function isVotingOpen(): boolean { const istOffset = 5.5 * 60 * 60 * 1000; const istTime = new Date(Date.now() + istOffset); @@ -42,7 +42,7 @@ export const submitVote = async ( throw new Error('Voting is currently closed'); } - const { stallId, rating } = vote + const { itemId, rating } = vote const ormDb = getDb(env.DB) try { @@ -50,7 +50,7 @@ export const submitVote = async ( await ensureUserExists(env, userId) // 2. Now proceed with the vote - await ormDb.insert(ratings).values({ userId, stallId, rating }) + await ormDb.insert(ratings).values({ userId, itemId, rating }) } catch (e: any) { if (e.message && e.message.includes('UNIQUE constraint failed')) { throw new Error('Already voted') @@ -74,22 +74,22 @@ export const submitVote = async ( .where(eq(users.id, userId)) } - // Update stall aggregates immediately + // Update item aggregates immediately if (progressCount === 11) { - // User just became qualified! All their stalls need refreshing + // User just became qualified! All their items need refreshing const userRatings = await ormDb - .select({ stallId: ratings.stallId }) + .select({ itemId: ratings.itemId }) .from(ratings) .where(eq(ratings.userId, userId)); - const stallIds = userRatings - .map(r => r.stallId) + const itemIds = userRatings + .map(r => r.itemId) .filter((id): id is number => id !== null); - await refreshStallAggregates(env.DB, stallIds); + await refreshItemAggregates(env.DB, itemIds); } else { - // Normal vote (could be qualified or not, but only this stall is affected) - await refreshStallAggregates(env.DB, [stallId]); + // Normal vote (could be qualified or not, but only this item is affected) + await refreshItemAggregates(env.DB, [itemId]); } return progressCount diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index a56fef2..7942dd4 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -30,13 +30,13 @@ export default function App() { return saved ? JSON.parse(saved) : {}; }); - const [ratedStalls, setRatedStalls] = useState>(() => { - const saved = localStorage.getItem('ratedStalls'); + const [ratedItems, setRatedItems] = useState>(() => { + const saved = localStorage.getItem('ratedItems'); return saved ? JSON.parse(saved) : []; }); - const [currentStallData, setCurrentStallData] = useState<{ id: number, name: string, description: string, logo: string } | null>(null); + const [currentItemData, setCurrentItemData] = useState<{ id: number, name: string, description: string, logo: string } | null>(null); - const totalStalls = 11; + const totalItems = 11; // Sync user to DB after Clerk sign-in and fetch true progress useEffect(() => { @@ -60,23 +60,23 @@ export default function App() { if (progJson.ratings && Array.isArray(progJson.ratings)) { const fetchedRatings: Record = {}; - const fetchedStalls: Array<{stallId: number, stallName: string, rating: number}> = []; - - progJson.ratings.forEach((r: { stallId: number, stallName: string, rating: number }) => { - fetchedRatings[r.stallId] = r.rating; - fetchedStalls.push({ stallId: r.stallId, stallName: r.stallName, rating: r.rating }); + const fetchedItems: Array<{itemId: number, itemName: string, rating: number}> = []; + + progJson.ratings.forEach((r: { itemId: number, itemName: string, rating: number }) => { + fetchedRatings[r.itemId] = r.rating; + fetchedItems.push({ itemId: r.itemId, itemName: r.itemName, rating: r.rating }); }); - + setRatings(fetchedRatings); - setRatedStalls(fetchedStalls); + setRatedItems(fetchedItems); localStorage.setItem('ratings', JSON.stringify(fetchedRatings)); - localStorage.setItem('ratedStalls', JSON.stringify(fetchedStalls)); + localStorage.setItem('ratedItems', JSON.stringify(fetchedItems)); } else { // If DB has 0 ratings, wipe the local cache to be completely accurate! setRatings({}); - setRatedStalls([]); + setRatedItems([]); localStorage.setItem('ratings', JSON.stringify({})); - localStorage.setItem('ratedStalls', JSON.stringify([])); + localStorage.setItem('ratedItems', JSON.stringify([])); } } } catch (e) { @@ -92,33 +92,33 @@ export default function App() { // Navigate based on auth state and URL params useEffect(() => { const params = new URLSearchParams(window.location.search); - let stallId = params.get('stallId'); + let itemId = params.get('itemId'); - if (!stallId && window.location.pathname !== '/' && window.location.pathname !== '') { + if (!itemId && window.location.pathname !== '/' && window.location.pathname !== '') { const pathSegments = window.location.pathname.split('/').filter(Boolean); if (pathSegments.length > 0) { - stallId = pathSegments[pathSegments.length - 1]; + itemId = pathSegments[pathSegments.length - 1]; } } - if (stallId) { + if (itemId) { if (isSignedIn) { - // Automatically verify and navigate to the stall - const fetchStallFromUrl = async () => { + // Automatically verify and navigate to the item + const fetchItemFromUrl = async () => { try { const token = await getToken(); - const res = await fetch(`${API_BASE}/api/v1/stalls/${stallId}`, { + const res = await fetch(`${API_BASE}/api/v1/items/${itemId}`, { headers: { Authorization: `Bearer ${token}` } }); const json = await res.json(); - + if (json.success && json.data) { - setCurrentStallData(json.data); - + setCurrentItemData(json.data); + if (ratings[json.data.id] !== undefined) { setFeedbackModal({ title: "Already Rated", - message: "You have already submitted a rating for this stall. Please scan a different stall's QR code to continue voting!", + message: "You have already submitted a rating for this item. Please scan a different item's QR code to continue voting!", type: 'error' }); setCurrentScreen('progress'); @@ -127,8 +127,8 @@ export default function App() { } } else { setFeedbackModal({ - title: "Invalid Stall Link", - message: "The link you followed does not belong to a valid stall for this event.", + title: "Invalid Item Link", + message: "The link you followed does not belong to a valid item for this event.", type: 'error' }); setCurrentScreen('progress'); @@ -145,7 +145,7 @@ export default function App() { }; setCurrentScreen('progress'); // Show loading state - fetchStallFromUrl(); + fetchItemFromUrl(); window.history.replaceState({}, document.title, '/'); } else { setCurrentScreen('login'); @@ -159,7 +159,7 @@ export default function App() { }, [isSignedIn]); const handleRatingSubmit = async (rating: number) => { - if (!currentStallData) return; + if (!currentItemData) return; try { const token = await getToken(); @@ -169,21 +169,21 @@ export default function App() { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, - body: JSON.stringify({ stallId: currentStallData.id, rating }) + body: JSON.stringify({ itemId: currentItemData.id, rating }) }); const json = await res.json(); if (json.success) { - const newRatings = { ...ratings, [currentStallData.id]: rating }; + const newRatings = { ...ratings, [currentItemData.id]: rating }; setRatings(newRatings); localStorage.setItem('ratings', JSON.stringify(newRatings)); - - setRatedStalls(prev => { - const exists = prev.find(s => s.stallId === currentStallData.id); - const updated = exists - ? prev.map(s => s.stallId === currentStallData.id ? { ...s, rating } : s) - : [...prev, { stallId: currentStallData.id, stallName: currentStallData.name, rating }]; - localStorage.setItem('ratedStalls', JSON.stringify(updated)); + + setRatedItems(prev => { + const exists = prev.find(i => i.itemId === currentItemData.id); + const updated = exists + ? prev.map(i => i.itemId === currentItemData.id ? { ...i, rating } : i) + : [...prev, { itemId: currentItemData.id, itemName: currentItemData.name, rating }]; + localStorage.setItem('ratedItems', JSON.stringify(updated)); return updated; }); @@ -192,11 +192,11 @@ export default function App() { setFeedbackModal({ title: "Vote Recorded!", - message: `You successfully rated ${currentStallData.name}! Your progress has been updated.`, + message: `You successfully rated ${currentItemData.name}! Your progress has been updated.`, type: 'success' }); - if (next >= totalStalls) { + if (next >= totalItems) { setCurrentScreen('completion'); } else { setCurrentScreen('progress'); @@ -223,41 +223,41 @@ export default function App() { }; const handleScanSuccess = async (decodedText: string) => { - let stallSlug = decodedText.trim(); - + let itemSlug = decodedText.trim(); + try { - const urlToParse = stallSlug.includes('://') ? stallSlug : `https://${stallSlug}`; + const urlToParse = itemSlug.includes('://') ? itemSlug : `https://${itemSlug}`; const url = new URL(urlToParse); - - if (url.searchParams.has('stallId')) { - stallSlug = url.searchParams.get('stallId') || decodedText; + + if (url.searchParams.has('itemId')) { + itemSlug = url.searchParams.get('itemId') || decodedText; } else { const pathSegments = url.pathname.split('/').filter(Boolean); if (pathSegments.length > 0) { - stallSlug = pathSegments[pathSegments.length - 1]; + itemSlug = pathSegments[pathSegments.length - 1]; } else { - stallSlug = decodedText; + itemSlug = decodedText; } } } catch { - const parts = stallSlug.split('?')[0].split('/').filter(Boolean); - stallSlug = parts[parts.length - 1] || decodedText; + const parts = itemSlug.split('?')[0].split('/').filter(Boolean); + itemSlug = parts[parts.length - 1] || decodedText; } try { const token = await getToken(); - const res = await fetch(`${API_BASE}/api/v1/stalls/${stallSlug}`, { + const res = await fetch(`${API_BASE}/api/v1/items/${itemSlug}`, { headers: { Authorization: `Bearer ${token}` } }); const json = await res.json(); - + if (json.success && json.data) { - setCurrentStallData(json.data); - + setCurrentItemData(json.data); + if (ratings[json.data.id] !== undefined) { setFeedbackModal({ title: "Already Rated", - message: "You have already submitted a rating for this stall. Please scan a different stall's QR code to continue voting!", + message: "You have already submitted a rating for this item. Please scan a different item's QR code to continue voting!", type: 'error' }); setCurrentScreen('progress'); @@ -266,8 +266,8 @@ export default function App() { } } else { setFeedbackModal({ - title: "Invalid Stall", - message: "The QR code you scanned does not belong to a valid stall for this event.", + title: "Invalid Item", + message: "The QR code you scanned does not belong to a valid item for this event.", type: 'error' }); setCurrentScreen('progress'); @@ -340,21 +340,21 @@ export default function App() { /> )} - {currentScreen === 'rating' && currentStallData && ( + {currentScreen === 'rating' && currentItemData && ( )} {currentScreen === 'progress' && ( setCurrentScreen('scanner')} - totalCount={totalStalls} + totalCount={totalItems} serverProgress={serverProgress} /> diff --git a/apps/web/src/components/CompletionScreen.tsx b/apps/web/src/components/CompletionScreen.tsx index 3545cc9..0a6a4bb 100644 --- a/apps/web/src/components/CompletionScreen.tsx +++ b/apps/web/src/components/CompletionScreen.tsx @@ -80,7 +80,7 @@ export default function CompletionScreen({

YOU'VE RATED ALL
- 11 STALLS! + 11 ITEMS!

Thank you for participating! diff --git a/apps/web/src/components/ProgressScreen.tsx b/apps/web/src/components/ProgressScreen.tsx index 4bd6c9c..62e9720 100644 --- a/apps/web/src/components/ProgressScreen.tsx +++ b/apps/web/src/components/ProgressScreen.tsx @@ -18,10 +18,10 @@ interface ProgressScreenProps { onProfile?: () => void; onScanNext?: () => void; ratings?: Record; - ratedStalls?: Array<{ stallId: number, stallName: string, rating: number }>; + ratedItems?: Array<{ itemId: number, itemName: string, rating: number }>; totalCount?: number; serverProgress?: number; - allStalls?: Array<{ id: number, name: string }>; + allItems?: Array<{ id: number, name: string }>; } export default function ProgressScreen({ @@ -29,7 +29,7 @@ export default function ProgressScreen({ ratings = {}, totalCount = 11, serverProgress = 0, - //allStalls: _allStalls = [] + //allItems: _allItems = [] }: ProgressScreenProps) { // Always use the server's synced progress as the ultimate source of truth const localRatedCount = Object.keys(ratings).length; @@ -78,7 +78,7 @@ export default function ProgressScreen({

-

Stall Directory

+

Item Directory

Live Updates @@ -87,12 +87,12 @@ export default function ProgressScreen({
{Array.from({ length: totalCount }).map((_, index) => { - const stallId = index + 1; + const itemId = index + 1; const status = index < ratedCount ? 'rated' : 'locked'; return ( ⚠️

- Your votes will be counted only if you rate all 11 stalls. + Your votes will be counted only if you rate all 11 items.

@@ -152,7 +152,7 @@ export default function ProgressScreen({ className="w-full py-5 bg-gradient-to-r from-emerald-500 to-teal-500 rounded-2xl text-white font-bold flex items-center justify-center gap-3 shadow-xl shadow-emerald-500/20 uppercase tracking-widest text-sm border border-emerald-300/30" > - All Stalls Rated! + All Items Rated!
)} diff --git a/apps/web/src/components/RatingScreen.tsx b/apps/web/src/components/RatingScreen.tsx index be8e189..ba0342b 100644 --- a/apps/web/src/components/RatingScreen.tsx +++ b/apps/web/src/components/RatingScreen.tsx @@ -3,13 +3,13 @@ import { motion } from 'motion/react'; import { Send } from 'lucide-react'; interface RatingScreenProps { - stallData: { id: number; name: string; description: string; logo: string | null }; + itemData: { id: number; name: string; description: string; logo: string | null }; onSubmitSuccess?: (rating: number) => void; ratedCount?: number; totalCount?: number; } -export default function RatingScreen({ stallData, onSubmitSuccess }: RatingScreenProps) { +export default function RatingScreen({ itemData, onSubmitSuccess }: RatingScreenProps) { const [selectedRating, setSelectedRating] = useState(null); const [isSubmitted, setIsSubmitted] = useState(false); @@ -60,15 +60,15 @@ export default function RatingScreen({ stallData, onSubmitSuccess }: RatingScree
- {/* CURRENT STALL */} + {/* CURRENT ITEM */}
- Current Stall + Current Item

- {stallData.name} + {itemData.name}

{/* Info Card */} @@ -93,7 +93,7 @@ export default function RatingScreen({ stallData, onSubmitSuccess }: RatingScree
⚠️

- Your votes will be counted only if you rate all 11 stalls. + Your votes will be counted only if you rate all 11 items.

@@ -267,7 +267,7 @@ export default function RatingScreen({ stallData, onSubmitSuccess }: RatingScree

- {stallData.description || "Innovating sustainable hardware for the next digital era."} + {itemData.description || "Innovating sustainable hardware for the next digital era."}

diff --git a/apps/web/src/components/ScannerScreen.tsx b/apps/web/src/components/ScannerScreen.tsx index 073b766..56528fa 100644 --- a/apps/web/src/components/ScannerScreen.tsx +++ b/apps/web/src/components/ScannerScreen.tsx @@ -140,8 +140,8 @@ export default function ScannerScreen({ onScanSuccess, onClose }: ScannerScreenP
-

Scan Stall QR Code

-

Align the QR code within the frame to start rating the stall.

+

Scan Item QR Code

+

Align the QR code within the frame to start rating the item.

@@ -166,7 +166,7 @@ export default function ScannerScreen({ onScanSuccess, onClose }: ScannerScreenP
-

Stall Identified!

+

Item Identified!

Redirecting to rating page...

diff --git a/packages/shared/index.ts b/packages/shared/index.ts index 567a1b2..91c7567 100644 --- a/packages/shared/index.ts +++ b/packages/shared/index.ts @@ -1,5 +1,5 @@ export interface VoteRequest { - stallId: number; + itemId: number; rating: number; }