1"use client";23import Link from "next/link";45export default function Home() {6 return (7 <main className="flex-1 flex flex-col items-center justify-center px-6 py-12">8 <div className="text-center mb-16 animate-fade-in">9 <div className="inline-flex items-center gap-3 mb-6">10 <div className="w-12 h-12 rounded-xl bg-merit-red flex items-center justify-center text-white font-bold text-2xl">11 M12 </div>13 <h1 className="text-5xl font-bold tracking-tight text-white">14 Meritrust15 </h1>16 </div>17 <p className="text-xl text-meritrust-400 max-w-lg mx-auto">18 AI-powered demos — built live during this presentation19 </p>20 </div>2122 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-6xl w-full">23 <Link24 href="/chat"25 className="group relative overflow-hidden rounded-2xl border border-meritrust-700 bg-meritrust-800/50 p-8 transition-all hover:border-merit-red/60 hover:bg-meritrust-800/80 hover:scale-[1.02]"26 >27 <div className="text-4xl mb-4">💬</div>28 <h2 className="text-2xl font-semibold text-white mb-2">29 AI Assistant30 </h2>31 <p className="text-meritrust-400 leading-relaxed">32 Chat with an AI that knows Meritrust — history, services, membership,33 and more.34 </p>35 <div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-merit-red/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />36 </Link>3738 <Link39 href="/trivia"40 className="group relative overflow-hidden rounded-2xl border border-meritrust-700 bg-meritrust-800/50 p-8 transition-all hover:border-merit-red/60 hover:bg-meritrust-800/80 hover:scale-[1.02]"41 >42 <div className="text-4xl mb-4">🏆</div>43 <h2 className="text-2xl font-semibold text-white mb-2">44 Live Trivia45 </h2>46 <p className="text-meritrust-400 leading-relaxed">47 Test your Meritrust knowledge! Scan the QR code on your phone and48 vote live.49 </p>50 <div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-merit-red/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />51 </Link>5253 <Link54 href="/buzz"55 className="group relative overflow-hidden rounded-2xl border border-meritrust-700 bg-meritrust-800/50 p-8 transition-all hover:border-merit-red/60 hover:bg-meritrust-800/80 hover:scale-[1.02]"56 >57 <div className="text-4xl mb-4">📡</div>58 <h2 className="text-2xl font-semibold text-white mb-2">59 Online Buzz60 </h2>61 <p className="text-meritrust-400 leading-relaxed">62 Find out what people are really saying about Meritrust across the63 internet.64 </p>65 <div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-merit-red/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />66 </Link>6768 <Link69 href="/source"70 className="group relative overflow-hidden rounded-2xl border border-meritrust-700 bg-meritrust-800/50 p-8 transition-all hover:border-merit-red/60 hover:bg-meritrust-800/80 hover:scale-[1.02]"71 >72 <div className="text-4xl mb-4"></></div>73 <h2 className="text-2xl font-semibold text-white mb-2">74 Source Code75 </h2>76 <p className="text-meritrust-400 leading-relaxed">77 See every line of code that Claude wrote to build this demo.78 </p>79 <div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-merit-red/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />80 </Link>81 </div>8283 <p className="mt-16 text-sm text-meritrust-600">84 Powered by Claude AI — built in real time during this presentation85 </p>86 </main>87 );88}1import type { Metadata } from "next";2import { Geist, Geist_Mono } from "next/font/google";3import "./globals.css";45const geistSans = Geist({6 variable: "--font-geist-sans",7 subsets: ["latin"],8});910const geistMono = Geist_Mono({11 variable: "--font-geist-mono",12 subsets: ["latin"],13});1415export const metadata: Metadata = {16 title: "Meritrust Credit Union — AI Demo",17 description: "AI-powered assistant and live trivia for Meritrust Credit Union",18};1920export default function RootLayout({21 children,22}: Readonly<{23 children: React.ReactNode;24}>) {25 return (26 <html27 lang="en"28 className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}29 >30 <body className="min-h-full flex flex-col">{children}</body>31 </html>32 );33}1@import "tailwindcss";23@theme {4 --color-meritrust-900: #0c0c0e;5 --color-meritrust-800: #18181b;6 --color-meritrust-700: #27272a;7 --color-meritrust-600: #52525b;8 --color-meritrust-500: #e41d38;9 --color-meritrust-400: #a1a1aa;10 --color-meritrust-300: #d4d4d8;11 --color-meritrust-200: #e4e4e7;12 --color-meritrust-100: #f4f4f5;13 --color-meritrust-50: #fafafa;14 --color-merit-red: #e41d38;15 --color-merit-red-hover: #f04058;16 --color-merit-red-glow: #e41d38;17}1819html {20 color-scheme: dark;21}2223body {24 background: var(--color-meritrust-900);25 color: white;26 font-family: var(--font-geist-sans), system-ui, -apple-system, sans-serif;27}2829@keyframes fade-in {30 from { opacity: 0; transform: translateY(8px); }31 to { opacity: 1; transform: translateY(0); }32}3334@keyframes pulse-glow {35 0%, 100% { box-shadow: 0 0 20px rgba(228, 29, 56, 0.3); }36 50% { box-shadow: 0 0 40px rgba(228, 29, 56, 0.6); }37}3839@keyframes count-up {40 from { transform: scale(1); }41 50% { transform: scale(1.15); }42 to { transform: scale(1); }43}4445.animate-fade-in {46 animation: fade-in 0.4s ease-out;47}4849.animate-glow {50 animation: pulse-glow 2s ease-in-out infinite;51}5253.animate-count {54 animation: count-up 0.3s ease-out;55}5657.vote-bar {58 transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);59}6061@keyframes float-up {62 0% { opacity: 1; transform: translateY(0) scale(1); }63 70% { opacity: 1; }64 100% { opacity: 0; transform: translateY(-400px) scale(1.4); }65}6667@keyframes burst {68 0% { opacity: 1; transform: scale(1) translateY(0); }69 100% { opacity: 0; transform: scale(2.5) translateY(-60px); }70}7172.floating-emoji {73 position: absolute;74 bottom: 80px;75 font-size: 2rem;76 animation: float-up 3s ease-out forwards;77 pointer-events: none;78 z-index: 50;79}8081.reaction-burst {82 position: fixed;83 bottom: 100px;84 left: 50%;85 font-size: 3rem;86 animation: burst 0.6s ease-out forwards;87 pointer-events: none;88 z-index: 100;89}1"use client";23import { useState, useRef, useEffect } from "react";4import { QRCodeSVG } from "qrcode.react";5import Link from "next/link";67interface Message {8 role: "user" | "assistant";9 content: string;10}1112export default function ChatPage() {13 const [messages, setMessages] = useState<Message[]>([]);14 const [input, setInput] = useState("");15 const [streaming, setStreaming] = useState(false);16 const [showQR, setShowQR] = useState(false);17 const scrollRef = useRef<HTMLDivElement>(null);18 const inputRef = useRef<HTMLInputElement>(null);1920 useEffect(() => {21 scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });22 }, [messages]);2324 async function handleSend() {25 const text = input.trim();26 if (!text || streaming) return;2728 const userMsg: Message = { role: "user", content: text };29 const newMessages = [...messages, userMsg];30 setMessages(newMessages);31 setInput("");32 setStreaming(true);3334 const assistantMsg: Message = { role: "assistant", content: "" };35 setMessages([...newMessages, assistantMsg]);3637 try {38 const res = await fetch("/api/chat", {39 method: "POST",40 headers: { "Content-Type": "application/json" },41 body: JSON.stringify({ messages: newMessages }),42 });4344 if (!res.ok || !res.body) throw new Error("Stream failed");4546 const reader = res.body.getReader();47 const decoder = new TextDecoder();48 let accumulated = "";49 let buffer = "";5051 while (true) {52 const { done, value } = await reader.read();53 if (done) break;5455 buffer += decoder.decode(value, { stream: true });56 const lines = buffer.split("\n");57 buffer = lines.pop() || "";5859 for (const line of lines) {60 if (line.startsWith("data: ") && line !== "data: [DONE]") {61 try {62 const parsed = JSON.parse(line.slice(6));63 accumulated += parsed.text;64 setMessages((prev) => {65 const updated = [...prev];66 updated[updated.length - 1] = { role: "assistant", content: accumulated };67 return updated;68 });69 } catch {}70 }71 }72 }73 } catch (err) {74 console.error(err);75 setMessages((prev) => {76 const updated = [...prev];77 updated[updated.length - 1] = {78 role: "assistant",79 content: "Sorry, something went wrong. Please try again.",80 };81 return updated;82 });83 } finally {84 setStreaming(false);85 inputRef.current?.focus();86 }87 }8889 const origin = typeof window !== "undefined" ? window.location.origin : "";9091 return (92 <div className="flex-1 flex flex-col h-screen">93 <header className="flex items-center justify-between px-6 py-4 border-b border-meritrust-700 bg-meritrust-800/60 backdrop-blur">94 <div className="flex items-center gap-3">95 <Link96 href="/"97 className="text-meritrust-400 hover:text-white transition-colors text-sm"98 >99 ← Back100 </Link>101 <div className="w-8 h-8 rounded-lg bg-merit-red flex items-center justify-center text-white font-bold text-sm">102 M103 </div>104 <div>105 <h1 className="text-lg font-semibold text-white">106 Meritrust AI Assistant107 </h1>108 <p className="text-xs text-meritrust-400">109 Ask anything about Meritrust Credit Union110 </p>111 </div>112 </div>113 <button114 onClick={() => setShowQR(!showQR)}115 className="px-3 py-1.5 rounded-lg bg-meritrust-700 text-meritrust-300 text-sm hover:bg-meritrust-600 transition-colors"116 >117 {showQR ? "Hide QR" : "Share QR"}118 </button>119 </header>120121 <div className="flex-1 flex overflow-hidden">122 <div ref={scrollRef} className="flex-1 overflow-y-auto px-6 py-6 space-y-4">123 {messages.length === 0 && (124 <div className="flex-1 flex flex-col items-center justify-center text-center py-20 animate-fade-in">125 <div className="w-16 h-16 rounded-2xl bg-meritrust-700 flex items-center justify-center text-3xl mb-6">126 💬127 </div>128 <h2 className="text-2xl font-semibold text-white mb-2">129 Ask me anything about Meritrust130 </h2>131 <p className="text-meritrust-400 max-w-md">132 I know about Meritrust's history, services, membership, and133 more. Try "When was Meritrust founded?" or "What134 services do you offer?"135 </p>136 <div className="flex flex-wrap gap-2 mt-6 justify-center">137 {[138 "When was Meritrust founded?",139 "What services do you offer?",140 "How do I become a member?",141 "Tell me about the merger",142 ].map((q) => (143 <button144 key={q}145 onClick={() => { setInput(q); inputRef.current?.focus(); }}146 className="px-3 py-1.5 rounded-full bg-meritrust-800 border border-meritrust-700 text-meritrust-300 text-sm hover:border-merit-red/50 transition-colors"147 >148 {q}149 </button>150 ))}151 </div>152 </div>153 )}154155 {messages.map((msg, i) => (156 <div157 key={i}158 className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"} animate-fade-in`}159 >160 <div161 className={`max-w-[75%] rounded-2xl px-4 py-3 ${162 msg.role === "user"163 ? "bg-merit-red text-white"164 : "bg-meritrust-800 border border-meritrust-700 text-meritrust-100"165 }`}166 >167 <p className="whitespace-pre-wrap leading-relaxed text-[15px]">168 {msg.content}169 {streaming && i === messages.length - 1 && msg.role === "assistant" && (170 <span className="inline-block w-2 h-4 bg-merit-red ml-1 animate-pulse" />171 )}172 </p>173 </div>174 </div>175 ))}176 </div>177178 {showQR && (179 <div className="w-64 border-l border-meritrust-700 bg-meritrust-800/40 flex flex-col items-center justify-center p-6 animate-fade-in">180 <p className="text-sm text-meritrust-300 mb-4 text-center">181 Scan to chat on your phone182 </p>183 <div className="bg-white p-3 rounded-xl">184 <QRCodeSVG value={`${origin}/chat`} size={180} />185 </div>186 <p className="text-xs text-meritrust-600 mt-3 text-center break-all">187 {origin}/chat188 </p>189 </div>190 )}191 </div>192193 <div className="px-6 py-4 border-t border-meritrust-700 bg-meritrust-800/60 backdrop-blur">194 <form195 onSubmit={(e) => { e.preventDefault(); handleSend(); }}196 className="flex gap-3 max-w-3xl mx-auto"197 >198 <input199 ref={inputRef}200 type="text"201 value={input}202 onChange={(e) => setInput(e.target.value)}203 placeholder="Ask about Meritrust..."204 className="flex-1 px-4 py-3 rounded-xl bg-meritrust-900 border border-meritrust-700 text-white placeholder:text-meritrust-600 focus:outline-none focus:border-merit-red transition-colors"205 disabled={streaming}206 autoFocus207 />208 <button209 type="submit"210 disabled={streaming || !input.trim()}211 className="px-6 py-3 rounded-xl bg-merit-red text-white font-medium hover:bg-merit-red-hover disabled:opacity-40 disabled:hover:bg-merit-red transition-colors"212 >213 {streaming ? "..." : "Send"}214 </button>215 </form>216 </div>217 </div>218 );219}1"use client";23import { useState, useRef, useEffect } from "react";4import { QRCodeSVG } from "qrcode.react";5import Link from "next/link";67interface Message {8 role: "user" | "assistant";9 content: string;10}1112export default function BuzzPage() {13 const [messages, setMessages] = useState<Message[]>([]);14 const [input, setInput] = useState("");15 const [streaming, setStreaming] = useState(false);16 const [showQR, setShowQR] = useState(false);17 const scrollRef = useRef<HTMLDivElement>(null);18 const inputRef = useRef<HTMLInputElement>(null);1920 useEffect(() => {21 scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });22 }, [messages]);2324 async function handleSend() {25 const text = input.trim();26 if (!text || streaming) return;2728 const userMsg: Message = { role: "user", content: text };29 const newMessages = [...messages, userMsg];30 setMessages(newMessages);31 setInput("");32 setStreaming(true);3334 const assistantMsg: Message = { role: "assistant", content: "" };35 setMessages([...newMessages, assistantMsg]);3637 try {38 const res = await fetch("/api/buzz", {39 method: "POST",40 headers: { "Content-Type": "application/json" },41 body: JSON.stringify({ messages: newMessages }),42 });4344 if (!res.ok || !res.body) throw new Error("Stream failed");4546 const reader = res.body.getReader();47 const decoder = new TextDecoder();48 let accumulated = "";49 let buffer = "";5051 while (true) {52 const { done, value } = await reader.read();53 if (done) break;5455 buffer += decoder.decode(value, { stream: true });56 const lines = buffer.split("\n");57 buffer = lines.pop() || "";5859 for (const line of lines) {60 if (line.startsWith("data: ") && line !== "data: [DONE]") {61 try {62 const parsed = JSON.parse(line.slice(6));63 accumulated += parsed.text;64 setMessages((prev) => {65 const updated = [...prev];66 updated[updated.length - 1] = { role: "assistant", content: accumulated };67 return updated;68 });69 } catch {}70 }71 }72 }73 } catch (err) {74 console.error(err);75 setMessages((prev) => {76 const updated = [...prev];77 updated[updated.length - 1] = {78 role: "assistant",79 content: "Sorry, something went wrong. Please try again.",80 };81 return updated;82 });83 } finally {84 setStreaming(false);85 inputRef.current?.focus();86 }87 }8889 const origin = typeof window !== "undefined" ? window.location.origin : "";9091 return (92 <div className="flex-1 flex flex-col h-screen">93 <header className="flex items-center justify-between px-6 py-4 border-b border-meritrust-700 bg-meritrust-800/60 backdrop-blur">94 <div className="flex items-center gap-3">95 <Link96 href="/"97 className="text-meritrust-400 hover:text-white transition-colors text-sm"98 >99 ← Back100 </Link>101 <div className="w-8 h-8 rounded-lg bg-merit-red flex items-center justify-center text-white font-bold text-sm">102 M103 </div>104 <div>105 <h1 className="text-lg font-semibold text-white">106 Online Buzz Bot107 </h1>108 <p className="text-xs text-meritrust-400">109 What are people saying about Meritrust online?110 </p>111 </div>112 </div>113 <button114 onClick={() => setShowQR(!showQR)}115 className="px-3 py-1.5 rounded-lg bg-meritrust-700 text-meritrust-300 text-sm hover:bg-meritrust-600 transition-colors"116 >117 {showQR ? "Hide QR" : "Share QR"}118 </button>119 </header>120121 <div className="flex-1 flex overflow-hidden">122 <div ref={scrollRef} className="flex-1 overflow-y-auto px-6 py-6 space-y-4">123 {messages.length === 0 && (124 <div className="flex-1 flex flex-col items-center justify-center text-center py-20 animate-fade-in">125 <div className="w-16 h-16 rounded-2xl bg-meritrust-700 flex items-center justify-center text-3xl mb-6">126 📡127 </div>128 <h2 className="text-2xl font-semibold text-white mb-2">129 The Online Buzz Bot130 </h2>131 <p className="text-meritrust-400 max-w-md">132 I've scoured the internet for what people are really saying133 about Meritrust. Ask me anything — the good, the bad, and134 the brutally honest.135 </p>136 <div className="flex flex-wrap gap-2 mt-6 justify-center">137 {[138 "What's the meanest thing someone said?",139 "What do employees love most?",140 "What's the #1 complaint?",141 "Give me the overall vibe",142 "Any long-time members upset?",143 "What do people say about auto loans?",144 ].map((q) => (145 <button146 key={q}147 onClick={() => { setInput(q); inputRef.current?.focus(); }}148 className="px-3 py-1.5 rounded-full bg-meritrust-800 border border-meritrust-700 text-meritrust-300 text-sm hover:border-merit-red/50 transition-colors"149 >150 {q}151 </button>152 ))}153 </div>154 </div>155 )}156157 {messages.map((msg, i) => (158 <div159 key={i}160 className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"} animate-fade-in`}161 >162 <div163 className={`max-w-[75%] rounded-2xl px-4 py-3 ${164 msg.role === "user"165 ? "bg-merit-red text-white"166 : "bg-meritrust-800 border border-meritrust-700 text-meritrust-100"167 }`}168 >169 <p className="whitespace-pre-wrap leading-relaxed text-[15px]">170 {msg.content}171 {streaming && i === messages.length - 1 && msg.role === "assistant" && (172 <span className="inline-block w-2 h-4 bg-merit-red ml-1 animate-pulse" />173 )}174 </p>175 </div>176 </div>177 ))}178 </div>179180 {showQR && (181 <div className="w-64 border-l border-meritrust-700 bg-meritrust-800/40 flex flex-col items-center justify-center p-6 animate-fade-in">182 <p className="text-sm text-meritrust-300 mb-4 text-center">183 Scan to try on your phone184 </p>185 <div className="bg-white p-3 rounded-xl">186 <QRCodeSVG value={`${origin}/buzz`} size={180} />187 </div>188 <p className="text-xs text-meritrust-600 mt-3 text-center break-all">189 {origin}/buzz190 </p>191 </div>192 )}193 </div>194195 <div className="px-6 py-4 border-t border-meritrust-700 bg-meritrust-800/60 backdrop-blur">196 <form197 onSubmit={(e) => { e.preventDefault(); handleSend(); }}198 className="flex gap-3 max-w-3xl mx-auto"199 >200 <input201 ref={inputRef}202 type="text"203 value={input}204 onChange={(e) => setInput(e.target.value)}205 placeholder="Ask what people are saying..."206 className="flex-1 px-4 py-3 rounded-xl bg-meritrust-900 border border-meritrust-700 text-white placeholder:text-meritrust-600 focus:outline-none focus:border-merit-red transition-colors"207 disabled={streaming}208 autoFocus209 />210 <button211 type="submit"212 disabled={streaming || !input.trim()}213 className="px-6 py-3 rounded-xl bg-merit-red text-white font-medium hover:bg-merit-red-hover disabled:opacity-40 disabled:hover:bg-merit-red transition-colors"214 >215 {streaming ? "..." : "Send"}216 </button>217 </form>218 </div>219 </div>220 );221}1"use client";23import { useState, useEffect, useCallback, useRef } from "react";4import { QRCodeSVG } from "qrcode.react";5import { questions, type GameState, createInitialState } from "@/lib/trivia";6import Link from "next/link";78interface FloatingEmoji {9 id: number;10 emoji: string;11 x: number;12 createdAt: number;13}1415let emojiIdCounter = 0;1617export default function TriviaPresenter() {18 const [state, setState] = useState<GameState>(createInitialState());19 const [floatingEmojis, setFloatingEmojis] = useState<FloatingEmoji[]>([]);20 const prevReactionsRef = useRef<Record<string, { up: number; down: number }>>({});2122 const fetchState = useCallback(async () => {23 try {24 const res = await fetch("/api/trivia/state");25 if (res.ok) {26 const data = (await res.json()) as GameState;27 setState(data);28 }29 } catch {}30 }, []);3132 useEffect(() => {33 fetchState();34 const interval = setInterval(fetchState, 1500);35 return () => clearInterval(interval);36 }, [fetchState]);3738 useEffect(() => {39 const qKey = String(state.currentQuestion);40 const current = state.reactions?.[qKey] || { up: 0, down: 0 };41 const prev = prevReactionsRef.current[qKey] || { up: 0, down: 0 };4243 const newUps = Math.max(0, current.up - prev.up);44 const newDowns = Math.max(0, current.down - prev.down);4546 if (newUps > 0 || newDowns > 0) {47 const newEmojis: FloatingEmoji[] = [];48 for (let i = 0; i < Math.min(newUps, 8); i++) {49 newEmojis.push({50 id: emojiIdCounter++,51 emoji: "👍",52 x: 70 + Math.random() * 25,53 createdAt: Date.now(),54 });55 }56 for (let i = 0; i < Math.min(newDowns, 8); i++) {57 newEmojis.push({58 id: emojiIdCounter++,59 emoji: "👎",60 x: 70 + Math.random() * 25,61 createdAt: Date.now(),62 });63 }64 setFloatingEmojis((prev) => [...prev, ...newEmojis]);65 }6667 prevReactionsRef.current = { ...prevReactionsRef.current, [qKey]: { ...current } };68 }, [state.reactions, state.currentQuestion]);6970 useEffect(() => {71 const interval = setInterval(() => {72 setFloatingEmojis((prev) => prev.filter((e) => Date.now() - e.createdAt < 3000));73 }, 500);74 return () => clearInterval(interval);75 }, []);7677 async function sendAction(action: string) {78 await fetch("/api/trivia/next", {79 method: "POST",80 headers: { "Content-Type": "application/json" },81 body: JSON.stringify({ action }),82 });83 await fetchState();84 }8586 async function reset() {87 await fetch("/api/trivia/reset", { method: "POST" });88 prevReactionsRef.current = {};89 await fetchState();90 }9192 const origin = typeof window !== "undefined" ? window.location.origin : "";93 const q = questions[state.currentQuestion];94 const votes = state.votes[String(state.currentQuestion)] || { A: 0, B: 0, C: 0, D: 0 };95 const totalVotes = Object.values(votes).reduce((a, b) => a + b, 0);96 const currentReactions = state.reactions?.[String(state.currentQuestion)] || { up: 0, down: 0 };9798 const answerColors: Record<string, string> = {99 A: "bg-blue-500",100 B: "bg-emerald-500",101 C: "bg-amber-500",102 D: "bg-rose-500",103 };104105 const answerBorders: Record<string, string> = {106 A: "border-blue-500",107 B: "border-emerald-500",108 C: "border-amber-500",109 D: "border-rose-500",110 };111112 return (113 <div className="flex-1 flex flex-col h-screen relative overflow-hidden">114 <header className="flex items-center justify-between px-6 py-3 border-b border-meritrust-700 bg-meritrust-800/60">115 <div className="flex items-center gap-3">116 <Link href="/" className="text-meritrust-400 hover:text-white transition-colors text-sm">117 ← Back118 </Link>119 <div className="w-7 h-7 rounded-lg bg-merit-red flex items-center justify-center text-white font-bold text-xs">120 M121 </div>122 <h1 className="text-lg font-semibold text-white">Meritrust Trivia</h1>123 </div>124 <div className="flex items-center gap-3">125 <span className="text-sm text-meritrust-400">126 {state.players.length} player{state.players.length !== 1 ? "s" : ""}127 </span>128 <button129 onClick={reset}130 className="px-3 py-1.5 rounded-lg bg-meritrust-700 text-meritrust-300 text-sm hover:bg-meritrust-600 transition-colors"131 >132 Reset133 </button>134 </div>135 </header>136137 <div className="flex-1 flex items-center justify-center p-8">138 {state.phase === "lobby" && (139 <div className="text-center animate-fade-in">140 <h2 className="text-4xl font-bold text-white mb-2">141 Meritrust Trivia142 </h2>143 <p className="text-xl text-meritrust-400 mb-10">144 Scan the QR code to join on your phone145 </p>146 <div className="inline-block bg-white p-5 rounded-2xl animate-glow mb-6">147 <QRCodeSVG value={`${origin}/trivia/play`} size={240} />148 </div>149 <p className="text-meritrust-400 text-lg mb-10 font-mono">150 {origin}/trivia/play151 </p>152 <div className="text-2xl text-meritrust-300 mb-8">153 <span className="text-4xl font-bold text-white">{state.players.length}</span>{" "}154 player{state.players.length !== 1 ? "s" : ""} joined155 </div>156 <button157 onClick={() => sendAction("start")}158 className="px-10 py-4 rounded-2xl bg-merit-red text-white text-xl font-semibold hover:bg-merit-red-hover transition-colors"159 >160 Start Game161 </button>162 </div>163 )}164165 {state.phase === "question" && q && (166 <div className="w-full max-w-4xl animate-fade-in">167 <div className="text-center mb-8">168 <span className="text-sm text-meritrust-400 uppercase tracking-wider">169 Question {state.currentQuestion + 1} of {questions.length}170 </span>171 <h2 className="text-3xl font-bold text-white mt-2 leading-tight">172 {q.question}173 </h2>174 </div>175176 <div className="grid grid-cols-1 gap-4 mb-8">177 {q.answers.map((a) => (178 <div179 key={a.key}180 className={`relative overflow-hidden rounded-xl border-2 transition-all ${answerBorders[a.key]} bg-meritrust-800/50`}181 >182 <div className="relative flex items-center px-6 py-5">183 <div className="flex items-center gap-4">184 <span185 className={`w-10 h-10 rounded-lg ${answerColors[a.key]} flex items-center justify-center text-white font-bold text-lg`}186 >187 {a.key}188 </span>189 <span className="text-xl text-white font-medium">190 {a.text}191 </span>192 </div>193 </div>194 </div>195 ))}196 </div>197198 <div className="flex items-center justify-between">199 <div className="flex items-center gap-4">200 <span className="text-meritrust-400">201 <span className="text-3xl font-bold text-white animate-count">{totalVotes}</span>{" "}202 vote{totalVotes !== 1 ? "s" : ""} submitted203 </span>204 {(currentReactions.up > 0 || currentReactions.down > 0) && (205 <span className="text-meritrust-500 text-sm">206 👍 {currentReactions.up} · 👎 {currentReactions.down}207 </span>208 )}209 </div>210 <button211 onClick={() => sendAction("reveal")}212 className="px-8 py-3 rounded-xl bg-amber-500 text-white font-semibold hover:bg-amber-400 transition-colors"213 >214 Reveal Answer215 </button>216 </div>217 </div>218 )}219220 {state.phase === "revealed" && q && (221 <div className="w-full max-w-4xl animate-fade-in">222 <div className="text-center mb-8">223 <span className="text-sm text-meritrust-400 uppercase tracking-wider">224 Question {state.currentQuestion + 1} of {questions.length}225 </span>226 <h2 className="text-3xl font-bold text-white mt-2 leading-tight">227 {q.question}228 </h2>229 </div>230231 <div className="grid grid-cols-1 gap-4 mb-8">232 {q.answers.map((a) => {233 const count = votes[a.key] || 0;234 const pct = totalVotes > 0 ? Math.round((count / totalVotes) * 100) : 0;235 const isCorrect = a.key === q.correct;236 const isWrong = a.key !== q.correct;237238 return (239 <div240 key={a.key}241 className={`relative overflow-hidden rounded-xl border-2 transition-all ${242 isCorrect243 ? "border-emerald-400 bg-emerald-500/20"244 : isWrong245 ? "border-meritrust-700/50 bg-meritrust-800/30 opacity-50"246 : `${answerBorders[a.key]} bg-meritrust-800/50`247 }`}248 >249 <div250 className={`vote-bar absolute inset-y-0 left-0 ${answerColors[a.key]} opacity-20`}251 style={{ width: `${pct}%` }}252 />253 <div className="relative flex items-center justify-between px-6 py-5">254 <div className="flex items-center gap-4">255 <span256 className={`w-10 h-10 rounded-lg ${answerColors[a.key]} flex items-center justify-center text-white font-bold text-lg`}257 >258 {a.key}259 </span>260 <span className="text-xl text-white font-medium">261 {a.text}262 </span>263 {isCorrect && (264 <span className="text-2xl ml-2">✓</span>265 )}266 </div>267 <div className="text-right">268 <span className="text-2xl font-bold text-white">269 {count}270 </span>271 <span className="text-meritrust-400 ml-2">272 {pct}%273 </span>274 </div>275 </div>276 </div>277 );278 })}279 </div>280281 <div className="flex items-center justify-between">282 <div className="flex items-center gap-4">283 <span className="text-meritrust-400">284 {totalVotes} vote{totalVotes !== 1 ? "s" : ""}285 </span>286 {(currentReactions.up > 0 || currentReactions.down > 0) && (287 <span className="text-meritrust-500 text-sm">288 👍 {currentReactions.up} · 👎 {currentReactions.down}289 </span>290 )}291 </div>292 <button293 onClick={() => sendAction("next")}294 className="px-8 py-3 rounded-xl bg-merit-red text-white font-semibold hover:bg-merit-red-hover transition-colors"295 >296 {state.currentQuestion < questions.length - 1297 ? "Next Question →"298 : "See Results →"}299 </button>300 </div>301 </div>302 )}303304 {state.phase === "finished" && (305 <div className="text-center animate-fade-in">306 <div className="text-6xl mb-6">🏆</div>307 <h2 className="text-4xl font-bold text-white mb-4">308 Great game!309 </h2>310 <p className="text-xl text-meritrust-300 mb-8">311 {state.players.length} player{state.players.length !== 1 ? "s" : ""} participated312 </p>313 <div className="space-y-4 max-w-2xl mx-auto mb-10">314 {questions.map((qq, i) => {315 const v = state.votes[String(i)] || { A: 0, B: 0, C: 0, D: 0 };316 const total = Object.values(v).reduce((a, b) => a + b, 0);317 const correctCount = v[qq.correct] || 0;318 const correctPct = total > 0 ? Math.round((correctCount / total) * 100) : 0;319 const r = state.reactions?.[String(i)] || { up: 0, down: 0 };320321 return (322 <div323 key={i}324 className="rounded-xl bg-meritrust-800/50 border border-meritrust-700 p-4 text-left"325 >326 <p className="text-meritrust-400 text-sm mb-1">327 Q{i + 1}: {qq.question}328 </p>329 <div className="flex items-center justify-between">330 <p className="text-white font-medium">331 {qq.answers.find((a) => a.key === qq.correct)?.text}{" "}332 <span className="text-emerald-400">333 — {correctPct}% got it right334 </span>335 </p>336 {(r.up > 0 || r.down > 0) && (337 <span className="text-meritrust-500 text-sm">338 👍 {r.up} · 👎 {r.down}339 </span>340 )}341 </div>342 </div>343 );344 })}345 </div>346 <button347 onClick={reset}348 className="px-8 py-3 rounded-xl bg-merit-red text-white font-semibold hover:bg-merit-red-hover transition-colors"349 >350 Play Again351 </button>352 </div>353 )}354 </div>355356 {/* Floating reaction emojis */}357 {floatingEmojis.map((e) => (358 <div359 key={e.id}360 className="floating-emoji"361 style={{ left: `${e.x}%` }}362 >363 {e.emoji}364 </div>365 ))}366 </div>367 );368}1"use client";23import { useState, useEffect, useCallback } from "react";4import { questions, type GameState, createInitialState } from "@/lib/trivia";56function getVoterId(): string {7 if (typeof window === "undefined") return "";8 let id = localStorage.getItem("meritrust_voter_id");9 if (!id) {10 id = Math.random().toString(36).slice(2) + Date.now().toString(36);11 localStorage.setItem("meritrust_voter_id", id);12 }13 return id;14}1516export default function TriviaPlayer() {17 const [state, setState] = useState<GameState>(createInitialState());18 const [voterId, setVoterId] = useState("");19 const [voted, setVoted] = useState<Record<number, string>>({});20 const [joined, setJoined] = useState(false);21 const [submitting, setSubmitting] = useState(false);22 const [reactionBursts, setReactionBursts] = useState<{ id: number; type: string }[]>([]);2324 useEffect(() => {25 setVoterId(getVoterId());26 }, []);2728 const fetchState = useCallback(async () => {29 try {30 const res = await fetch("/api/trivia/state");31 if (res.ok) setState((await res.json()) as GameState);32 } catch {}33 }, []);3435 useEffect(() => {36 fetchState();37 const interval = setInterval(fetchState, 1500);38 return () => clearInterval(interval);39 }, [fetchState]);4041 async function joinGame() {42 if (!voterId) return;43 await fetch("/api/trivia/vote", {44 method: "POST",45 headers: { "Content-Type": "application/json" },46 body: JSON.stringify({ answer: null, voterId, join: true }),47 });48 setJoined(true);49 }5051 async function vote(answer: string) {52 if (submitting || voted[state.currentQuestion]) return;53 setSubmitting(true);5455 try {56 const res = await fetch("/api/trivia/vote", {57 method: "POST",58 headers: { "Content-Type": "application/json" },59 body: JSON.stringify({ answer, voterId }),60 });6162 if (res.ok || res.status === 409) {63 setVoted((prev) => ({ ...prev, [state.currentQuestion]: answer }));64 }65 } catch {} finally {66 setSubmitting(false);67 }68 }6970 let burstId = 0;71 async function sendReaction(type: "up" | "down") {72 const id = burstId++;73 const emoji = type === "up" ? "👍" : "👎";74 setReactionBursts((prev) => [...prev.slice(-20), { id, type: emoji }]);75 setTimeout(() => {76 setReactionBursts((prev) => prev.filter((b) => b.id !== id));77 }, 600);7879 fetch("/api/trivia/reactions", {80 method: "POST",81 headers: { "Content-Type": "application/json" },82 body: JSON.stringify({ type }),83 }).catch(() => {});84 }8586 const q = questions[state.currentQuestion];87 const myVote = voted[state.currentQuestion];8889 const answerColors: Record<string, { bg: string; active: string }> = {90 A: { bg: "bg-blue-500/20 border-blue-500/50", active: "bg-blue-500" },91 B: { bg: "bg-emerald-500/20 border-emerald-500/50", active: "bg-emerald-500" },92 C: { bg: "bg-amber-500/20 border-amber-500/50", active: "bg-amber-500" },93 D: { bg: "bg-rose-500/20 border-rose-500/50", active: "bg-rose-500" },94 };9596 const showReactions = state.phase === "question" || state.phase === "revealed";9798 return (99 <div className="min-h-screen flex flex-col px-4 py-6 max-w-lg mx-auto relative overflow-hidden">100 <div className="text-center mb-6">101 <div className="inline-flex items-center gap-2 mb-1">102 <div className="w-7 h-7 rounded-lg bg-merit-red flex items-center justify-center text-white font-bold text-xs">103 M104 </div>105 <h1 className="text-lg font-semibold text-white">Meritrust Trivia</h1>106 </div>107 </div>108109 {state.phase === "lobby" && (110 <div className="flex-1 flex flex-col items-center justify-center text-center animate-fade-in">111 {!joined ? (112 <>113 <div className="text-5xl mb-6">🏆</div>114 <h2 className="text-2xl font-bold text-white mb-2">Ready to play?</h2>115 <p className="text-meritrust-400 mb-8">116 Test your Meritrust knowledge!117 </p>118 <button119 onClick={joinGame}120 className="px-8 py-4 rounded-2xl bg-merit-red text-white text-lg font-semibold hover:bg-merit-red-hover transition-colors w-full"121 >122 Join Game123 </button>124 </>125 ) : (126 <>127 <div className="text-5xl mb-6">✅</div>128 <h2 className="text-2xl font-bold text-white mb-2">You're in!</h2>129 <p className="text-meritrust-400">130 Waiting for the host to start...131 </p>132 <div className="mt-6 w-8 h-8 border-3 border-merit-red border-t-transparent rounded-full animate-spin" />133 </>134 )}135 </div>136 )}137138 {state.phase === "question" && q && (139 <div className="flex-1 flex flex-col animate-fade-in">140 <div className="text-center mb-6">141 <span className="text-xs text-meritrust-400 uppercase tracking-wider">142 Question {state.currentQuestion + 1} of {questions.length}143 </span>144 <h2 className="text-xl font-bold text-white mt-2 leading-snug">145 {q.question}146 </h2>147 </div>148149 {!myVote ? (150 <div className="space-y-3 flex-1">151 {q.answers.map((a) => (152 <button153 key={a.key}154 onClick={() => vote(a.key)}155 disabled={submitting}156 className={`w-full flex items-center gap-3 p-4 rounded-xl border-2 transition-all active:scale-[0.98] ${answerColors[a.key].bg} hover:opacity-90 disabled:opacity-50`}157 >158 <span159 className={`w-9 h-9 rounded-lg ${answerColors[a.key].active} flex items-center justify-center text-white font-bold`}160 >161 {a.key}162 </span>163 <span className="text-white font-medium text-left flex-1">164 {a.text}165 </span>166 </button>167 ))}168 </div>169 ) : (170 <div className="flex-1 flex flex-col items-center justify-center text-center">171 <div className="text-5xl mb-4">👍</div>172 <h3 className="text-xl font-semibold text-white mb-2">173 Vote recorded!174 </h3>175 <p className="text-meritrust-300">176 You picked{" "}177 <span className="font-semibold text-white">178 {q.answers.find((a) => a.key === myVote)?.text}179 </span>180 </p>181 <p className="text-meritrust-600 text-sm mt-4">182 Waiting for the host to reveal...183 </p>184 </div>185 )}186 </div>187 )}188189 {state.phase === "revealed" && q && (190 <div className="flex-1 flex flex-col items-center justify-center text-center animate-fade-in">191 {myVote === q.correct ? (192 <>193 <div className="text-6xl mb-4">🎉</div>194 <h3 className="text-2xl font-bold text-emerald-400 mb-2">195 Correct!196 </h3>197 </>198 ) : (199 <>200 <div className="text-6xl mb-4">😅</div>201 <h3 className="text-2xl font-bold text-rose-400 mb-2">202 Not quite!203 </h3>204 </>205 )}206 <p className="text-meritrust-300 text-lg">207 The answer was{" "}208 <span className="font-semibold text-white">209 {q.answers.find((a) => a.key === q.correct)?.text}210 </span>211 </p>212 {!myVote && (213 <p className="text-meritrust-600 text-sm mt-2">214 (You didn't vote on this one)215 </p>216 )}217 </div>218 )}219220 {state.phase === "finished" && (221 <div className="flex-1 flex flex-col items-center justify-center text-center animate-fade-in">222 <div className="text-6xl mb-4">🏆</div>223 <h2 className="text-2xl font-bold text-white mb-2">Game over!</h2>224 <p className="text-meritrust-300 mb-6">Thanks for playing!</p>225 <div className="space-y-3 w-full">226 {questions.map((qq, i) => {227 const myAnswer = voted[i];228 const correct = myAnswer === qq.correct;229 return (230 <div231 key={i}232 className={`rounded-xl p-3 border ${233 correct234 ? "bg-emerald-500/10 border-emerald-500/30"235 : "bg-rose-500/10 border-rose-500/30"236 }`}237 >238 <p className="text-sm text-meritrust-400">Q{i + 1}</p>239 <p className="text-white font-medium">240 {correct ? "✓" : "✗"}{" "}241 {qq.answers.find((a) => a.key === qq.correct)?.text}242 </p>243 </div>244 );245 })}246 </div>247 </div>248 )}249250 {/* Reaction buttons */}251 {showReactions && (252 <div className="flex gap-4 justify-center py-4 mt-2">253 <button254 onClick={() => sendReaction("up")}255 className="relative w-16 h-16 rounded-full bg-emerald-500/20 border-2 border-emerald-500/50 flex items-center justify-center text-2xl active:scale-90 transition-transform"256 >257 👍258 </button>259 <button260 onClick={() => sendReaction("down")}261 className="relative w-16 h-16 rounded-full bg-rose-500/20 border-2 border-rose-500/50 flex items-center justify-center text-2xl active:scale-90 transition-transform"262 >263 👎264 </button>265 </div>266 )}267268 {/* Burst feedback on tap */}269 {reactionBursts.map((b) => (270 <div key={b.id} className="reaction-burst">271 {b.type}272 </div>273 ))}274 </div>275 );276}1import Anthropic from "@anthropic-ai/sdk";2import { MERITRUST_SYSTEM_PROMPT } from "@/lib/meritrust-context";3import { getCloudflareContext } from "@opennextjs/cloudflare";45export const dynamic = "force-dynamic";67export async function POST(request: Request) {8 try {9 const { env } = await getCloudflareContext();10 const apiKey = (env as Record<string, string>).ANTHROPIC_API_KEY;1112 if (!apiKey) {13 return new Response(14 JSON.stringify({ error: "API key not configured" }),15 { status: 500, headers: { "Content-Type": "application/json" } }16 );17 }1819 const body = (await request.json()) as { messages: { role: "user" | "assistant"; content: string }[] };20 const messages = body.messages;2122 const client = new Anthropic({ apiKey });2324 const stream = client.messages.stream({25 model: "claude-sonnet-4-6",26 max_tokens: 1024,27 system: MERITRUST_SYSTEM_PROMPT,28 messages,29 });3031 const encoder = new TextEncoder();32 const readable = new ReadableStream({33 async start(controller) {34 try {35 for await (const event of stream) {36 if (37 event.type === "content_block_delta" &&38 event.delta.type === "text_delta"39 ) {40 controller.enqueue(41 encoder.encode(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`)42 );43 }44 }45 controller.enqueue(encoder.encode("data: [DONE]\n\n"));46 controller.close();47 } catch (err) {48 console.error("Stream error:", err);49 controller.error(err);50 }51 },52 });5354 return new Response(readable, {55 headers: {56 "Content-Type": "text/event-stream",57 "Cache-Control": "no-cache, no-store",58 Connection: "keep-alive",59 },60 });61 } catch (err) {62 console.error("Chat API error:", err);63 return new Response(JSON.stringify({ error: "Internal error" }), {64 status: 500,65 headers: { "Content-Type": "application/json" },66 });67 }68}1import Anthropic from "@anthropic-ai/sdk";2import { BUZZ_SYSTEM_PROMPT } from "@/lib/buzz-context";3import { getCloudflareContext } from "@opennextjs/cloudflare";45export const dynamic = "force-dynamic";67export async function POST(request: Request) {8 try {9 const { env } = await getCloudflareContext();10 const apiKey = (env as Record<string, string>).ANTHROPIC_API_KEY;1112 if (!apiKey) {13 return new Response(14 JSON.stringify({ error: "API key not configured" }),15 { status: 500, headers: { "Content-Type": "application/json" } }16 );17 }1819 const body = (await request.json()) as { messages: { role: "user" | "assistant"; content: string }[] };20 const messages = body.messages;2122 const client = new Anthropic({ apiKey });2324 const stream = client.messages.stream({25 model: "claude-sonnet-4-6",26 max_tokens: 1024,27 system: BUZZ_SYSTEM_PROMPT,28 messages,29 });3031 const encoder = new TextEncoder();32 const readable = new ReadableStream({33 async start(controller) {34 try {35 for await (const event of stream) {36 if (37 event.type === "content_block_delta" &&38 event.delta.type === "text_delta"39 ) {40 controller.enqueue(41 encoder.encode(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`)42 );43 }44 }45 controller.enqueue(encoder.encode("data: [DONE]\n\n"));46 controller.close();47 } catch (err) {48 console.error("Stream error:", err);49 controller.error(err);50 }51 },52 });5354 return new Response(readable, {55 headers: {56 "Content-Type": "text/event-stream",57 "Cache-Control": "no-cache, no-store",58 Connection: "keep-alive",59 },60 });61 } catch (err) {62 console.error("Buzz API error:", err);63 return new Response(JSON.stringify({ error: "Internal error" }), {64 status: 500,65 headers: { "Content-Type": "application/json" },66 });67 }68}1import { getCloudflareContext } from "@opennextjs/cloudflare";2import { createInitialState, type GameState } from "@/lib/trivia";34export const dynamic = "force-dynamic";56export async function GET() {7 try {8 const { env } = await getCloudflareContext();9 const kv = (env as Record<string, KVNamespace>).TRIVIA_KV;1011 if (!kv) {12 return Response.json({ error: "KV not configured" }, { status: 500 });13 }1415 const raw = await kv.get("state", { type: "text" });16 const state: GameState = raw ? JSON.parse(raw) : createInitialState();1718 return Response.json(state, {19 headers: { "Cache-Control": "no-cache, no-store" },20 });21 } catch (err) {22 console.error("Trivia state error:", err);23 return Response.json({ error: "Internal error" }, { status: 500 });24 }25}1import { getCloudflareContext } from "@opennextjs/cloudflare";2import { createInitialState, type GameState } from "@/lib/trivia";34export const dynamic = "force-dynamic";56export async function POST(request: Request) {7 try {8 const { env } = await getCloudflareContext();9 const kv = (env as Record<string, KVNamespace>).TRIVIA_KV;1011 if (!kv) {12 return Response.json({ error: "KV not configured" }, { status: 500 });13 }1415 const { answer, voterId, join } = (await request.json()) as {16 answer: string | null;17 voterId: string;18 join?: boolean;19 };2021 if (!voterId) {22 return Response.json({ error: "Missing voterId" }, { status: 400 });23 }2425 const raw = await kv.get("state", { type: "text" });26 const state: GameState = raw ? JSON.parse(raw) : createInitialState();2728 if (!state.players.includes(voterId)) {29 state.players.push(voterId);30 }3132 if (join || !answer) {33 await kv.put("state", JSON.stringify(state));34 return Response.json({ ok: true }, {35 headers: { "Cache-Control": "no-cache, no-store" },36 });37 }3839 if (state.phase !== "question") {40 return Response.json({ error: "Not accepting votes" }, { status: 400 });41 }4243 const qId = String(state.currentQuestion);4445 if (state.voters[qId]?.includes(voterId)) {46 return Response.json({ error: "Already voted" }, { status: 409 });47 }4849 if (!state.votes[qId]) {50 state.votes[qId] = { A: 0, B: 0, C: 0, D: 0 };51 }52 if (!state.voters[qId]) {53 state.voters[qId] = [];54 }5556 state.votes[qId][answer] = (state.votes[qId][answer] || 0) + 1;57 state.voters[qId].push(voterId);5859 await kv.put("state", JSON.stringify(state));6061 return Response.json({ ok: true }, {62 headers: { "Cache-Control": "no-cache, no-store" },63 });64 } catch (err) {65 console.error("Vote error:", err);66 return Response.json({ error: "Internal error" }, { status: 500 });67 }68}1import { getCloudflareContext } from "@opennextjs/cloudflare";2import { createInitialState, questions, type GameState } from "@/lib/trivia";34export const dynamic = "force-dynamic";56export async function POST(request: Request) {7 try {8 const { env } = await getCloudflareContext();9 const kv = (env as Record<string, KVNamespace>).TRIVIA_KV;1011 if (!kv) {12 return Response.json({ error: "KV not configured" }, { status: 500 });13 }1415 const { action } = (await request.json()) as { action: string };1617 const raw = await kv.get("state", { type: "text" });18 const state: GameState = raw ? JSON.parse(raw) : createInitialState();1920 if (action === "start") {21 state.phase = "question";22 state.currentQuestion = 0;23 } else if (action === "reveal") {24 state.phase = "revealed";25 } else if (action === "next") {26 const nextQ = state.currentQuestion + 1;27 if (nextQ >= questions.length) {28 state.phase = "finished";29 } else {30 state.currentQuestion = nextQ;31 state.phase = "question";32 }33 }3435 await kv.put("state", JSON.stringify(state));3637 return Response.json(state, {38 headers: { "Cache-Control": "no-cache, no-store" },39 });40 } catch (err) {41 console.error("Next error:", err);42 return Response.json({ error: "Internal error" }, { status: 500 });43 }44}1import { getCloudflareContext } from "@opennextjs/cloudflare";2import { createInitialState } from "@/lib/trivia";34export const dynamic = "force-dynamic";56export async function POST() {7 try {8 const { env } = await getCloudflareContext();9 const kv = (env as Record<string, KVNamespace>).TRIVIA_KV;1011 if (!kv) {12 return Response.json({ error: "KV not configured" }, { status: 500 });13 }1415 const state = createInitialState();16 await kv.put("state", JSON.stringify(state));1718 return Response.json(state, {19 headers: { "Cache-Control": "no-cache, no-store" },20 });21 } catch (err) {22 console.error("Reset error:", err);23 return Response.json({ error: "Internal error" }, { status: 500 });24 }25}1import { getCloudflareContext } from "@opennextjs/cloudflare";2import type { GameState } from "@/lib/trivia";34export const dynamic = "force-dynamic";56export async function POST(request: Request) {7 try {8 const { env } = await getCloudflareContext();9 const kv = (env as Record<string, KVNamespace>).TRIVIA_KV;1011 if (!kv) {12 return Response.json({ error: "KV not configured" }, { status: 500 });13 }1415 const { type } = (await request.json()) as { type: "up" | "down" };16 if (type !== "up" && type !== "down") {17 return Response.json({ error: "Invalid type" }, { status: 400 });18 }1920 const raw = await kv.get("state");21 if (!raw) {22 return Response.json({ error: "No game state" }, { status: 404 });23 }2425 const state = JSON.parse(raw) as GameState;2627 if (state.phase !== "question" && state.phase !== "revealed") {28 return Response.json({ error: "Not in active phase" }, { status: 400 });29 }3031 const qKey = String(state.currentQuestion);32 if (!state.reactions[qKey]) {33 state.reactions[qKey] = { up: 0, down: 0 };34 }35 state.reactions[qKey][type]++;3637 await kv.put("state", JSON.stringify(state));3839 return Response.json({ ok: true }, {40 headers: { "Cache-Control": "no-cache, no-store" },41 });42 } catch (err) {43 console.error("Reactions error:", err);44 return Response.json({ error: "Internal error" }, { status: 500 });45 }46}1export interface TriviaQuestion {2 id: number;3 question: string;4 answers: { key: string; text: string }[];5 correct: string;6}78export const questions: TriviaQuestion[] = [9 {10 id: 0,11 question:12 "How did Meritrust announce its 2009 rebrand from \"Boeing Wichita Credit Union\"?",13 answers: [14 { key: "A", text: "Full-page ad in the Wichita Eagle" },15 { key: "B", text: "Bought 10 commercial slots during Super Bowl XLIII on local TV" },16 { key: "C", text: "Sponsored a Wichita State basketball halftime show" },17 { key: "D", text: "Billboard campaign across I-35" },18 ],19 correct: "B",20 },21 {22 id: 1,23 question:24 "The name \"Meritrust\" was selected from an initial list of how many candidate names?",25 answers: [26 { key: "A", text: "50" },27 { key: "B", text: "150" },28 { key: "C", text: "400" },29 { key: "D", text: "1,000" },30 ],31 correct: "C",32 },33 {34 id: 2,35 question:36 "Board Chairman Steve Dunn has served on the Meritrust board since what year?",37 answers: [38 { key: "A", text: "1978" },39 { key: "B", text: "1986" },40 { key: "C", text: "1994" },41 { key: "D", text: "2001" },42 ],43 correct: "B",44 },45 {46 id: 3,47 question:48 "In 1959, the founding deposits of Boulder Valley Schools Credit Union (a PMCU predecessor) were kept where?",49 answers: [50 { key: "A", text: "The principal's desk drawer at Boulder High" },51 { key: "B", text: "A safe deposit box at First National Bank" },52 { key: "C", text: "A box at Casey Junior High School" },53 { key: "D", text: "A locked filing cabinet at the district office" },54 ],55 correct: "C",56 },57];5859export interface GameState {60 phase: "lobby" | "question" | "revealed" | "finished";61 currentQuestion: number;62 votes: Record<string, Record<string, number>>;63 voters: Record<string, string[]>;64 players: string[];65 reactions: Record<string, { up: number; down: number }>;66}6768export function createInitialState(): GameState {69 return {70 phase: "lobby",71 currentQuestion: 0,72 votes: {73 "0": { A: 0, B: 0, C: 0, D: 0 },74 "1": { A: 0, B: 0, C: 0, D: 0 },75 "2": { A: 0, B: 0, C: 0, D: 0 },76 "3": { A: 0, B: 0, C: 0, D: 0 },77 },78 voters: { "0": [], "1": [], "2": [], "3": [] },79 players: [],80 reactions: {81 "0": { up: 0, down: 0 },82 "1": { up: 0, down: 0 },83 "2": { up: 0, down: 0 },84 "3": { up: 0, down: 0 },85 },86 };87}1export const MERITRUST_SYSTEM_PROMPT = `You are a friendly, knowledgeable AI assistant for Meritrust Credit Union. You help members and prospective members learn about Meritrust's services, history, and benefits.23ABOUT MERITRUST:4- Founded in 1935 by seven employees of the Stearman Aircraft Company in Wichita, Kansas5- In 1941, Boeing Aircraft Company acquired Stearman, and the credit union became "Wichita Boeing Employees Credit Union"6- In 1994, renamed to "Boeing Wichita Credit Union" (BWCU)7- In 2009, rebranded as "Meritrust Credit Union" to reflect its broader community charter8- On August 1, 2025, merged with Premier Members Credit Union (founded 1959 by Boulder Valley Schools employees in Colorado)9- Now proudly serves over 200,000 members10- 33 branch locations across Kansas (Wichita, Manhattan, Lawrence) and Colorado11- Approximately 650 employees12- Nearly \$4 billion in assets13- Deep roots in the aviation industry — membership grew from Stearman to Boeing to Spirit AeroSystems employees, and eventually the broader Wichita community1415SERVICES:16- Checking and savings accounts17- Auto loans, personal loans, and home equity loans18- Mortgage lending19- Credit cards20- Online and mobile banking21- Investment services22- Insurance products23- Business banking24- Youth accounts and financial literacy programs2526CORE VALUES:27- Member-owned, not-for-profit cooperative28- Community-focused — investing in local communities across Kansas and Colorado29- Committed to financial education and empowerment30- "People helping people" credit union philosophy3132GUIDELINES:33- Be warm, professional, and helpful34- If asked about specific rates, fees, or account details you don't have, direct them to meritrust.org or their local branch35- You can discuss general credit union benefits (lower fees, better rates, member ownership) but don't make specific rate claims36- Keep responses concise — 2-3 paragraphs max37- If asked about something unrelated to Meritrust or financial services, politely redirect38- Never make up information you don't know — say "I'd recommend checking meritrust.org or calling a branch for the most current details"`;1export const BUZZ_SYSTEM_PROMPT = `You are the Meritrust "Online Buzz Bot" — an AI that has read real public reviews and comments about Meritrust Credit Union from across the internet. Your job is to answer fun, candid questions about what people are saying online.23You have access to the following real reviews scraped from public sources (WalletHub, Indeed, and other review sites). Use them to answer questions honestly and with personality.45=== MEMBER REVIEWS (WalletHub & public review sites) ===67[1 star] "This is a terrible credit union. The customer service, if you can contact one, is abysmal. They do things without notifying you first. I was out of town and they canceled my debit card. It took two weeks to get a new one."89[1 star] "Meritrust secretly switched my free checking account to a \$7/month paid account without telling me. I only found out when I noticed the charges on my statement months later."1011[1 star] "After 50 years of membership, first with Boeing Employees CU and then Meritrust, their new fee structure and unrealistic demands have led to our parting ways. Very disappointing after half a century."1213[1 star] "They put a hold on my account for no reason. I couldn't access my money for days. When I called, I was transferred 4 times and still didn't get an answer."1415[1 star] "Worst experience ever. They charged me overdraft fees on transactions that should have been declined. When I disputed it, they basically said too bad."1617[2 stars] "Used to be great when they were Boeing Employees CU. Ever since the name change and mergers, service has gone downhill. Too big for their own good now."1819[3 stars] "Average credit union. Rates are okay but nothing special. The app is decent but could use work. Staff at the Kellogg branch are friendly at least."2021[4 stars] "I've been a member for 15 years and generally happy. Their auto loan rates are competitive and the process was smooth. Only ding is the website feels outdated."2223[5 stars] "In times of need, when my family was going through a rough patch, PMCU (now Meritrust) stepped in to help us keep our vehicle. They worked with us on payments when other banks wouldn't even talk to us. Forever grateful."2425[5 stars] "Great credit union! I've had my accounts here since they were Boeing Employees. The merger with PMCU has actually been pretty smooth. More branches now which is nice."2627[4 stars] "Solid local credit union. Not going to blow you away but they're reliable. The Wichita branches are clean and well-staffed. Mobile deposit works well."2829[2 stars] "They need to update their technology. The mobile app crashes frequently and the online banking interface looks like it's from 2010. Come on, it's 2024."3031[1 star] "Tried to get a mortgage through them. The process took 3 months and they kept asking for the same documents over and over. Eventually went elsewhere."3233[5 stars] "Best auto loan rates in Wichita! Got 4.5% when everyone else was quoting 6%+. The loan officer at the Webb Road branch was super helpful."3435[3 stars] "It's fine. Nothing to write home about. They're a credit union, they do credit union things. At least they're not a big bank."3637=== EMPLOYEE REVIEWS (Indeed & Glassdoor) ===3839[5 stars] "One of the best places I've ever worked. Management actually cares about employees. Small enough to feel like a family but big enough to have good benefits."4041[5 stars] "They pay you too good to be stressed. Great work-life balance, good PTO, and the people are genuinely nice. Would recommend."4243[4 stars] "Good benefits and decent pay for the Wichita area. Culture is positive overall. Only downside is limited room for advancement since it's not a huge organization."4445[1 star] "Management plays favorites. If you're not in the inner circle, don't expect to move up. Pay is below market for IT positions."4647[3 stars] "Okay place to work. Benefits are average. Management can be hit or miss depending on which branch you're at. Some managers are great, others are terrible."4849[2 stars] "Started great but went downhill after the merger. Too much corporate red tape now. Lost the small credit union feel that made it special."5051[5 stars] "Fantastic culture! They do fun team events, celebrate birthdays, and actually recognize hard work. The CEO knows employees by name."5253[4 stars] "Good entry-level financial job. They train you well and the teller position is a great starting point. Just don't expect to get rich."5455[1 star] "Turnover is insane in the call center. They hire in batches and people leave just as fast. The workload is unreasonable."5657[3 stars] "It's a job. Benefits keep you there. Pay could be better but it's stable. If you want excitement, look elsewhere. If you want steady, this is it."5859[5 stars] "Been here 12 years and I'm still happy. They genuinely care about the community and it shows in how they treat both members and employees."6061[4 stars] "Love the community involvement. They sponsor local events and encourage volunteering on company time. Makes you feel good about where you work."6263[2 stars] "The merger with PMCU created a lot of confusion and redundancy. Some departments are still figuring out who does what. Growing pains I guess."6465[1 star] "Work-life balance is a joke in the lending department. Expected to hit unrealistic numbers while being understaffed. Burnout is real here."6667[5 stars] "Amazing holiday party and employee appreciation events. They go all out. Free lunch on Fridays at the main branch. Little things that make a big difference."6869[3 stars] "Middle of the road. Not the worst place to work, not the best. If you want a chill 9-5 in Wichita, you could do worse."7071[4 stars] "They invested heavily in DEI initiatives recently and it shows. The workplace feels more inclusive than it did 5 years ago."7273[2 stars] "Communication from upper management is terrible. We hear about changes from members before we hear from leadership. Embarrassing."7475[5 stars] "The financial wellness programs they offer employees are top-notch. Free financial planning, matching 401k, and employee loan discounts."7677[3 stars] "Decent gig. The branches close early on Fridays which is nice. Just wish the pay kept up with inflation better."7879=== END OF REVIEWS ===8081PERSONALITY & RULES:82- Be entertaining and candid. This is meant to be fun during a live presentation.83- When asked about negative reviews, be honest but frame it constructively — "here's what people are saying, here's the pattern."84- When asked about positive reviews, celebrate the wins genuinely.85- You can quote or paraphrase specific reviews when relevant.86- If asked "what's the meanest thing someone said" — give them the real answer, don't sugarcoat.87- If asked about patterns or themes, analyze across all reviews.88- You can give ratings breakdowns, sentiment summaries, word clouds, etc.89- Keep responses concise and punchy — this is a live demo.90- If asked something not covered by the reviews, say so honestly.91- Never make up fake reviews. Only reference what's above.92- Sign off responses with fun energy — this is supposed to be entertaining!`;1// This file — you're looking at the rendered version right now!2// It contains all 18 source files hardcoded as string literals3// so it works on Cloudflare Workers (no filesystem access).18 files · 1,858 lines of code
Generated by Claude AI in a single prompt