feat: AI quiz generation per lesson + hide edit buttons for non-authors
- Generate unique quiz for each lesson using OpenRouter API - Parse lesson content (TipTap JSON) and send to AI for question generation - Cache quiz in database to avoid regeneration - Hide Edit/Delete buttons if current user is not course author - Add backendUser to auth context for proper authorization checks - Show certificate button prominently when course is completed Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -45,13 +45,14 @@ type CourseData = {
|
|||||||
title: string;
|
title: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
|
authorId: string;
|
||||||
chapters: Chapter[];
|
chapters: Chapter[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CoursePage() {
|
export default function CoursePage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { loading: authLoading } = useAuth();
|
const { loading: authLoading, backendUser } = useAuth();
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
const [course, setCourse] = useState<CourseData | null>(null);
|
const [course, setCourse] = useState<CourseData | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -249,6 +250,9 @@ export default function CoursePage() {
|
|||||||
? flatLessons.find((l) => l.id === selectedLessonId)
|
? flatLessons.find((l) => l.id === selectedLessonId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const isAuthor = course && backendUser && course.authorId === backendUser.id;
|
||||||
|
const courseCompleted = completedCount >= totalLessons && totalLessons > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)] -m-6 flex-col">
|
<div className="flex h-[calc(100vh-4rem)] -m-6 flex-col">
|
||||||
{/* Top bar */}
|
{/* Top bar */}
|
||||||
@ -272,6 +276,18 @@ export default function CoursePage() {
|
|||||||
<div className="h-2 w-2 rounded-full bg-primary" />
|
<div className="h-2 w-2 rounded-full bg-primary" />
|
||||||
<span className="text-xs font-medium">{progressPercent}% пройдено</span>
|
<span className="text-xs font-medium">{progressPercent}% пройдено</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Certificate button - show when course completed */}
|
||||||
|
{courseCompleted && (
|
||||||
|
<Button size="sm" variant="default" onClick={handleGetCertificate} disabled={generatingCertificate}>
|
||||||
|
<GraduationCap className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
{generatingCertificate ? 'Генерация...' : 'Получить сертификат'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Edit/Delete - only for author */}
|
||||||
|
{isAuthor && (
|
||||||
|
<>
|
||||||
<Button size="sm" variant="outline" asChild>
|
<Button size="sm" variant="outline" asChild>
|
||||||
<Link href={`/dashboard/courses/${course.id}/edit`}>
|
<Link href={`/dashboard/courses/${course.id}/edit`}>
|
||||||
<Edit className="mr-1.5 h-3.5 w-3.5" />
|
<Edit className="mr-1.5 h-3.5 w-3.5" />
|
||||||
@ -303,6 +319,8 @@ export default function CoursePage() {
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -520,16 +538,10 @@ export default function CoursePage() {
|
|||||||
Следующий урок
|
Следующий урок
|
||||||
<ChevronRight className="ml-1.5 h-4 w-4" />
|
<ChevronRight className="ml-1.5 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
) : completedCount >= totalLessons ? (
|
|
||||||
<Button size="sm" onClick={handleGetCertificate} disabled={generatingCertificate}>
|
|
||||||
<GraduationCap className="mr-1.5 h-4 w-4" />
|
|
||||||
{generatingCertificate ? 'Генерация...' : 'Получить сертификат'}
|
|
||||||
</Button>
|
|
||||||
) : (
|
) : (
|
||||||
<Button size="sm" variant="outline" disabled>
|
<div className="text-sm text-muted-foreground">
|
||||||
<GraduationCap className="mr-1.5 h-4 w-4" />
|
{courseCompleted ? 'Курс пройден!' : 'Последний урок'}
|
||||||
Завершить курс
|
</div>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,8 +6,17 @@ import { getSupabase } from '@/lib/supabase';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { api, setApiToken } from '@/lib/api';
|
import { api, setApiToken } from '@/lib/api';
|
||||||
|
|
||||||
|
interface BackendUser {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
subscriptionTier: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
|
backendUser: BackendUser | null;
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
signUp: (email: string, password: string, name: string) => Promise<{ error: Error | null }>;
|
signUp: (email: string, password: string, name: string) => Promise<{ error: Error | null }>;
|
||||||
@ -20,6 +29,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [backendUser, setBackendUser] = useState<BackendUser | null>(null);
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -64,8 +74,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const tryExchange = () => {
|
const tryExchange = () => {
|
||||||
api
|
api
|
||||||
.exchangeToken(session.access_token)
|
.exchangeToken(session.access_token)
|
||||||
.then(({ accessToken }) => {
|
.then(({ accessToken, user: backendUserData }) => {
|
||||||
setApiToken(accessToken);
|
setApiToken(accessToken);
|
||||||
|
setBackendUser(backendUserData);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -152,6 +163,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
|
backendUser,
|
||||||
session,
|
session,
|
||||||
loading,
|
loading,
|
||||||
signUp,
|
signUp,
|
||||||
|
|||||||
Reference in New Issue
Block a user