feat: implement knowledge testing system with leaderboard, quiz generation, and PocketBase integration
This commit is contained in:
@@ -5,6 +5,7 @@ import Tag from '../../components/ui/Tag';
|
||||
import Button from '../../components/ui/Button';
|
||||
import Input from '../../components/ui/Input';
|
||||
import { storage } from '../../lib/storage';
|
||||
import * as db from '../../lib/db';
|
||||
import UploadZone from '../../components/admin/UploadZone';
|
||||
import KnowledgeGraph from '../../components/admin/KnowledgeGraph';
|
||||
import ContentManager from '../../components/admin/ContentManager';
|
||||
@@ -20,8 +21,9 @@ const Admin = () => {
|
||||
const [useSimulation, setUseSimulation] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState(null);
|
||||
|
||||
const loadSources = () => {
|
||||
setSources(storage.get('admin:sources', []));
|
||||
const loadSources = async () => {
|
||||
const data = await db.getSources();
|
||||
setSources(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,11 +46,10 @@ const Admin = () => {
|
||||
setTimeout(() => setSaveStatus(null), 3000);
|
||||
};
|
||||
|
||||
const handleDeleteSource = (id) => {
|
||||
const handleDeleteSource = async (id) => {
|
||||
if (confirm('Are you sure you want to delete this source? This will not delete topics already extracted.')) {
|
||||
const updated = sources.filter(s => s.id !== id);
|
||||
storage.set('admin:sources', updated);
|
||||
setSources(updated);
|
||||
await db.deleteSource(id);
|
||||
await loadSources();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,10 +64,9 @@ const Admin = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row h-[calc(100vh-64px)] overflow-hidden">
|
||||
{/* Admin Sidebar */}
|
||||
<div className="w-full md:w-64 bg-paper border-r border-bg-warm flex-shrink-0 flex flex-row md:flex-col p-4 gap-2 overflow-x-auto md:overflow-y-auto">
|
||||
<h2 className="hidden md:block text-teal font-bold text-lg mb-4 px-2">Knowledge Mgmt</h2>
|
||||
|
||||
|
||||
{navItems.map(({ key, icon: Icon, label, bottom }) => (
|
||||
<button
|
||||
key={key}
|
||||
@@ -79,15 +79,13 @@ const Admin = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Admin Main Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 md:p-8 bg-bg pb-24 md:pb-8">
|
||||
|
||||
{/* ── Sources ─────────────────────────────── */}
|
||||
{activeTab === 'sources' && (
|
||||
<div className="animate-in fade-in duration-300 max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl text-teal mb-2">Source Material</h1>
|
||||
<p className="text-fg-muted mb-8">Upload files to feed the AI knowledge extraction pipeline.</p>
|
||||
|
||||
|
||||
<UploadZone onUploadComplete={loadSources} />
|
||||
|
||||
<h3 className="text-xl mb-4 mt-12">Recent Sources</h3>
|
||||
@@ -109,7 +107,7 @@ const Admin = () => {
|
||||
<div>
|
||||
<p className="font-medium">{source.name}</p>
|
||||
<p className="text-xs text-fg-muted flex items-center gap-1 mt-1">
|
||||
<Clock size={12} /> {new Date(source.date).toLocaleString()}
|
||||
<Clock size={12} /> {new Date(source.created).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,7 +127,6 @@ const Admin = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Content Manager ──────────────────────── */}
|
||||
{activeTab === 'content' && (
|
||||
<div className="animate-in fade-in duration-300 max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl text-teal mb-2">Learning Content</h1>
|
||||
@@ -138,7 +135,6 @@ const Admin = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Test Manager ─────────────────────────── */}
|
||||
{activeTab === 'tests' && (
|
||||
<div className="animate-in fade-in duration-300 max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl text-teal mb-2">Question Banks</h1>
|
||||
@@ -147,18 +143,16 @@ const Admin = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Knowledge Graph ──────────────────────── */}
|
||||
{activeTab === 'graph' && (
|
||||
<div className="animate-in fade-in duration-300 h-full flex flex-col">
|
||||
<h1 className="text-3xl text-teal mb-2">Knowledge Graph</h1>
|
||||
<p className="text-fg-muted mb-4">Visual map of all extracted Respellion knowledge.</p>
|
||||
<Card className="flex-1 p-0 border border-bg-warm overflow-hidden bg-paper min-h-[400px]">
|
||||
<KnowledgeGraph />
|
||||
<KnowledgeGraph />
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Team ────────────────────────────────── */}
|
||||
{activeTab === 'team' && (
|
||||
<div className="animate-in fade-in duration-300 max-w-4xl mx-auto">
|
||||
<h1 className="text-3xl text-teal mb-2">Team Management</h1>
|
||||
@@ -167,17 +161,16 @@ const Admin = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Settings ────────────────────────────── */}
|
||||
{activeTab === 'settings' && (
|
||||
<div className="animate-in fade-in duration-300 max-w-2xl">
|
||||
<h1 className="text-3xl text-teal mb-2">Settings</h1>
|
||||
<p className="text-fg-muted mb-8">Manage API keys and application configuration.</p>
|
||||
|
||||
|
||||
<Card className="border border-bg-warm">
|
||||
<form onSubmit={saveSettings} className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">AI Configuration</h3>
|
||||
<Input
|
||||
<Input
|
||||
label="Anthropic API Key"
|
||||
type="password"
|
||||
placeholder="sk-ant-..."
|
||||
@@ -185,7 +178,7 @@ const Admin = () => {
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<Input
|
||||
<Input
|
||||
label="Model ID"
|
||||
placeholder="claude-sonnet-4-20250514"
|
||||
value={model}
|
||||
@@ -205,9 +198,9 @@ const Admin = () => {
|
||||
<p className="text-sm text-fg-muted">Use simulated AI responses when the API is unavailable.</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={useSimulation}
|
||||
onChange={(e) => setUseSimulation(e.target.checked)}
|
||||
/>
|
||||
@@ -221,7 +214,7 @@ const Admin = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-4 pt-4">
|
||||
<Button type="submit">
|
||||
<Save size={18} className="mr-2" /> Save Settings
|
||||
|
||||
@@ -1,47 +1,62 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useApp } from '../store/AppContext';
|
||||
import Card from '../components/ui/Card';
|
||||
import Button from '../components/ui/Button';
|
||||
import Tag from '../components/ui/Tag';
|
||||
|
||||
import { storage } from '../lib/storage';
|
||||
import * as db from '../lib/db';
|
||||
import { getAssignedTopic } from '../lib/learningService';
|
||||
import { getTestResult } from '../lib/testService';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { state } = useApp();
|
||||
const { currentUser, weekNumber } = state;
|
||||
|
||||
const topic = getAssignedTopic(currentUser?.id, weekNumber);
|
||||
const learnDone = storage.get(`user:${currentUser?.id}:week:${weekNumber}:learn_done`, false);
|
||||
const testResult = getTestResult(currentUser?.id, weekNumber);
|
||||
|
||||
const allUsers = storage.get('users:registry', []);
|
||||
const nonAdminIds = allUsers.filter(u => u.role !== 'admin').map(u => u.id);
|
||||
|
||||
const leaderboard = storage.get('leaderboard:current', [])
|
||||
.filter(u => nonAdminIds.includes(u.userId))
|
||||
.sort((a, b) => b.points - a.points);
|
||||
|
||||
const top3 = leaderboard.slice(0, 3);
|
||||
const myRank = leaderboard.findIndex(u => u.userId === currentUser?.id) + 1;
|
||||
const myPoints = leaderboard.find(u => u.userId === currentUser?.id)?.points || 0;
|
||||
const [dashData, setDashData] = useState({
|
||||
topic: null,
|
||||
learnDone: false,
|
||||
testResult: null,
|
||||
top3: [],
|
||||
myRank: 0,
|
||||
myPoints: 0,
|
||||
activity: [],
|
||||
});
|
||||
|
||||
// Gather recent activity from past weeks
|
||||
const activity = [];
|
||||
for (let w = weekNumber; w >= Math.max(1, weekNumber - 3); w--) {
|
||||
const pastLearn = storage.get(`user:${currentUser?.id}:week:${w}:learn_done`, false);
|
||||
const pastTest = getTestResult(currentUser?.id, w);
|
||||
const pastTopic = getAssignedTopic(currentUser?.id, w);
|
||||
|
||||
if (pastTest) {
|
||||
activity.push({ type: 'test', week: w, topic: pastTopic?.label, score: pastTest.percentage, points: pastTest.pointsEarned });
|
||||
}
|
||||
if (pastLearn) {
|
||||
activity.push({ type: 'learn', week: w, topic: pastTopic?.label });
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!currentUser) return;
|
||||
|
||||
const load = async () => {
|
||||
const [topic, learnDone, testResult, allUsers, leaderboard] = await Promise.all([
|
||||
getAssignedTopic(currentUser.id, weekNumber),
|
||||
db.getLearnDone(currentUser.id, weekNumber),
|
||||
db.getQuizResult(currentUser.id, weekNumber),
|
||||
db.getTeamMembers(),
|
||||
db.getLeaderboard(),
|
||||
]);
|
||||
|
||||
const nonAdminIds = allUsers.filter(u => u.role !== 'admin').map(u => u.id);
|
||||
const filtered = leaderboard.filter(u => nonAdminIds.includes(u.user_id));
|
||||
const top3 = filtered.slice(0, 3);
|
||||
const myRank = filtered.findIndex(u => u.user_id === currentUser.id) + 1;
|
||||
const myPoints = filtered.find(u => u.user_id === currentUser.id)?.points || 0;
|
||||
|
||||
const activity = [];
|
||||
for (let w = weekNumber; w >= Math.max(1, weekNumber - 3); w--) {
|
||||
const [pastLearn, pastTest, pastTopic] = await Promise.all([
|
||||
db.getLearnDone(currentUser.id, w),
|
||||
db.getQuizResult(currentUser.id, w),
|
||||
getAssignedTopic(currentUser.id, w),
|
||||
]);
|
||||
if (pastTest) activity.push({ type: 'test', week: w, topic: pastTopic?.label, score: pastTest.percentage, points: pastTest.points_earned });
|
||||
if (pastLearn) activity.push({ type: 'learn', week: w, topic: pastTopic?.label });
|
||||
}
|
||||
|
||||
setDashData({ topic, learnDone, testResult, top3, myRank, myPoints, activity });
|
||||
};
|
||||
|
||||
load();
|
||||
}, [currentUser, weekNumber]);
|
||||
|
||||
const { topic, learnDone, testResult, top3, myRank, myPoints, activity } = dashData;
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-10 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
@@ -118,7 +133,7 @@ const Dashboard = () => {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h3 className="text-2xl mb-4">Mini Leaderboard</h3>
|
||||
<Card className="border border-bg-warm">
|
||||
@@ -127,10 +142,10 @@ const Dashboard = () => {
|
||||
<div className="text-center text-fg-muted py-4">No points yet</div>
|
||||
) : (
|
||||
top3.map((u, i) => (
|
||||
<div key={u.userId} className="flex items-center justify-between">
|
||||
<div key={u.user_id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`font-mono font-bold ${i === 0 ? 'text-yellow-500' : 'text-teal'}`}>{i + 1}.</span>
|
||||
<span className="font-medium">{u.name} {u.userId === currentUser?.id && '(You)'}</span>
|
||||
<span className="font-medium">{u.name} {u.user_id === currentUser?.id && '(You)'}</span>
|
||||
</div>
|
||||
<Tag variant="dark">{u.points} pts</Tag>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,12 @@ import { motion } from 'framer-motion';
|
||||
import Card from '../components/ui/Card';
|
||||
import Tag from '../components/ui/Tag';
|
||||
import { useApp } from '../store/AppContext';
|
||||
import { storage } from '../lib/storage';
|
||||
import * as db from '../lib/db';
|
||||
|
||||
const BADGE_RULES = [
|
||||
{ id: 'first_test', icon: Star, label: 'First Steps', condition: (u) => u.testsCompleted > 0 },
|
||||
{ id: 'first_test', icon: Star, label: 'First Steps', condition: (u) => u.tests_completed > 0 },
|
||||
{ id: 'perfectionist', icon: Award, label: 'Perfectionist', condition: (u) => u.perfectScores > 0 },
|
||||
{ id: 'veteran', icon: Medal, label: 'Veteran', condition: (u) => u.testsCompleted >= 5 },
|
||||
{ id: 'veteran', icon: Medal, label: 'Veteran', condition: (u) => u.tests_completed >= 5 },
|
||||
];
|
||||
|
||||
const Leaderboard = () => {
|
||||
@@ -17,40 +17,38 @@ const Leaderboard = () => {
|
||||
const [board, setBoard] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Re-evaluate badges before displaying
|
||||
let data = storage.get('leaderboard:current', []);
|
||||
const allUsers = storage.get('users:registry', []);
|
||||
|
||||
// Filter out admins from the leaderboard entirely
|
||||
const nonAdminIds = allUsers.filter(u => u.role !== 'admin').map(u => u.id);
|
||||
data = data.filter(entry => nonAdminIds.includes(entry.userId));
|
||||
const load = async () => {
|
||||
const [leaderboardData, allUsers] = await Promise.all([
|
||||
db.getLeaderboard(),
|
||||
db.getTeamMembers(),
|
||||
]);
|
||||
|
||||
const userMap = new Map(data.map(u => [u.userId, u]));
|
||||
const nonAdminIds = allUsers.filter(u => u.role !== 'admin').map(u => u.id);
|
||||
let data = leaderboardData.filter(entry => nonAdminIds.includes(entry.user_id));
|
||||
|
||||
allUsers.forEach(user => {
|
||||
if (!userMap.has(user.id) && user.role !== 'admin') {
|
||||
data.push({
|
||||
userId: user.id,
|
||||
name: user.name,
|
||||
points: 0,
|
||||
testsCompleted: 0,
|
||||
});
|
||||
// Ensure all non-admin users appear even if they have no points yet
|
||||
const inBoard = new Set(data.map(e => e.user_id));
|
||||
for (const user of allUsers) {
|
||||
if (!inBoard.has(user.id) && user.role !== 'admin') {
|
||||
data.push({ user_id: user.id, name: user.name, points: 0, tests_completed: 0, perfectScores: 0 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check for perfect scores by peeking at test results
|
||||
data = data.map(entry => {
|
||||
let perfectScores = 0;
|
||||
// Loop over all possible weeks
|
||||
for (let w = 1; w <= state.weekNumber; w++) {
|
||||
const result = storage.get(`quiz:result:${entry.userId}:week:${w}`);
|
||||
if (result && result.percentage === 100) perfectScores++;
|
||||
}
|
||||
return { ...entry, perfectScores };
|
||||
});
|
||||
// Compute perfect scores per user
|
||||
data = await Promise.all(data.map(async entry => {
|
||||
let perfectScores = 0;
|
||||
for (let w = 1; w <= state.weekNumber; w++) {
|
||||
const result = await db.getQuizResult(entry.user_id, w);
|
||||
if (result && result.percentage === 100) perfectScores++;
|
||||
}
|
||||
return { ...entry, perfectScores };
|
||||
}));
|
||||
|
||||
data.sort((a, b) => b.points - a.points);
|
||||
setBoard(data);
|
||||
data.sort((a, b) => b.points - a.points);
|
||||
setBoard(data);
|
||||
};
|
||||
|
||||
load();
|
||||
}, [state.weekNumber]);
|
||||
|
||||
return (
|
||||
@@ -61,10 +59,8 @@ const Leaderboard = () => {
|
||||
<p className="text-fg-muted">Compete, learn, and earn badges based on your weekly knowledge tests.</p>
|
||||
</div>
|
||||
|
||||
{/* Top 3 Podium */}
|
||||
{board.length >= 3 && (
|
||||
<div className="flex justify-center items-end gap-2 md:gap-6 mb-16 h-64 mt-12">
|
||||
{/* 2nd Place */}
|
||||
<motion.div initial={{ y: 50, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.2 }} className="w-1/3 max-w-[140px] flex flex-col items-center">
|
||||
<div className="relative mb-4">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-200 border-4 border-paper flex items-center justify-center text-slate-500 font-bold text-2xl shadow-lg z-10 relative">2</div>
|
||||
@@ -75,7 +71,6 @@ const Leaderboard = () => {
|
||||
<div className="w-full h-24 bg-gradient-to-t from-slate-200/50 to-transparent rounded-t-[var(--r-sm)] mt-4"></div>
|
||||
</motion.div>
|
||||
|
||||
{/* 1st Place */}
|
||||
<motion.div initial={{ y: 50, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="w-1/3 max-w-[160px] flex flex-col items-center z-10">
|
||||
<div className="relative mb-6">
|
||||
<div className="w-20 h-20 rounded-full bg-yellow-400 border-4 border-paper flex items-center justify-center text-yellow-800 font-bold text-3xl shadow-xl z-10 relative">1</div>
|
||||
@@ -87,7 +82,6 @@ const Leaderboard = () => {
|
||||
<div className="w-full h-32 bg-gradient-to-t from-yellow-400/20 to-transparent rounded-t-[var(--r-sm)] mt-4 border-t border-yellow-400/30"></div>
|
||||
</motion.div>
|
||||
|
||||
{/* 3rd Place */}
|
||||
<motion.div initial={{ y: 50, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.4 }} className="w-1/3 max-w-[140px] flex flex-col items-center">
|
||||
<div className="relative mb-4">
|
||||
<div className="w-16 h-16 rounded-full bg-amber-700 border-4 border-paper flex items-center justify-center text-white font-bold text-2xl shadow-lg z-10 relative">3</div>
|
||||
@@ -100,22 +94,20 @@ const Leaderboard = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Full List */}
|
||||
<Card className="border border-bg-warm p-0 overflow-hidden">
|
||||
<div className="divide-y divide-bg-warm">
|
||||
{board.map((user, index) => {
|
||||
const earnedBadges = BADGE_RULES.filter(r => r.condition(user));
|
||||
const isMe = user.userId === state.currentUser?.id;
|
||||
const isMe = user.user_id === state.currentUser?.id;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
key={user.userId}
|
||||
key={user.user_id}
|
||||
className={`p-4 flex flex-col sm:flex-row sm:items-center gap-4 transition-colors ${isMe ? 'bg-teal/5 border-l-4 border-teal' : 'hover:bg-bg-warm/30'}`}
|
||||
>
|
||||
{/* Rank & Name */}
|
||||
<div className="flex items-center gap-4 min-w-[200px]">
|
||||
<span className={`font-mono text-lg font-bold w-6 text-center ${
|
||||
index === 0 ? 'text-yellow-500' :
|
||||
@@ -130,12 +122,11 @@ const Leaderboard = () => {
|
||||
{isMe && <Tag variant="accent" className="text-[10px]">You</Tag>}
|
||||
</p>
|
||||
<p className="text-xs text-fg-muted flex items-center gap-1">
|
||||
<CheckSquare size={12} /> {user.testsCompleted || 0} tests completed
|
||||
<CheckSquare size={12} /> {user.tests_completed || 0} tests completed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Points */}
|
||||
<div className="flex-1 flex sm:justify-center">
|
||||
<div className="flex items-center gap-1.5 text-teal font-bold bg-teal/10 px-3 py-1 rounded-full text-sm">
|
||||
<TrendingUp size={14} />
|
||||
@@ -143,7 +134,6 @@ const Leaderboard = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badges */}
|
||||
<div className="flex gap-2 sm:justify-end min-w-[120px]">
|
||||
{earnedBadges.length > 0 ? (
|
||||
earnedBadges.map(b => (
|
||||
|
||||
@@ -9,7 +9,7 @@ import Input from '../components/ui/Input';
|
||||
import LearningContentViewer from '../components/ui/LearningContentViewer';
|
||||
import { useApp } from '../store/AppContext';
|
||||
import { getAssignedTopic, generateLearningContent, getCachedContent, generateCustomTopic } from '../lib/learningService';
|
||||
import { storage } from '../lib/storage';
|
||||
import * as db from '../lib/db';
|
||||
|
||||
const Leren = () => {
|
||||
const { state } = useApp();
|
||||
@@ -39,12 +39,17 @@ const Leren = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (state.currentUser) {
|
||||
const assigned = getAssignedTopic(state.currentUser.id, state.weekNumber);
|
||||
setAssignedTopic(assigned);
|
||||
setAllTopics(storage.get('kb:topics', []));
|
||||
|
||||
const done = storage.get(`user:${state.currentUser.id}:week:${state.weekNumber}:learn_done`, false);
|
||||
if (done) setWeeklyDone(true);
|
||||
const load = async () => {
|
||||
const [assigned, topics, done] = await Promise.all([
|
||||
getAssignedTopic(state.currentUser.id, state.weekNumber),
|
||||
db.getTopics(),
|
||||
db.getLearnDone(state.currentUser.id, state.weekNumber),
|
||||
]);
|
||||
setAssignedTopic(assigned);
|
||||
setAllTopics(topics);
|
||||
if (done) setWeeklyDone(true);
|
||||
};
|
||||
load();
|
||||
}
|
||||
}, [state.currentUser, state.weekNumber]);
|
||||
|
||||
@@ -97,11 +102,11 @@ const Leren = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const doComplete = () => {
|
||||
const doComplete = async () => {
|
||||
setSessionDone(true);
|
||||
if (!weeklyDone) {
|
||||
setWeeklyDone(true);
|
||||
storage.set(`user:${state.currentUser.id}:week:${state.weekNumber}:learn_done`, true);
|
||||
await db.setLearnDone(state.currentUser.id, state.weekNumber);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -114,14 +119,12 @@ const Leren = () => {
|
||||
doComplete();
|
||||
};
|
||||
|
||||
const handleSubmitFeedback = () => {
|
||||
const handleSubmitFeedback = async () => {
|
||||
if (feedbackText.trim()) {
|
||||
storage.set(`user:${state.currentUser.id}:feedback:${activeTopic.id}`, {
|
||||
text: feedbackText.trim(),
|
||||
topicId: activeTopic.id,
|
||||
week: state.weekNumber,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
await db.setSetting(
|
||||
`feedback:${state.currentUser.id}:${activeTopic.id}:${state.weekNumber}`,
|
||||
feedbackText.trim()
|
||||
);
|
||||
}
|
||||
setShowFeedbackModal(false);
|
||||
doComplete();
|
||||
|
||||
Reference in New Issue
Block a user