feat: add Leren page for AI-generated learning modules and update Caddyfile configuration for static routing
This commit is contained in:
30
Caddyfile
30
Caddyfile
@@ -1,7 +1,4 @@
|
||||
:80 {
|
||||
root * /srv
|
||||
file_server
|
||||
|
||||
encode gzip zstd
|
||||
|
||||
header {
|
||||
@@ -13,16 +10,6 @@
|
||||
Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||
}
|
||||
|
||||
@static {
|
||||
path *.css *.js *.png *.jpg *.jpeg *.gif *.webp *.svg *.woff *.woff2 *.ttf
|
||||
}
|
||||
header @static Cache-Control "public, max-age=31536000, immutable"
|
||||
|
||||
@html {
|
||||
path *.html /
|
||||
}
|
||||
header @html Cache-Control "public, max-age=0, must-revalidate"
|
||||
|
||||
handle /api/anthropic/* {
|
||||
uri strip_prefix /api/anthropic
|
||||
reverse_proxy https://api.anthropic.com {
|
||||
@@ -30,7 +17,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
try_files {path} /index.html
|
||||
handle {
|
||||
root * /srv
|
||||
|
||||
@static {
|
||||
path *.css *.js *.png *.jpg *.jpeg *.gif *.webp *.svg *.woff *.woff2 *.ttf
|
||||
}
|
||||
header @static Cache-Control "public, max-age=31536000, immutable"
|
||||
|
||||
@html {
|
||||
path *.html /
|
||||
}
|
||||
header @html Cache-Control "public, max-age=0, must-revalidate"
|
||||
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
|
||||
handle_errors {
|
||||
rewrite * /index.html
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { BookOpen, CheckCircle, Loader, ArrowRight, Plus, Search, ChevronLeft } from 'lucide-react';
|
||||
import { BookOpen, CheckCircle, Loader, ArrowRight, Plus, Search, ChevronLeft, MessageSquare } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Card from '../components/ui/Card';
|
||||
@@ -32,6 +32,11 @@ const Leren = () => {
|
||||
const [weeklyDone, setWeeklyDone] = useState(false);
|
||||
const [sessionDone, setSessionDone] = useState(false);
|
||||
|
||||
// Feedback
|
||||
const [showFeedbackModal, setShowFeedbackModal] = useState(false);
|
||||
const [feedbackText, setFeedbackText] = useState('');
|
||||
const [feedbackPrompted, setFeedbackPrompted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.currentUser) {
|
||||
const assigned = getAssignedTopic(state.currentUser.id, state.weekNumber);
|
||||
@@ -48,6 +53,8 @@ const Leren = () => {
|
||||
setView('detail');
|
||||
setSessionDone(false);
|
||||
setError(null);
|
||||
setFeedbackText('');
|
||||
setFeedbackPrompted(false);
|
||||
const cached = getCachedContent(topic.id);
|
||||
if (cached) {
|
||||
setContent(cached);
|
||||
@@ -90,7 +97,7 @@ const Leren = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
const doComplete = () => {
|
||||
setSessionDone(true);
|
||||
if (!weeklyDone) {
|
||||
setWeeklyDone(true);
|
||||
@@ -98,6 +105,33 @@ const Leren = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
if (!feedbackPrompted) {
|
||||
setFeedbackPrompted(true);
|
||||
setShowFeedbackModal(true);
|
||||
return;
|
||||
}
|
||||
doComplete();
|
||||
};
|
||||
|
||||
const handleSubmitFeedback = () => {
|
||||
if (feedbackText.trim()) {
|
||||
storage.set(`user:${state.currentUser.id}:feedback:${activeTopic.id}`, {
|
||||
text: feedbackText.trim(),
|
||||
topicId: activeTopic.id,
|
||||
week: state.weekNumber,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
setShowFeedbackModal(false);
|
||||
doComplete();
|
||||
};
|
||||
|
||||
const handleSkipFeedback = () => {
|
||||
setShowFeedbackModal(false);
|
||||
doComplete();
|
||||
};
|
||||
|
||||
// ── Detail View ──────────────────────────────────────────
|
||||
if (view === 'detail' && activeTopic) {
|
||||
if (sessionDone) {
|
||||
@@ -123,6 +157,50 @@ const Leren = () => {
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-8 max-w-4xl mx-auto pb-24 md:pb-8">
|
||||
<AnimatePresence>
|
||||
{showFeedbackModal && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0, y: 16 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.95, opacity: 0, y: 16 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 24 }}
|
||||
className="w-full max-w-lg"
|
||||
>
|
||||
<Card className="border border-bg-warm shadow-xl">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-full bg-teal/10 flex items-center justify-center flex-shrink-0">
|
||||
<MessageSquare size={20} className="text-teal" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold">How was this content?</h2>
|
||||
</div>
|
||||
<p className="text-fg-muted text-sm mb-5">
|
||||
Your feedback helps improve future learning content. This is optional — you can skip if you prefer.
|
||||
</p>
|
||||
<textarea
|
||||
autoFocus
|
||||
value={feedbackText}
|
||||
onChange={e => setFeedbackText(e.target.value)}
|
||||
placeholder="What was clear? What could be improved? Anything missing?"
|
||||
rows={4}
|
||||
className="w-full rounded-[var(--r-sm)] border border-bg-warm bg-bg p-3 text-sm resize-none focus:outline-none focus:border-teal transition-colors"
|
||||
/>
|
||||
<div className="flex justify-end gap-3 mt-4">
|
||||
<Button variant="outline" onClick={handleSkipFeedback}>Skip</Button>
|
||||
<Button onClick={handleSubmitFeedback}>Submit Feedback</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<button onClick={() => setView('overview')} className="flex items-center gap-2 text-fg-muted hover:text-teal mb-6 transition-colors">
|
||||
<ChevronLeft size={16} /> Back to overview
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user