First commit
This commit is contained in:
26
lib/auth/requireTeacher.ts
Normal file
26
lib/auth/requireTeacher.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { requireUser } from "@/lib/auth/requireUser";
|
||||
|
||||
const readTeacherEmails = (): string[] =>
|
||||
(process.env.TEACHER_EMAILS ?? "")
|
||||
.split(",")
|
||||
.map((email) => email.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
export const requireTeacher = async () => {
|
||||
const user = await requireUser("/teacher");
|
||||
if (!user?.email) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const allowed = readTeacherEmails();
|
||||
if (allowed.length === 0) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
if (!allowed.includes(user.email.toLowerCase())) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
16
lib/auth/requireUser.ts
Normal file
16
lib/auth/requireUser.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { supabaseServer } from "@/lib/supabase/server";
|
||||
|
||||
export const requireUser = async (redirectTo: string) => {
|
||||
const supabase = await supabaseServer();
|
||||
if (!supabase) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (!data.user) {
|
||||
redirect(`/auth/login?redirectTo=${encodeURIComponent(redirectTo)}`);
|
||||
}
|
||||
|
||||
return data.user;
|
||||
};
|
||||
20
lib/data/courseCatalog.ts
Normal file
20
lib/data/courseCatalog.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { mockCourses } from "@/lib/data/mockCourses";
|
||||
import { getTeacherCourseBySlug, getTeacherCourses } from "@/lib/data/teacherCourses";
|
||||
import type { Course } from "@/types/course";
|
||||
|
||||
export const getAllCourses = (): Course[] => {
|
||||
const teacherCourses = getTeacherCourses();
|
||||
if (teacherCourses.length === 0) return mockCourses;
|
||||
|
||||
const teacherSlugs = new Set(teacherCourses.map((course) => course.slug));
|
||||
const baseCourses = mockCourses.filter((course) => !teacherSlugs.has(course.slug));
|
||||
return [...baseCourses, ...teacherCourses];
|
||||
};
|
||||
|
||||
export const getCourseBySlug = (slug: string): Course | undefined => {
|
||||
const teacher = getTeacherCourseBySlug(slug);
|
||||
if (teacher) return teacher;
|
||||
return mockCourses.find((course) => course.slug === slug);
|
||||
};
|
||||
|
||||
export const getAllCourseSlugs = (): string[] => getAllCourses().map((course) => course.slug);
|
||||
37
lib/data/mockCaseStudies.ts
Normal file
37
lib/data/mockCaseStudies.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { CaseStudy } from "@/types/caseStudy";
|
||||
|
||||
export const mockCaseStudies: CaseStudy[] = [
|
||||
{
|
||||
slug: "acme-v-zenith",
|
||||
title: "Acme v. Zenith: Non-Compete Enforcement",
|
||||
citation: "2021 App. Ct. 402",
|
||||
year: 2021,
|
||||
summary: "Dispute over enforceability of a cross-state non-compete with broad scope language.",
|
||||
level: "Intermediate",
|
||||
topic: "Employment",
|
||||
keyTerms: ["Reasonableness", "Geographic scope", "Public policy"],
|
||||
},
|
||||
{
|
||||
slug: "state-v-garcia",
|
||||
title: "State v. Garcia: Evidence Admissibility",
|
||||
citation: "2019 Sup. Ct. 88",
|
||||
year: 2019,
|
||||
summary: "Examines when digital communications meet admissibility and chain-of-custody standards.",
|
||||
level: "Advanced",
|
||||
topic: "Criminal Procedure",
|
||||
keyTerms: ["Authentication", "Hearsay exception", "Prejudice test"],
|
||||
},
|
||||
{
|
||||
slug: "harbor-bank-v-orchid",
|
||||
title: "Harbor Bank v. Orchid Labs",
|
||||
citation: "2023 Com. Ct. 110",
|
||||
year: 2023,
|
||||
summary: "Breach of financing covenants and acceleration remedies in a distressed credit event.",
|
||||
level: "Beginner",
|
||||
topic: "Commercial",
|
||||
keyTerms: ["Default", "Covenant breach", "Acceleration"],
|
||||
},
|
||||
];
|
||||
|
||||
export const getCaseStudyBySlug = (slug: string) =>
|
||||
mockCaseStudies.find((caseStudy) => caseStudy.slug === slug);
|
||||
84
lib/data/mockCourses.ts
Normal file
84
lib/data/mockCourses.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { Course } from "@/types/course";
|
||||
|
||||
export const mockCourses: Course[] = [
|
||||
{
|
||||
slug: "legal-english-foundations",
|
||||
title: "Legal English Foundations",
|
||||
level: "Beginner",
|
||||
summary: "Build legal vocabulary, core writing patterns, and court terminology.",
|
||||
rating: 4.8,
|
||||
weeks: 4,
|
||||
lessonsCount: 6,
|
||||
students: 1240,
|
||||
instructor: "Ana Morales, Esq.",
|
||||
lessons: [
|
||||
{ id: "l1", title: "Introduction to Legal Registers", type: "video", minutes: 12, isPreview: true },
|
||||
{ id: "l2", title: "Contract Vocabulary Basics", type: "reading", minutes: 10 },
|
||||
{ id: "l3", title: "Clause Spotting Drill", type: "interactive", minutes: 15 },
|
||||
{ id: "l4", title: "Writing a Formal Email", type: "reading", minutes: 9 },
|
||||
{ id: "l5", title: "Courtroom Terminology", type: "video", minutes: 14 },
|
||||
{ id: "l6", title: "Final Vocabulary Check", type: "interactive", minutes: 18 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "contract-analysis-practice",
|
||||
title: "Contract Analysis Practice",
|
||||
level: "Intermediate",
|
||||
summary: "Analyze contract sections and identify risk-heavy clauses quickly.",
|
||||
rating: 4.7,
|
||||
weeks: 5,
|
||||
lessonsCount: 7,
|
||||
students: 820,
|
||||
instructor: "Daniel Kim, LL.M.",
|
||||
lessons: [
|
||||
{ id: "l1", title: "How to Read a Service Agreement", type: "video", minutes: 16, isPreview: true },
|
||||
{ id: "l2", title: "Indemnity Clauses", type: "reading", minutes: 12 },
|
||||
{ id: "l3", title: "Liability Cap Walkthrough", type: "video", minutes: 11 },
|
||||
{ id: "l4", title: "Negotiation Red Flags", type: "interactive", minutes: 17 },
|
||||
{ id: "l5", title: "Warranty Language", type: "reading", minutes: 8 },
|
||||
{ id: "l6", title: "Termination Mechanics", type: "video", minutes: 13 },
|
||||
{ id: "l7", title: "Mini Review Exercise", type: "interactive", minutes: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "litigation-brief-writing",
|
||||
title: "Litigation Brief Writing",
|
||||
level: "Advanced",
|
||||
summary: "Craft persuasive briefs with strong authority usage and argument flow.",
|
||||
rating: 4.9,
|
||||
weeks: 6,
|
||||
lessonsCount: 8,
|
||||
students: 430,
|
||||
instructor: "Priya Shah, J.D.",
|
||||
lessons: [
|
||||
{ id: "l1", title: "Brief Structure and Strategy", type: "video", minutes: 19, isPreview: true },
|
||||
{ id: "l2", title: "Rule Synthesis", type: "reading", minutes: 14 },
|
||||
{ id: "l3", title: "Fact Framing Techniques", type: "video", minutes: 11 },
|
||||
{ id: "l4", title: "Citation Signals Deep Dive", type: "reading", minutes: 15 },
|
||||
{ id: "l5", title: "Counterargument Blocks", type: "interactive", minutes: 18 },
|
||||
{ id: "l6", title: "Oral Argument Prep", type: "video", minutes: 16 },
|
||||
{ id: "l7", title: "Editing for Precision", type: "interactive", minutes: 20 },
|
||||
{ id: "l8", title: "Submission Checklist", type: "reading", minutes: 9 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "cross-border-ip-strategy",
|
||||
title: "Cross-Border IP Strategy",
|
||||
level: "Advanced",
|
||||
summary: "Understand IP enforcement strategy across multiple jurisdictions.",
|
||||
rating: 4.6,
|
||||
weeks: 4,
|
||||
lessonsCount: 5,
|
||||
students: 290,
|
||||
instructor: "Luca Bianchi, Counsel",
|
||||
lessons: [
|
||||
{ id: "l1", title: "International IP Map", type: "video", minutes: 13, isPreview: true },
|
||||
{ id: "l2", title: "Venue and Jurisdiction", type: "reading", minutes: 11 },
|
||||
{ id: "l3", title: "Enforcement Cost Tradeoffs", type: "interactive", minutes: 14 },
|
||||
{ id: "l4", title: "Injunction Strategy", type: "video", minutes: 10 },
|
||||
{ id: "l5", title: "Portfolio Action Plan", type: "interactive", minutes: 17 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const getCourseBySlug = (slug: string) => mockCourses.find((course) => course.slug === slug);
|
||||
45
lib/data/mockPractice.ts
Normal file
45
lib/data/mockPractice.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { PracticeModule } from "@/types/practice";
|
||||
|
||||
export const mockPracticeModules: PracticeModule[] = [
|
||||
{
|
||||
slug: "translation",
|
||||
title: "Legal Translation Challenge",
|
||||
description: "Translate legal terms accurately in context with timed multiple-choice questions.",
|
||||
isInteractive: true,
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
prompt: "Spanish term: incumplimiento contractual",
|
||||
choices: ["Contractual compliance", "Breach of contract", "Contract interpretation", "Mutual assent"],
|
||||
answerIndex: 1,
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
prompt: "Spanish term: medida cautelar",
|
||||
choices: ["Class action", "Summary judgment", "Injunctive relief", "Arbitration clause"],
|
||||
answerIndex: 2,
|
||||
},
|
||||
{
|
||||
id: "q3",
|
||||
prompt: "Spanish term: fuerza mayor",
|
||||
choices: ["Strict liability", "Force majeure", "Punitive damages", "Specific performance"],
|
||||
answerIndex: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "term-matching",
|
||||
title: "Term Matching Game",
|
||||
description: "Pair legal terms with practical definitions.",
|
||||
isInteractive: false,
|
||||
},
|
||||
{
|
||||
slug: "contract-clauses",
|
||||
title: "Contract Clause Practice",
|
||||
description: "Identify weak and risky clause drafting choices.",
|
||||
isInteractive: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const getPracticeBySlug = (slug: string) =>
|
||||
mockPracticeModules.find((module) => module.slug === slug);
|
||||
133
lib/data/teacherCourses.ts
Normal file
133
lib/data/teacherCourses.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { Course, CourseLevel, Lesson } from "@/types/course";
|
||||
|
||||
const STORAGE_KEY = "acve.teacher-courses.v1";
|
||||
const TEACHER_UPDATED_EVENT = "acve:teacher-courses-updated";
|
||||
|
||||
export type TeacherCourseInput = {
|
||||
title: string;
|
||||
level: CourseLevel;
|
||||
summary: string;
|
||||
instructor: string;
|
||||
weeks: number;
|
||||
};
|
||||
|
||||
export type TeacherCoursePatch = Partial<Omit<Course, "slug" | "lessons">> & {
|
||||
summary?: string;
|
||||
title?: string;
|
||||
level?: CourseLevel;
|
||||
instructor?: string;
|
||||
weeks?: number;
|
||||
};
|
||||
|
||||
const isBrowser = () => typeof window !== "undefined";
|
||||
|
||||
const readStore = (): Course[] => {
|
||||
if (!isBrowser()) return [];
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return [];
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as Course[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const writeStore = (courses: Course[]) => {
|
||||
if (!isBrowser()) return;
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(courses));
|
||||
window.dispatchEvent(new Event(TEACHER_UPDATED_EVENT));
|
||||
};
|
||||
|
||||
const slugify = (value: string) =>
|
||||
value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-");
|
||||
|
||||
const lessonId = (index: number) => `lesson-${index}`;
|
||||
|
||||
const normalizeCourse = (course: Course): Course => ({
|
||||
...course,
|
||||
lessonsCount: course.lessons.length,
|
||||
});
|
||||
|
||||
export const teacherCoursesUpdatedEventName = TEACHER_UPDATED_EVENT;
|
||||
|
||||
export const getTeacherCourses = (): Course[] => readStore().map(normalizeCourse);
|
||||
|
||||
export const getTeacherCourseBySlug = (slug: string): Course | undefined =>
|
||||
getTeacherCourses().find((course) => course.slug === slug);
|
||||
|
||||
export const ensureUniqueTeacherCourseSlug = (title: string, reservedSlugs: string[]) => {
|
||||
const baseSlug = slugify(title) || "untitled-course";
|
||||
let slug = baseSlug;
|
||||
let index = 2;
|
||||
|
||||
const pool = new Set(reservedSlugs.map((item) => item.toLowerCase()));
|
||||
while (pool.has(slug.toLowerCase())) {
|
||||
slug = `${baseSlug}-${index}`;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return slug;
|
||||
};
|
||||
|
||||
export const createTeacherCourse = (input: TeacherCourseInput, reservedSlugs: string[]) => {
|
||||
const courses = getTeacherCourses();
|
||||
const slug = ensureUniqueTeacherCourseSlug(input.title, reservedSlugs);
|
||||
|
||||
const starterLesson: Lesson = {
|
||||
id: lessonId(1),
|
||||
title: "Course introduction",
|
||||
type: "video",
|
||||
minutes: 8,
|
||||
isPreview: true,
|
||||
videoUrl: "https://example.com/video-placeholder",
|
||||
};
|
||||
|
||||
const next: Course = {
|
||||
slug,
|
||||
title: input.title,
|
||||
level: input.level,
|
||||
summary: input.summary,
|
||||
rating: 5,
|
||||
weeks: Math.max(1, input.weeks),
|
||||
lessonsCount: 1,
|
||||
students: 0,
|
||||
instructor: input.instructor,
|
||||
lessons: [starterLesson],
|
||||
};
|
||||
|
||||
writeStore([...courses, next]);
|
||||
return next;
|
||||
};
|
||||
|
||||
export const updateTeacherCourse = (slug: string, patch: TeacherCoursePatch) => {
|
||||
const courses = getTeacherCourses();
|
||||
const nextCourses = courses.map((course) =>
|
||||
course.slug === slug ? normalizeCourse({ ...course, ...patch }) : course,
|
||||
);
|
||||
writeStore(nextCourses);
|
||||
return nextCourses.find((course) => course.slug === slug);
|
||||
};
|
||||
|
||||
export const addLessonToTeacherCourse = (slug: string, lesson: Omit<Lesson, "id">) => {
|
||||
const courses = getTeacherCourses();
|
||||
const nextCourses = courses.map((course) => {
|
||||
if (course.slug !== slug) return course;
|
||||
const nextLesson: Lesson = {
|
||||
...lesson,
|
||||
id: lessonId(course.lessons.length + 1),
|
||||
};
|
||||
return normalizeCourse({
|
||||
...course,
|
||||
lessons: [...course.lessons, nextLesson],
|
||||
});
|
||||
});
|
||||
|
||||
writeStore(nextCourses);
|
||||
return nextCourses.find((course) => course.slug === slug);
|
||||
};
|
||||
86
lib/progress/localProgress.ts
Normal file
86
lib/progress/localProgress.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
export type CourseProgress = {
|
||||
lastLessonId: string | null;
|
||||
completedLessonIds: string[];
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "acve.local-progress.v1";
|
||||
const PROGRESS_UPDATED_EVENT = "acve:progress-updated";
|
||||
|
||||
type ProgressStore = Record<string, Record<string, CourseProgress>>;
|
||||
|
||||
const defaultProgress: CourseProgress = {
|
||||
lastLessonId: null,
|
||||
completedLessonIds: [],
|
||||
};
|
||||
|
||||
const isBrowser = () => typeof window !== "undefined";
|
||||
|
||||
const readStore = (): ProgressStore => {
|
||||
if (!isBrowser()) return {};
|
||||
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return {};
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as ProgressStore;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const writeStore = (store: ProgressStore) => {
|
||||
if (!isBrowser()) return;
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
|
||||
window.dispatchEvent(new Event(PROGRESS_UPDATED_EVENT));
|
||||
};
|
||||
|
||||
const getOrCreateProgress = (store: ProgressStore, userId: string, courseSlug: string) => {
|
||||
if (!store[userId]) {
|
||||
store[userId] = {};
|
||||
}
|
||||
if (!store[userId][courseSlug]) {
|
||||
store[userId][courseSlug] = { ...defaultProgress };
|
||||
}
|
||||
return store[userId][courseSlug];
|
||||
};
|
||||
|
||||
export const getCourseProgress = (userId: string, courseSlug: string): CourseProgress => {
|
||||
const store = readStore();
|
||||
return store[userId]?.[courseSlug] ?? { ...defaultProgress };
|
||||
};
|
||||
|
||||
export const getCourseProgressPercent = (
|
||||
userId: string,
|
||||
courseSlug: string,
|
||||
totalLessons: number,
|
||||
): number => {
|
||||
if (totalLessons <= 0) return 0;
|
||||
const progress = getCourseProgress(userId, courseSlug);
|
||||
return Math.round((progress.completedLessonIds.length / totalLessons) * 100);
|
||||
};
|
||||
|
||||
export const setLastLesson = (userId: string, courseSlug: string, lessonId: string) => {
|
||||
const store = readStore();
|
||||
const progress = getOrCreateProgress(store, userId, courseSlug);
|
||||
progress.lastLessonId = lessonId;
|
||||
writeStore(store);
|
||||
};
|
||||
|
||||
export const markLessonComplete = (userId: string, courseSlug: string, lessonId: string) => {
|
||||
const store = readStore();
|
||||
const progress = getOrCreateProgress(store, userId, courseSlug);
|
||||
if (!progress.completedLessonIds.includes(lessonId)) {
|
||||
progress.completedLessonIds = [...progress.completedLessonIds, lessonId];
|
||||
}
|
||||
progress.lastLessonId = lessonId;
|
||||
writeStore(store);
|
||||
};
|
||||
|
||||
export const clearCourseProgress = (userId: string, courseSlug: string) => {
|
||||
const store = readStore();
|
||||
if (!store[userId]) return;
|
||||
delete store[userId][courseSlug];
|
||||
writeStore(store);
|
||||
};
|
||||
|
||||
export const progressUpdatedEventName = PROGRESS_UPDATED_EVENT;
|
||||
18
lib/supabase/browser.ts
Normal file
18
lib/supabase/browser.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
let browserClient: SupabaseClient | null = null;
|
||||
|
||||
export const supabaseBrowser = (): SupabaseClient | null => {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!url || !anonKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!browserClient) {
|
||||
browserClient = createClient(url, anonKey);
|
||||
}
|
||||
|
||||
return browserClient;
|
||||
};
|
||||
49
lib/supabase/middleware.ts
Normal file
49
lib/supabase/middleware.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
||||
|
||||
export type SessionSnapshot = {
|
||||
response: NextResponse;
|
||||
isAuthed: boolean;
|
||||
userEmail: string | null;
|
||||
isConfigured: boolean;
|
||||
};
|
||||
|
||||
export const updateSession = async (req: NextRequest): Promise<SessionSnapshot> => {
|
||||
const response = NextResponse.next({ request: req });
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
if (!url || !anonKey) {
|
||||
return {
|
||||
response,
|
||||
isAuthed: false,
|
||||
userEmail: null,
|
||||
isConfigured: false,
|
||||
};
|
||||
}
|
||||
|
||||
const supabase = createServerClient(url, anonKey, {
|
||||
cookies: {
|
||||
getAll: () => req.cookies.getAll(),
|
||||
setAll: (
|
||||
cookiesToSet: Array<{
|
||||
name: string;
|
||||
value: string;
|
||||
options?: CookieOptions;
|
||||
}>,
|
||||
) => {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
response.cookies.set(name, value, options);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await supabase.auth.getUser();
|
||||
return {
|
||||
response,
|
||||
isAuthed: Boolean(data?.user),
|
||||
userEmail: data.user?.email ?? null,
|
||||
isConfigured: true,
|
||||
};
|
||||
};
|
||||
36
lib/supabase/server.ts
Normal file
36
lib/supabase/server.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
||||
|
||||
export const supabaseServer = async () => {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!url || !anonKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cookieStore = await cookies();
|
||||
|
||||
return createServerClient(url, anonKey, {
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll();
|
||||
},
|
||||
setAll(
|
||||
cookiesToSet: Array<{
|
||||
name: string;
|
||||
value: string;
|
||||
options?: CookieOptions;
|
||||
}>,
|
||||
) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) => {
|
||||
cookieStore.set(name, value, options);
|
||||
});
|
||||
} catch {
|
||||
// Server Components may not be able to write cookies.
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user