From fe22e5d90be3aa83078537302fc9b1d160f1bbb6 Mon Sep 17 00:00:00 2001 From: VNOsST Date: Sun, 8 Jun 2025 13:06:34 +0700 Subject: [PATCH 01/15] export custom-data-table to packages/ui --- .../src/components/custom-data-table.tsx | 51 +------ apps/web/src/components/custom-data-table.tsx | 51 +------ .../ui/custom/tables/custom-data-table.tsx | 52 ++++++++ packages/ui/src/hooks/useSearchParams.tsx | 126 ++++++++++++++++++ 4 files changed, 180 insertions(+), 100 deletions(-) create mode 100644 packages/ui/src/components/ui/custom/tables/custom-data-table.tsx create mode 100644 packages/ui/src/hooks/useSearchParams.tsx diff --git a/apps/upskii/src/components/custom-data-table.tsx b/apps/upskii/src/components/custom-data-table.tsx index 622ef00843..d3236d376f 100644 --- a/apps/upskii/src/components/custom-data-table.tsx +++ b/apps/upskii/src/components/custom-data-table.tsx @@ -1,52 +1,3 @@ 'use client'; -import useSearchParams from '@/hooks/useSearchParams'; -import { - DataTable, - DataTableProps, -} from '@tuturuuu/ui/custom/tables/data-table'; -import { useTranslations } from 'next-intl'; -import { useRouter } from 'next/navigation'; -import { Suspense } from 'react'; - -export function CustomDataTable({ - namespace, - hideToolbar, - hidePagination, - className, - ...props -}: DataTableProps) { - const t = useTranslations(); - const router = useRouter(); - const searchParams = useSearchParams(); - - const pageSize = Number(searchParams.get('pageSize') || 10); - const page = Number(searchParams.get('page') || 0); - const pageIndex = page > 0 ? page - 1 : 0; - - return ( - - router.refresh()} - defaultQuery={searchParams.getSingle('q', '')} - onSearch={(query: string) => - query - ? searchParams.set({ q: query, page: '1' }) - : searchParams.reset() - } - setParams={(params) => searchParams.set(params)} - resetParams={() => searchParams.reset()} - isEmpty={searchParams.isEmpty} - newObjectTitle={t('common.create')} - className={className} - {...props} - /> - - ); -} +export { CustomDataTable } from '@tuturuuu/ui/custom/tables/custom-data-table'; diff --git a/apps/web/src/components/custom-data-table.tsx b/apps/web/src/components/custom-data-table.tsx index 622ef00843..d3236d376f 100644 --- a/apps/web/src/components/custom-data-table.tsx +++ b/apps/web/src/components/custom-data-table.tsx @@ -1,52 +1,3 @@ 'use client'; -import useSearchParams from '@/hooks/useSearchParams'; -import { - DataTable, - DataTableProps, -} from '@tuturuuu/ui/custom/tables/data-table'; -import { useTranslations } from 'next-intl'; -import { useRouter } from 'next/navigation'; -import { Suspense } from 'react'; - -export function CustomDataTable({ - namespace, - hideToolbar, - hidePagination, - className, - ...props -}: DataTableProps) { - const t = useTranslations(); - const router = useRouter(); - const searchParams = useSearchParams(); - - const pageSize = Number(searchParams.get('pageSize') || 10); - const page = Number(searchParams.get('page') || 0); - const pageIndex = page > 0 ? page - 1 : 0; - - return ( - - router.refresh()} - defaultQuery={searchParams.getSingle('q', '')} - onSearch={(query: string) => - query - ? searchParams.set({ q: query, page: '1' }) - : searchParams.reset() - } - setParams={(params) => searchParams.set(params)} - resetParams={() => searchParams.reset()} - isEmpty={searchParams.isEmpty} - newObjectTitle={t('common.create')} - className={className} - {...props} - /> - - ); -} +export { CustomDataTable } from '@tuturuuu/ui/custom/tables/custom-data-table'; diff --git a/packages/ui/src/components/ui/custom/tables/custom-data-table.tsx b/packages/ui/src/components/ui/custom/tables/custom-data-table.tsx new file mode 100644 index 0000000000..6164065d38 --- /dev/null +++ b/packages/ui/src/components/ui/custom/tables/custom-data-table.tsx @@ -0,0 +1,52 @@ +'use client'; + +import useSearchParams from '../../../../hooks/useSearchParams'; +import { + DataTable, + DataTableProps, +} from '@tuturuuu/ui/custom/tables/data-table'; +import { useTranslations } from 'next-intl'; +import { useRouter } from 'next/navigation'; +import { Suspense } from 'react'; + +export function CustomDataTable({ + namespace, + hideToolbar, + hidePagination, + className, + ...props +}: DataTableProps) { + const t = useTranslations(); + const router = useRouter(); + const searchParams = useSearchParams(); + + const pageSize = Number(searchParams.get('pageSize') || 10); + const page = Number(searchParams.get('page') || 0); + const pageIndex = page > 0 ? page - 1 : 0; + + return ( + + router.refresh()} + defaultQuery={searchParams.getSingle('q', '')} + onSearch={(query: string) => + query + ? searchParams.set({ q: query, page: '1' }) + : searchParams.reset() + } + setParams={(params) => searchParams.set(params)} + resetParams={() => searchParams.reset()} + isEmpty={searchParams.isEmpty} + newObjectTitle={t('common.create')} + className={className} + {...props} + /> + + ); +} diff --git a/packages/ui/src/hooks/useSearchParams.tsx b/packages/ui/src/hooks/useSearchParams.tsx new file mode 100644 index 0000000000..9405c0a6a8 --- /dev/null +++ b/packages/ui/src/hooks/useSearchParams.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { + useSearchParams as useDefaultSearchParams, + usePathname, + useRouter, +} from 'next/navigation'; +import { useCallback } from 'react'; + +const useSearchParams = () => { + const searchParams = useDefaultSearchParams(); + const pathname = usePathname(); + const router = useRouter(); + + const has = useCallback( + (key: string) => searchParams.has(key), + [searchParams] + ); + + const get = useCallback( + (data: { key: string; fallbackValue?: string | string[] } | string) => { + if (typeof data === 'string') return searchParams.getAll(data) || ''; + + const { key, fallbackValue } = data; + return searchParams.getAll(key).length > 0 + ? searchParams.getAll(key) || fallbackValue + : fallbackValue; + }, + [searchParams] + ); + + const getSingle = useCallback( + (key: string, fallbackValue?: string) => { + return searchParams.get(key) || fallbackValue; + }, + [searchParams] + ); + + const set = useCallback( + ( + data: Record, + refresh = true + ) => { + const params = new URLSearchParams(searchParams); + + Object.entries(data).forEach(([key, value]) => { + if (value === undefined) return; + if (Array.isArray(value)) { + params.delete(key); + value + .filter((v) => v !== undefined && v !== null) + .forEach((item) => params.append(key, item)); + } else { + params.set(key, value.toString()); + } + }); + + router.push(`${pathname}?${params.toString()}`); + if (refresh) router.refresh(); + }, + [pathname, router, searchParams] + ); + + const add = useCallback( + (key: string, value: string | string[], refresh = true) => { + const params = new URLSearchParams(searchParams); + if (Array.isArray(value)) { + value.forEach((item) => params.append(key, item)); + } else { + params.append(key, value); + } + router.push(`${pathname}?${params.toString()}`); + if (refresh) router.refresh(); + }, + [pathname, router, searchParams] + ); + + const remove = useCallback( + (key: string, refresh = true) => { + const params = new URLSearchParams(searchParams); + params.delete(key); + router.push(`${pathname}?${params.toString()}`); + if (refresh) router.refresh(); + }, + [pathname, router, searchParams] + ); + + const reset = useCallback( + (refresh = true) => { + router.push(pathname); + if (refresh) router.refresh(); + }, + [pathname, router] + ); + + const getAll = useCallback(() => { + const params = new URLSearchParams(searchParams); + const entries = params.entries(); + const result: Record = {}; + for (const [key, value] of entries) { + if (result[key]) { + result[key]?.push(value); + } else { + result[key] = [value]; + } + } + return result; + }, [searchParams]); + + const isEmpty = searchParams.toString().length === 0; + + return { + isEmpty, + has, + get, + set, + add, + remove, + reset, + clear: reset, + getAll, + getSingle, + }; +}; + +export default useSearchParams; From 9af37d68301f81cf16bb11ac2b9ef15fa2170f97 Mon Sep 17 00:00:00 2001 From: VNOsST Date: Sun, 8 Jun 2025 13:26:46 +0700 Subject: [PATCH 02/15] export course-form --- .../(dashboard)/[wsId]/courses/page.tsx | 2 +- .../[wsId]/courses/row-actions.tsx | 4 +- apps/web/messages/en.json | 3 +- apps/web/messages/vi.json | 3 +- .../[wsId]/education/courses/form.tsx | 104 ------------------ .../[wsId]/education/courses/page.tsx | 2 +- .../[wsId]/education/courses/row-actions.tsx | 9 +- .../ui/custom/education/course-form.tsx | 2 +- 8 files changed, 16 insertions(+), 113 deletions(-) delete mode 100644 apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/form.tsx rename apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/form.tsx => packages/ui/src/components/ui/custom/education/course-form.tsx (97%) diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx index e0bfec42d1..48b2813981 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx @@ -1,7 +1,7 @@ import { CourseCardView } from './card-view'; import { getWorkspaceCourseColumns } from './columns'; import { CoursePagination } from './course-pagination'; -import CourseForm from './form'; +import { CourseForm } from '@tuturuuu/ui/custom/education/course-form'; import { ViewToggle } from './view-toggle'; //import { mockData } from './mock/mock-courses'; import { CustomDataTable } from '@/components/custom-data-table'; diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/row-actions.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/row-actions.tsx index 054834377d..765b18079c 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/row-actions.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/row-actions.tsx @@ -1,9 +1,9 @@ 'use client'; -import WorkspaceCourseForm from './form'; import { Row } from '@tanstack/react-table'; import { WorkspaceCourse } from '@tuturuuu/types/db'; import { Button } from '@tuturuuu/ui/button'; +import { CourseForm } from '@tuturuuu/ui/custom/education/course-form'; import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger'; import { DropdownMenu, @@ -102,7 +102,7 @@ export function WorkspaceCourseRowActions({ editDescription={t('ws-courses.edit_description')} setOpen={setShowEditDialog} form={ - diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 9e3a0a5cb1..a25880c47a 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1910,7 +1910,8 @@ "description": "Manage courses in your workspace that include markdown content, slides, quizzes, and flashcards.", "edit": "Edit course", "name": "Course name", - "edit_description": "Edit an existing course" + "edit_description": "Edit an existing course", + "course_description": "Course description" }, "ws-course-modules": { "plural": "Course Modules", diff --git a/apps/web/messages/vi.json b/apps/web/messages/vi.json index 2b4a3a174b..431dbfa1e5 100644 --- a/apps/web/messages/vi.json +++ b/apps/web/messages/vi.json @@ -1922,7 +1922,8 @@ "create_description": "Tạo một khóa học mới", "edit": "Chỉnh sửa khóa học", "name": "Tên khóa học", - "edit_description": "Chỉnh sửa khóa học hiện có" + "edit_description": "Chỉnh sửa khóa học hiện có", + "course_description": "Mô tả khóa học" }, "ws-course-modules": { "plural": "Mô-đun khóa học", diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/form.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/form.tsx deleted file mode 100644 index 465075907d..0000000000 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/form.tsx +++ /dev/null @@ -1,104 +0,0 @@ -'use client'; - -import { WorkspaceCourse } from '@tuturuuu/types/db'; -import { Button } from '@tuturuuu/ui/button'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@tuturuuu/ui/form'; -import { useForm } from '@tuturuuu/ui/hooks/use-form'; -import { toast } from '@tuturuuu/ui/hooks/use-toast'; -import { Input } from '@tuturuuu/ui/input'; -import { zodResolver } from '@tuturuuu/ui/resolvers'; -import { useTranslations } from 'next-intl'; -import { useRouter } from 'next/navigation'; -import * as z from 'zod'; - -interface Props { - wsId: string; - data?: WorkspaceCourse; - // eslint-disable-next-line no-unused-vars - onFinish?: (data: z.infer) => void; -} - -const FormSchema = z.object({ - id: z.string().optional(), - name: z.string().min(1), -}); - -export default function CourseForm({ wsId, data, onFinish }: Props) { - const t = useTranslations('ws-courses'); - const router = useRouter(); - - const form = useForm({ - resolver: zodResolver(FormSchema), - values: { - id: data?.id, - name: data?.name || '', - }, - }); - - const isDirty = form.formState.isDirty; - const isValid = form.formState.isValid; - const isSubmitting = form.formState.isSubmitting; - - const disabled = !isDirty || !isValid || isSubmitting; - - const onSubmit = async (data: z.infer) => { - try { - const res = await fetch( - data.id - ? `/api/v1/workspaces/${wsId}/courses/${data.id}` - : `/api/v1/workspaces/${wsId}/courses`, - { - method: data.id ? 'PUT' : 'POST', - body: JSON.stringify(data), - } - ); - - if (res.ok) { - onFinish?.(data); - router.refresh(); - } else { - const data = await res.json(); - toast({ - title: `Failed to ${data.id ? 'edit' : 'create'} course`, - description: data.message, - }); - } - } catch (error) { - toast({ - title: `Failed to ${data.id ? 'edit' : 'create'} course`, - description: error instanceof Error ? error.message : String(error), - }); - } - }; - - return ( -
- - ( - - {t('name')} - - - - - - )} - /> - - - - - ); -} diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx index e0abe53dc8..133c358489 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx @@ -1,5 +1,5 @@ import { getWorkspaceCourseColumns } from './columns'; -import CourseForm from './form'; +import { CourseForm } from '@tuturuuu/ui/custom/education/course-form'; import { CustomDataTable } from '@/components/custom-data-table'; import { createClient } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourse } from '@tuturuuu/types/db'; diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx index aaf18caff3..ff3eae7e8b 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx @@ -1,9 +1,9 @@ 'use client'; -import WorkspaceCourseForm from './form'; import { Row } from '@tanstack/react-table'; import { WorkspaceCourse } from '@tuturuuu/types/db'; import { Button } from '@tuturuuu/ui/button'; +import { CourseForm } from '@tuturuuu/ui/custom/education/course-form'; import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger'; import { DropdownMenu, @@ -91,7 +91,12 @@ export function WorkspaceCourseRowActions({ title={t('ws-flashcards.edit')} editDescription={t('ws-flashcards.edit_description')} setOpen={setShowEditDialog} - form={} + form={ + + } /> ); diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/form.tsx b/packages/ui/src/components/ui/custom/education/course-form.tsx similarity index 97% rename from apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/form.tsx rename to packages/ui/src/components/ui/custom/education/course-form.tsx index 82cb54ba87..83c94a1ad3 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/form.tsx +++ b/packages/ui/src/components/ui/custom/education/course-form.tsx @@ -31,7 +31,7 @@ const FormSchema = z.object({ description: z.string().optional(), }); -export default function CourseForm({ wsId, data, onFinish }: Props) { +export function CourseForm({ wsId, data, onFinish }: Props) { const t = useTranslations('ws-courses'); const router = useRouter(); From 4950da7c7b8bccf712d5a081c08b0b3b455ae7d6 Mon Sep 17 00:00:00 2001 From: VNOsST Date: Sun, 8 Jun 2025 13:42:09 +0700 Subject: [PATCH 03/15] export card view and pagniation --- apps/upskii/messages/en.json | 8 +- apps/upskii/messages/vi.json | 8 +- .../(dashboard)/[wsId]/courses/page.tsx | 6 +- apps/web/messages/en.json | 8 +- .../[wsId]/education/courses/page.tsx | 99 ++++++++++++++----- .../ui/custom/education/course-card-view.tsx | 2 +- .../ui/custom/education/course-pagination.tsx | 56 +++++++++++ .../src/components/ui/custom}/view-toggle.tsx | 0 8 files changed, 151 insertions(+), 36 deletions(-) rename apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/card-view.tsx => packages/ui/src/components/ui/custom/education/course-card-view.tsx (98%) create mode 100644 packages/ui/src/components/ui/custom/education/course-pagination.tsx rename {apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses => packages/ui/src/components/ui/custom}/view-toggle.tsx (100%) diff --git a/apps/upskii/messages/en.json b/apps/upskii/messages/en.json index 1090935f1b..d63692663c 100644 --- a/apps/upskii/messages/en.json +++ b/apps/upskii/messages/en.json @@ -3524,7 +3524,9 @@ "edit_description": "Edit an existing course", "delete_confirm_title": "Deleting \"{name}\"", "card_view": "Card View", - "table_view": "Table View" + "table_view": "Table View", + "no_courses_found": "No courses found", + "no_description_provided": "No description provided" }, "ws-crawlers": { "plural": "Crawlers", @@ -4136,7 +4138,5 @@ "ai_teach_studio": "AI Teaching Studio", "users": "Users", "teams": "Teams" - }, - "no-courses-found": "No courses found", - "no-description-provided": "No description provided" + } } diff --git a/apps/upskii/messages/vi.json b/apps/upskii/messages/vi.json index 90934abef9..6fb8ec32d1 100644 --- a/apps/upskii/messages/vi.json +++ b/apps/upskii/messages/vi.json @@ -3525,7 +3525,9 @@ "delete_confirm_title": "Xác nhận xoá \"{name}\"", "course_description": "Mô tả khóa học", "card_view": "Dạng thẻ", - "table_view": "Dạng bảng" + "table_view": "Dạng bảng", + "no_courses_found": "Chưa có khóa học nào", + "no_description_provided": "Không có mô tả" }, "ws-crawlers": { "plural": "Công cụ cào dữ liệu", @@ -4131,7 +4133,5 @@ "empty_state_description": "Hoàn thành các khóa học để giành chứng chỉ và thể hiện thành tích của bạn.", "browse_courses": "Xem các khóa học", "certified": "Được chứng nhận" - }, - "no-courses-found": "Chưa có khóa học nào", - "no-description-provided": "Không có mô tả" + } } diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx index 48b2813981..4d42561e28 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx @@ -1,13 +1,13 @@ -import { CourseCardView } from './card-view'; +import { CourseCardView } from '@tuturuuu/ui/custom/education/course-card-view'; import { getWorkspaceCourseColumns } from './columns'; import { CoursePagination } from './course-pagination'; -import { CourseForm } from '@tuturuuu/ui/custom/education/course-form'; -import { ViewToggle } from './view-toggle'; //import { mockData } from './mock/mock-courses'; import { CustomDataTable } from '@/components/custom-data-table'; import { createClient } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourse } from '@tuturuuu/types/db'; +import { CourseForm } from '@tuturuuu/ui/custom/education/course-form'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; +import { ViewToggle } from '@tuturuuu/ui/custom/view-toggle'; import { Separator } from '@tuturuuu/ui/separator'; import { getTranslations } from 'next-intl/server'; diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index a25880c47a..4f10ff0674 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1911,7 +1911,12 @@ "edit": "Edit course", "name": "Course name", "edit_description": "Edit an existing course", - "course_description": "Course description" + "course_description": "Course description", + "card_view": "Card view", + "table_view": "Table view", + "no_courses_found": "No courses found", + "no_courses_found_description": "No courses found in your workspace. Create a new course to get started.", + "no_description_provided": "No description provided" }, "ws-course-modules": { "plural": "Course Modules", @@ -3067,4 +3072,5 @@ "start_over": "Start over" }, "benefits:": "Benefits:" + } diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx index 133c358489..6a54ddbf06 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx @@ -1,9 +1,13 @@ import { getWorkspaceCourseColumns } from './columns'; -import { CourseForm } from '@tuturuuu/ui/custom/education/course-form'; +//import { mockData } from './mock/mock-courses'; import { CustomDataTable } from '@/components/custom-data-table'; import { createClient } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourse } from '@tuturuuu/types/db'; +import { CourseCardView } from '@tuturuuu/ui/custom/education/course-card-view'; +import { CourseForm } from '@tuturuuu/ui/custom/education/course-form'; +import { CoursePagination } from '@tuturuuu/ui/custom/education/course-pagination'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; +import { ViewToggle } from '@tuturuuu/ui/custom/view-toggle'; import { Separator } from '@tuturuuu/ui/separator'; import { getTranslations } from 'next-intl/server'; @@ -13,6 +17,7 @@ interface SearchParams { pageSize?: string; includedTags?: string | string[]; excludedTags?: string | string[]; + view?: 'card' | 'table'; } interface Props { @@ -28,36 +33,64 @@ export default async function WorkspaceCoursesPage({ }: Props) { const t = await getTranslations(); const { wsId } = await params; + const searchParamsResolved = await searchParams; + const { page = '1', pageSize = '10' } = searchParamsResolved; - const { data, count } = await getData(wsId, await searchParams); + const { data, count } = await getData(wsId, searchParamsResolved); + const currentView = searchParamsResolved.view || 'card'; + const currentPage = parseInt(page); + const currentPageSize = parseInt(pageSize); + const totalPages = Math.ceil(count / currentPageSize); const courses = data.map((c) => ({ ...c, ws_id: wsId, - href: `/${wsId}/education/courses/${c.id}`, + href: `/${wsId}/courses/${c.id}`, })); return ( <> - } - /> - - +
+ } + /> + + +
+ +
+ + {currentView === 'card' ? ( + <> + +
+ +
+ + ) : ( + + )} +
); } @@ -71,6 +104,26 @@ async function getData( retry = true, }: { q?: string; page?: string; pageSize?: string; retry?: boolean } = {} ) { + // if (process.env.NODE_ENV === 'development') { + // // Placing mock data for testing + // const allMock: WorkspaceCourse[] = mockData(); + + // const filteredData = allMock.filter((course) => + // q ? course.name.toLowerCase().includes(q.toLowerCase()) : true + // ); + + // const parsedPage = parseInt(page); + // const parsedSize = parseInt(pageSize); + // const start = (parsedPage - 1) * parsedSize; + // const end = parsedPage * parsedSize; + // const paginatedData = filteredData.slice(start, end); + + // return { + // data: paginatedData, + // count: filteredData.length, + // }; + // } + const supabase = await createClient(); const queryBuilder = supabase @@ -103,5 +156,5 @@ async function getData( modules: workspace_course_modules?.[0]?.count || 0, })), count, - } as { data: WorkspaceCourse[]; count: number }; + } as { data: (WorkspaceCourse & { modules: number })[]; count: number }; } diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/card-view.tsx b/packages/ui/src/components/ui/custom/education/course-card-view.tsx similarity index 98% rename from apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/card-view.tsx rename to packages/ui/src/components/ui/custom/education/course-card-view.tsx index 333ff36072..a1181f784c 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/card-view.tsx +++ b/packages/ui/src/components/ui/custom/education/course-card-view.tsx @@ -75,7 +75,7 @@ export function CourseCardView({ courses }: CourseCardViewProps) {

- {course.description || t('no-description-provided')} + {course.description || t('ws-courses.no_description_provided')}

diff --git a/packages/ui/src/components/ui/custom/education/course-pagination.tsx b/packages/ui/src/components/ui/custom/education/course-pagination.tsx new file mode 100644 index 0000000000..9fc6af6745 --- /dev/null +++ b/packages/ui/src/components/ui/custom/education/course-pagination.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { DataPagination } from '@tuturuuu/ui/custom/data-pagination'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useCallback } from 'react'; + +interface CoursePaginationProps { + currentPage: number; + totalPages: number; + totalCount: number; + pageSize: number; + wsId: string; +} + +export function CoursePagination({ + currentPage, + totalPages, + totalCount, + pageSize, + wsId, +}: CoursePaginationProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const handlePageChange = useCallback( + (page: number) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('page', page.toString()); + router.push(`/${wsId}/courses?${params.toString()}`); + }, + [router, searchParams, wsId] + ); + + const handlePageSizeChange = useCallback( + (newPageSize: number) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('pageSize', newPageSize.toString()); + params.set('page', '1'); // Reset to first page when changing page size + router.push(`/${wsId}/courses?${params.toString()}`); + }, + [router, searchParams, wsId] + ); + + return ( + + ); +} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/view-toggle.tsx b/packages/ui/src/components/ui/custom/view-toggle.tsx similarity index 100% rename from apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/view-toggle.tsx rename to packages/ui/src/components/ui/custom/view-toggle.tsx From 3a9477afebcf1db1048cfe4ee937d0eff7b6e962 Mon Sep 17 00:00:00 2001 From: VNOsST Date: Sun, 8 Jun 2025 13:55:58 +0700 Subject: [PATCH 04/15] export course-module-form --- .../[wsId]/courses/[courseId]/page.tsx | 2 +- .../[wsId]/courses/[courseId]/row-actions.tsx | 5 +- .../(dashboard)/[wsId]/courses/page.tsx | 4 +- .../[wsId]/courses/row-actions.tsx | 2 +- .../education/courses/[courseId]/page.tsx | 2 +- .../courses/[courseId]/row-actions.tsx | 10 +- .../[wsId]/education/courses/page.tsx | 8 +- .../[wsId]/education/courses/row-actions.tsx | 2 +- .../{ => courses}/course-card-view.tsx | 0 .../education/{ => courses}/course-form.tsx | 0 .../{ => courses}/course-pagination.tsx | 0 .../education/modules/course-module-form.tsx | 110 ++++++++++++++++++ 12 files changed, 125 insertions(+), 20 deletions(-) rename packages/ui/src/components/ui/custom/education/{ => courses}/course-card-view.tsx (100%) rename packages/ui/src/components/ui/custom/education/{ => courses}/course-form.tsx (100%) rename packages/ui/src/components/ui/custom/education/{ => courses}/course-pagination.tsx (100%) create mode 100644 packages/ui/src/components/ui/custom/education/modules/course-module-form.tsx diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/page.tsx index 3c5efc3c71..af4488057c 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/page.tsx @@ -1,10 +1,10 @@ import { getWorkspaceCourseModuleColumns } from './columns'; -import CourseModuleForm from './form'; import { CustomDataTable } from '@/components/custom-data-table'; import { createClient } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; import { Database } from '@tuturuuu/types/supabase'; import { Button } from '@tuturuuu/ui/button'; +import { CourseModuleForm } from '@tuturuuu/ui/custom/education/modules/course-module-form'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; import { Separator } from '@tuturuuu/ui/separator'; import { getTranslations } from 'next-intl/server'; diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx index de39cb9c59..e7cbbc6c94 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx @@ -1,6 +1,6 @@ 'use client'; -import WorkspaceCourseModuleForm from './form'; + import { Row } from '@tanstack/react-table'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; import { Button } from '@tuturuuu/ui/button'; @@ -17,6 +17,7 @@ import { Ellipsis } from '@tuturuuu/ui/icons'; import { useTranslations } from 'next-intl'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import { CourseModuleForm } from '@tuturuuu/ui/custom/education/modules/course-module-form'; interface WorkspaceCourseModuleRowActionsProps { wsId: string; @@ -97,7 +98,7 @@ export function WorkspaceCourseModuleRowActions({ editDescription={t('ws-course-modules.edit_description')} setOpen={setShowEditDialog} form={ - - } + form={} />
); diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx index 6a54ddbf06..6ae764ee09 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/page.tsx @@ -3,9 +3,9 @@ import { getWorkspaceCourseColumns } from './columns'; import { CustomDataTable } from '@/components/custom-data-table'; import { createClient } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourse } from '@tuturuuu/types/db'; -import { CourseCardView } from '@tuturuuu/ui/custom/education/course-card-view'; -import { CourseForm } from '@tuturuuu/ui/custom/education/course-form'; -import { CoursePagination } from '@tuturuuu/ui/custom/education/course-pagination'; +import { CourseCardView } from '@tuturuuu/ui/custom/education/courses/course-card-view'; +import { CourseForm } from '@tuturuuu/ui/custom/education/courses/course-form'; +import { CoursePagination } from '@tuturuuu/ui/custom/education/courses/course-pagination'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; import { ViewToggle } from '@tuturuuu/ui/custom/view-toggle'; import { Separator } from '@tuturuuu/ui/separator'; @@ -45,7 +45,7 @@ export default async function WorkspaceCoursesPage({ const courses = data.map((c) => ({ ...c, ws_id: wsId, - href: `/${wsId}/courses/${c.id}`, + href: `/${wsId}/education/courses/${c.id}`, })); return ( diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx index ff3eae7e8b..0bfb2dd15d 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx @@ -3,7 +3,7 @@ import { Row } from '@tanstack/react-table'; import { WorkspaceCourse } from '@tuturuuu/types/db'; import { Button } from '@tuturuuu/ui/button'; -import { CourseForm } from '@tuturuuu/ui/custom/education/course-form'; +import { CourseForm } from '@tuturuuu/ui/custom/education/courses/course-form'; import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger'; import { DropdownMenu, diff --git a/packages/ui/src/components/ui/custom/education/course-card-view.tsx b/packages/ui/src/components/ui/custom/education/courses/course-card-view.tsx similarity index 100% rename from packages/ui/src/components/ui/custom/education/course-card-view.tsx rename to packages/ui/src/components/ui/custom/education/courses/course-card-view.tsx diff --git a/packages/ui/src/components/ui/custom/education/course-form.tsx b/packages/ui/src/components/ui/custom/education/courses/course-form.tsx similarity index 100% rename from packages/ui/src/components/ui/custom/education/course-form.tsx rename to packages/ui/src/components/ui/custom/education/courses/course-form.tsx diff --git a/packages/ui/src/components/ui/custom/education/course-pagination.tsx b/packages/ui/src/components/ui/custom/education/courses/course-pagination.tsx similarity index 100% rename from packages/ui/src/components/ui/custom/education/course-pagination.tsx rename to packages/ui/src/components/ui/custom/education/courses/course-pagination.tsx diff --git a/packages/ui/src/components/ui/custom/education/modules/course-module-form.tsx b/packages/ui/src/components/ui/custom/education/modules/course-module-form.tsx new file mode 100644 index 0000000000..60328c6761 --- /dev/null +++ b/packages/ui/src/components/ui/custom/education/modules/course-module-form.tsx @@ -0,0 +1,110 @@ +'use client'; + +import { WorkspaceCourseModule } from '@tuturuuu/types/db'; +import { Button } from '@tuturuuu/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@tuturuuu/ui/form'; +import { useForm } from '@tuturuuu/ui/hooks/use-form'; +import { toast } from '@tuturuuu/ui/hooks/use-toast'; +import { Input } from '@tuturuuu/ui/input'; +import { zodResolver } from '@tuturuuu/ui/resolvers'; +import { useTranslations } from 'next-intl'; +import { useRouter } from 'next/navigation'; +import * as z from 'zod'; + +interface Props { + wsId: string; + courseId: string; + data?: WorkspaceCourseModule; + // eslint-disable-next-line no-unused-vars + onFinish?: (data: z.infer) => void; +} + +const FormSchema = z.object({ + id: z.string().optional(), + name: z.string().min(1), +}); + +export function CourseModuleForm({ + wsId, + courseId, + data, + onFinish, +}: Props) { + const t = useTranslations('ws-course-modules'); + const router = useRouter(); + + const form = useForm({ + resolver: zodResolver(FormSchema), + values: { + id: data?.id, + name: data?.name || '', + }, + }); + + const isDirty = form.formState.isDirty; + const isValid = form.formState.isValid; + const isSubmitting = form.formState.isSubmitting; + + const disabled = !isDirty || !isValid || isSubmitting; + + const onSubmit = async (data: z.infer) => { + try { + const res = await fetch( + data.id + ? `/api/v1/workspaces/${wsId}/course-modules/${data.id}` + : `/api/v1/workspaces/${wsId}/courses/${courseId}/modules`, + { + method: data.id ? 'PUT' : 'POST', + body: JSON.stringify(data), + } + ); + + if (res.ok) { + onFinish?.(data); + router.refresh(); + } else { + const data = await res.json(); + toast({ + title: `Failed to ${data.id ? 'edit' : 'create'} course module`, + description: data.message, + }); + } + } catch (error) { + toast({ + title: `Failed to ${data.id ? 'edit' : 'create'} course module`, + description: error instanceof Error ? error.message : String(error), + }); + } + }; + + return ( +
+ + ( + + {t('name')} + + + + + + )} + /> + + + + + ); +} From 0d3186ad7c83e2f9e570f8100d0e587b24f34d21 Mon Sep 17 00:00:00 2001 From: VNOsST <118870645+VNOsST@users.noreply.github.com> Date: Sun, 8 Jun 2025 07:06:43 +0000 Subject: [PATCH 05/15] style: apply prettier formatting --- .../[wsId]/courses/[courseId]/row-actions.tsx | 11 ++--------- .../app/[locale]/(dashboard)/[wsId]/courses/page.tsx | 2 +- apps/web/messages/en.json | 1 - .../ui/custom/education/courses/course-card-view.tsx | 3 ++- .../custom/education/modules/course-module-form.tsx | 7 +------ 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx index e7cbbc6c94..4a2296a4a9 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx @@ -1,9 +1,9 @@ 'use client'; - import { Row } from '@tanstack/react-table'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; import { Button } from '@tuturuuu/ui/button'; +import { CourseModuleForm } from '@tuturuuu/ui/custom/education/modules/course-module-form'; import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger'; import { DropdownMenu, @@ -17,7 +17,6 @@ import { Ellipsis } from '@tuturuuu/ui/icons'; import { useTranslations } from 'next-intl'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; -import { CourseModuleForm } from '@tuturuuu/ui/custom/education/modules/course-module-form'; interface WorkspaceCourseModuleRowActionsProps { wsId: string; @@ -97,13 +96,7 @@ export function WorkspaceCourseModuleRowActions({ title={t('ws-course-modules.edit')} editDescription={t('ws-course-modules.edit_description')} setOpen={setShowEditDialog} - form={ - - } + form={} />

- {course.description || t('ws-courses.no_description_provided')} + {course.description || + t('ws-courses.no_description_provided')}

diff --git a/packages/ui/src/components/ui/custom/education/modules/course-module-form.tsx b/packages/ui/src/components/ui/custom/education/modules/course-module-form.tsx index 60328c6761..54d3e21cfa 100644 --- a/packages/ui/src/components/ui/custom/education/modules/course-module-form.tsx +++ b/packages/ui/src/components/ui/custom/education/modules/course-module-form.tsx @@ -31,12 +31,7 @@ const FormSchema = z.object({ name: z.string().min(1), }); -export function CourseModuleForm({ - wsId, - courseId, - data, - onFinish, -}: Props) { +export function CourseModuleForm({ wsId, courseId, data, onFinish }: Props) { const t = useTranslations('ws-course-modules'); const router = useRouter(); From 03eec5a7f478dce7e5059d7d9757480309f44239 Mon Sep 17 00:00:00 2001 From: VNOsST Date: Sun, 8 Jun 2025 14:29:16 +0700 Subject: [PATCH 06/15] Migrate row actions --- .../[wsId]/courses/[courseId]/columns.tsx | 2 +- .../(dashboard)/[wsId]/courses/columns.tsx | 2 +- .../(dashboard)/[wsId]/courses/page.tsx | 2 +- apps/web/messages/en.json | 4 +- .../education/courses/[courseId]/columns.tsx | 2 +- .../courses/[courseId]/row-actions.tsx | 102 ----------------- .../[wsId]/education/courses/columns.tsx | 2 +- .../[wsId]/education/courses/row-actions.tsx | 103 ------------------ .../education/courses/course-row-actions.tsx | 0 .../modules/course-module-row-actions.tsx | 11 +- 10 files changed, 9 insertions(+), 221 deletions(-) delete mode 100644 apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/row-actions.tsx delete mode 100644 apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx rename apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/row-actions.tsx => packages/ui/src/components/ui/custom/education/courses/course-row-actions.tsx (100%) rename apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx => packages/ui/src/components/ui/custom/education/modules/course-module-row-actions.tsx (96%) diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/columns.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/columns.tsx index 54d17645fd..1a2856f15d 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/columns.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/columns.tsx @@ -1,8 +1,8 @@ 'use client'; -import { WorkspaceCourseModuleRowActions } from './row-actions'; import { ColumnDef } from '@tanstack/react-table'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; +import { WorkspaceCourseModuleRowActions } from '@tuturuuu/ui/custom/education/modules/course-module-row-actions'; import { DataTableColumnHeader } from '@tuturuuu/ui/custom/tables/data-table-column-header'; import { Check, X } from '@tuturuuu/ui/icons'; import moment from 'moment'; diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/columns.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/columns.tsx index 23a2c178b1..de835d2b15 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/columns.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/columns.tsx @@ -1,6 +1,6 @@ 'use client'; -import { WorkspaceCourseRowActions } from './row-actions'; +import { WorkspaceCourseRowActions } from '@tuturuuu/ui/custom/education/courses/course-row-actions'; import { ColumnDef } from '@tanstack/react-table'; import { WorkspaceCourse } from '@tuturuuu/types/db'; import { DataTableColumnHeader } from '@tuturuuu/ui/custom/tables/data-table-column-header'; diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx index 9ca12ef06f..9c0b06200f 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/page.tsx @@ -4,8 +4,8 @@ import { CoursePagination } from './course-pagination'; import { CustomDataTable } from '@/components/custom-data-table'; import { createClient } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourse } from '@tuturuuu/types/db'; -import { CourseForm } from '@tuturuuu/ui/custom/education/courses/course-form'; import { CourseCardView } from '@tuturuuu/ui/custom/education/courses/course-card-view'; +import { CourseForm } from '@tuturuuu/ui/custom/education/courses/course-form'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; import { ViewToggle } from '@tuturuuu/ui/custom/view-toggle'; import { Separator } from '@tuturuuu/ui/separator'; diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 4f10ff0674..7be677b857 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1916,7 +1916,8 @@ "table_view": "Table view", "no_courses_found": "No courses found", "no_courses_found_description": "No courses found in your workspace. Create a new course to get started.", - "no_description_provided": "No description provided" + "no_description_provided": "No description provided", + "delete_confirm_title": "Deleting \"{name}\"" }, "ws-course-modules": { "plural": "Course Modules", @@ -3072,5 +3073,4 @@ "start_over": "Start over" }, "benefits:": "Benefits:" - } diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/columns.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/columns.tsx index 7e28c27a04..b6a758fd98 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/columns.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/columns.tsx @@ -1,8 +1,8 @@ 'use client'; -import { WorkspaceCourseModuleRowActions } from './row-actions'; import { ColumnDef } from '@tanstack/react-table'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; +import { WorkspaceCourseModuleRowActions } from '@tuturuuu/ui/custom/education/modules/course-module-row-actions'; import { DataTableColumnHeader } from '@tuturuuu/ui/custom/tables/data-table-column-header'; import { Check, X } from '@tuturuuu/ui/icons'; import moment from 'moment'; diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/row-actions.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/row-actions.tsx deleted file mode 100644 index 53d5ce7680..0000000000 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/row-actions.tsx +++ /dev/null @@ -1,102 +0,0 @@ -'use client'; - -import { Row } from '@tanstack/react-table'; -import { WorkspaceCourseModule } from '@tuturuuu/types/db'; -import { Button } from '@tuturuuu/ui/button'; -import { CourseModuleForm } from '@tuturuuu/ui/custom/education/modules/course-module-form'; -import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@tuturuuu/ui/dropdown-menu'; -import { toast } from '@tuturuuu/ui/hooks/use-toast'; -import { Ellipsis } from '@tuturuuu/ui/icons'; -import { useTranslations } from 'next-intl'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; - -interface WorkspaceCourseModuleRowActionsProps { - wsId: string; - courseId: string; - row: Row; -} - -export function WorkspaceCourseModuleRowActions({ - wsId, - courseId, - row, -}: WorkspaceCourseModuleRowActionsProps) { - const router = useRouter(); - const t = useTranslations(); - - const data = row.original; - - const deleteWorkspaceCourseModule = async () => { - const res = await fetch( - `/api/v1/workspaces/${wsId}/course-modules/${data.id}`, - { - method: 'DELETE', - } - ); - - if (res.ok) { - router.refresh(); - } else { - const data = await res.json(); - toast({ - title: 'Failed to delete workspace user group tag', - description: data.message, - }); - } - }; - - const [showEditDialog, setShowEditDialog] = useState(false); - - if (!data.id || !wsId) return null; - - return ( -
- {/* {data.href && ( - - - - )} */} - - - - - - - setShowEditDialog(true)}> - {t('common.edit')} - - - - {t('common.delete')} - - - - - } - /> -
- ); -} diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/columns.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/columns.tsx index 23a2c178b1..ae306dc57d 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/columns.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/columns.tsx @@ -1,8 +1,8 @@ 'use client'; -import { WorkspaceCourseRowActions } from './row-actions'; import { ColumnDef } from '@tanstack/react-table'; import { WorkspaceCourse } from '@tuturuuu/types/db'; +import { WorkspaceCourseRowActions } from '@tuturuuu/ui/custom/education/courses/course-row-actions'; import { DataTableColumnHeader } from '@tuturuuu/ui/custom/tables/data-table-column-header'; import moment from 'moment'; import Link from 'next/link'; diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx deleted file mode 100644 index 0bfb2dd15d..0000000000 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/row-actions.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client'; - -import { Row } from '@tanstack/react-table'; -import { WorkspaceCourse } from '@tuturuuu/types/db'; -import { Button } from '@tuturuuu/ui/button'; -import { CourseForm } from '@tuturuuu/ui/custom/education/courses/course-form'; -import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@tuturuuu/ui/dropdown-menu'; -import { toast } from '@tuturuuu/ui/hooks/use-toast'; -import { Ellipsis } from '@tuturuuu/ui/icons'; -import { useTranslations } from 'next-intl'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; - -interface WorkspaceCourseRowActionsProps { - row: Row; -} - -export function WorkspaceCourseRowActions({ - row, -}: WorkspaceCourseRowActionsProps) { - const router = useRouter(); - const t = useTranslations(); - - const data = row.original; - - const deleteWorkspaceCourse = async () => { - const res = await fetch( - `/api/v1/workspaces/${data.ws_id}/courses/${data.id}`, - { - method: 'DELETE', - } - ); - - if (res.ok) { - router.refresh(); - } else { - const data = await res.json(); - toast({ - title: 'Failed to delete workspace user group tag', - description: data.message, - }); - } - }; - - const [showEditDialog, setShowEditDialog] = useState(false); - - if (!data.id || !data.ws_id) return null; - - return ( -
- {/* {data.href && ( - - - - )} */} - - - - - - - setShowEditDialog(true)}> - {t('common.edit')} - - - - {t('common.delete')} - - - - - - } - /> -
- ); -} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/row-actions.tsx b/packages/ui/src/components/ui/custom/education/courses/course-row-actions.tsx similarity index 100% rename from apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/row-actions.tsx rename to packages/ui/src/components/ui/custom/education/courses/course-row-actions.tsx diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx b/packages/ui/src/components/ui/custom/education/modules/course-module-row-actions.tsx similarity index 96% rename from apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx rename to packages/ui/src/components/ui/custom/education/modules/course-module-row-actions.tsx index e7cbbc6c94..4a2296a4a9 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/row-actions.tsx +++ b/packages/ui/src/components/ui/custom/education/modules/course-module-row-actions.tsx @@ -1,9 +1,9 @@ 'use client'; - import { Row } from '@tanstack/react-table'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; import { Button } from '@tuturuuu/ui/button'; +import { CourseModuleForm } from '@tuturuuu/ui/custom/education/modules/course-module-form'; import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger'; import { DropdownMenu, @@ -17,7 +17,6 @@ import { Ellipsis } from '@tuturuuu/ui/icons'; import { useTranslations } from 'next-intl'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; -import { CourseModuleForm } from '@tuturuuu/ui/custom/education/modules/course-module-form'; interface WorkspaceCourseModuleRowActionsProps { wsId: string; @@ -97,13 +96,7 @@ export function WorkspaceCourseModuleRowActions({ title={t('ws-course-modules.edit')} editDescription={t('ws-course-modules.edit_description')} setOpen={setShowEditDialog} - form={ - - } + form={} /> Date: Sun, 8 Jun 2025 14:47:05 +0700 Subject: [PATCH 07/15] export section Implement tiptap editor to ttr --- .../[wsId]/courses/[courseId]/form.tsx | 110 ------------------ .../modules/[moduleId]/content/page.tsx | 2 +- .../[courseId]/modules/[moduleId]/page.tsx | 2 +- apps/web/messages/en.json | 6 +- .../modules/[moduleId]/content/page.tsx | 42 +++++-- .../[courseId]/modules/[moduleId]/page.tsx | 22 ++-- .../education/modules/content-section.tsx | 0 .../ui/text-editor}/content-editor.tsx | 6 +- 8 files changed, 54 insertions(+), 136 deletions(-) delete mode 100644 apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/form.tsx rename apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/section.tsx => packages/ui/src/components/ui/custom/education/modules/content-section.tsx (100%) rename {apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/content => packages/ui/src/components/ui/text-editor}/content-editor.tsx (94%) diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/form.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/form.tsx deleted file mode 100644 index 4973b7ad09..0000000000 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/form.tsx +++ /dev/null @@ -1,110 +0,0 @@ -'use client'; - -import { WorkspaceCourseModule } from '@tuturuuu/types/db'; -import { Button } from '@tuturuuu/ui/button'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@tuturuuu/ui/form'; -import { useForm } from '@tuturuuu/ui/hooks/use-form'; -import { toast } from '@tuturuuu/ui/hooks/use-toast'; -import { Input } from '@tuturuuu/ui/input'; -import { zodResolver } from '@tuturuuu/ui/resolvers'; -import { useTranslations } from 'next-intl'; -import { useRouter } from 'next/navigation'; -import * as z from 'zod'; - -interface Props { - wsId: string; - courseId: string; - data?: WorkspaceCourseModule; - // eslint-disable-next-line no-unused-vars - onFinish?: (data: z.infer) => void; -} - -const FormSchema = z.object({ - id: z.string().optional(), - name: z.string().min(1), -}); - -export default function CourseModuleForm({ - wsId, - courseId, - data, - onFinish, -}: Props) { - const t = useTranslations('ws-course-modules'); - const router = useRouter(); - - const form = useForm({ - resolver: zodResolver(FormSchema), - values: { - id: data?.id, - name: data?.name || '', - }, - }); - - const isDirty = form.formState.isDirty; - const isValid = form.formState.isValid; - const isSubmitting = form.formState.isSubmitting; - - const disabled = !isDirty || !isValid || isSubmitting; - - const onSubmit = async (data: z.infer) => { - try { - const res = await fetch( - data.id - ? `/api/v1/workspaces/${wsId}/course-modules/${data.id}` - : `/api/v1/workspaces/${wsId}/courses/${courseId}/modules`, - { - method: data.id ? 'PUT' : 'POST', - body: JSON.stringify(data), - } - ); - - if (res.ok) { - onFinish?.(data); - router.refresh(); - } else { - const data = await res.json(); - toast({ - title: `Failed to ${data.id ? 'edit' : 'create'} course module`, - description: data.message, - }); - } - } catch (error) { - toast({ - title: `Failed to ${data.id ? 'edit' : 'create'} course module`, - description: error instanceof Error ? error.message : String(error), - }); - } - }; - - return ( -
- - ( - - {t('name')} - - - - - - )} - /> - - - - - ); -} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/content/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/content/page.tsx index 1c1af586d2..84b1e4b735 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/content/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/content/page.tsx @@ -1,8 +1,8 @@ -import ModuleContentEditor from './content-editor'; import { createClient } from '@tuturuuu/supabase/next/server'; import { Button } from '@tuturuuu/ui/button'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; import { Goal, Sparkles } from '@tuturuuu/ui/icons'; +import { ModuleContentEditor } from '@tuturuuu/ui/text-editor/content-editor'; import { JSONContent } from '@tuturuuu/ui/tiptap'; import { getTranslations } from 'next-intl/server'; diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx index 0cd17885f8..ca184a6762 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx @@ -1,4 +1,4 @@ -import { CourseSection } from '../../section'; +import { CourseSection } from '@tuturuuu/ui/custom/education/modules/content-section'; import ClientFlashcards from './flashcards/client-flashcards'; import ClientQuizzes from './quizzes/client-quizzes'; import FileDisplay from './resources/file-display'; diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 7be677b857..fd17505d3b 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -284,7 +284,11 @@ "waitlist": "Waitlist", "sign_in_with_google": "Sign in with Google", "sign_in_with_microsoft": "Sign in with Microsoft", - "sign_in_with_github": "Sign in with Github" + "sign_in_with_github": "Sign in with Github", + "whats_the_title": "What's the title?", + "write_something": "Write something", + "error_saving_content": "There was an error saving your content", + "not_completed": "Not completed" }, "date_helper": { "time-unit": "Time unit", diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/page.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/page.tsx index b6e20ed372..84b1e4b735 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/page.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/content/page.tsx @@ -1,19 +1,39 @@ +import { createClient } from '@tuturuuu/supabase/next/server'; import { Button } from '@tuturuuu/ui/button'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; import { Goal, Sparkles } from '@tuturuuu/ui/icons'; +import { ModuleContentEditor } from '@tuturuuu/ui/text-editor/content-editor'; +import { JSONContent } from '@tuturuuu/ui/tiptap'; import { getTranslations } from 'next-intl/server'; -// interface Props { -// params: Promise<{ -// wsId: string; -// courseId: string; -// moduleId: string; -// }>; -// } +interface Props { + params: Promise<{ + courseId: string; + moduleId: string; + }>; +} -export default async function ModuleContentPage() { +export default async function ModuleContentPage({ params }: Props) { + const { courseId, moduleId } = await params; const t = await getTranslations(); + const getContent = async (courseId: string, moduleId: string) => { + const supabase = await createClient(); + const { data, error } = await supabase + .from('workspace_course_modules') + .select('content') + .eq('id', moduleId) + .eq('course_id', courseId); + + if (error) { + console.error(error); + } + + return data?.[0]?.content as JSONContent; + }; + + const content = await getContent(courseId, moduleId); + return (
- {/* */} +
); } diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/page.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/page.tsx index f13e841956..dcc874c95b 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/page.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/page.tsx @@ -1,4 +1,3 @@ -import { CourseSection } from '../../section'; import ClientFlashcards from './flashcards/client-flashcards'; import ClientQuizzes from './quizzes/client-quizzes'; import FileDisplay from './resources/file-display'; @@ -9,6 +8,8 @@ import { createDynamicClient, } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; +import { Accordion } from '@tuturuuu/ui/accordion'; +import { CourseSection } from '@tuturuuu/ui/custom/education/modules/content-section'; import { BookText, Goal, @@ -18,6 +19,8 @@ import { Youtube, } from '@tuturuuu/ui/icons'; import { Separator } from '@tuturuuu/ui/separator'; +import { RichTextEditor } from '@tuturuuu/ui/text-editor/editor'; +import { JSONContent } from '@tuturuuu/ui/tiptap'; import { getTranslations } from 'next-intl/server'; interface Props { @@ -67,17 +70,18 @@ export default async function UserGroupDetailsPage({ params }: Props) { })); return ( -
+ } rawContent={data.content as any | undefined} content={ - data.content - ? // - undefined - : undefined + data.content ? ( +
+ +
+ ) : undefined } /> - } @@ -158,7 +162,7 @@ export default async function UserGroupDetailsPage({ params }: Props) {
) : undefined } - /> + /> */} -
+ ); } diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/section.tsx b/packages/ui/src/components/ui/custom/education/modules/content-section.tsx similarity index 100% rename from apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/section.tsx rename to packages/ui/src/components/ui/custom/education/modules/content-section.tsx diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/content/content-editor.tsx b/packages/ui/src/components/ui/text-editor/content-editor.tsx similarity index 94% rename from apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/content/content-editor.tsx rename to packages/ui/src/components/ui/text-editor/content-editor.tsx index f3ab4451dc..4a6fa4f098 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/content/content-editor.tsx +++ b/packages/ui/src/components/ui/text-editor/content-editor.tsx @@ -13,11 +13,7 @@ interface Props { content?: JSONContent; } -export default function ModuleContentEditor({ - courseId, - moduleId, - content, -}: Props) { +export function ModuleContentEditor({ courseId, moduleId, content }: Props) { const [post, setPost] = useState(content || null); const t = useTranslations(); From 5453faa83eaa221096aee6ad7672e1047bf6e354 Mon Sep 17 00:00:00 2001 From: VNOsST Date: Sun, 8 Jun 2025 15:10:31 +0700 Subject: [PATCH 08/15] export link button and course header --- .../[wsId]/courses/[courseId]/layout.tsx | 25 ++-------- .../[courseId]/modules/[moduleId]/layout.tsx | 4 +- .../education/courses/[courseId]/layout.tsx | 24 ++------- .../courses/[courseId]/link-button.tsx | 50 ------------------- .../[courseId]/modules/[moduleId]/layout.tsx | 4 +- .../education/courses/course-header.tsx | 38 ++++++++++++++ .../custom/education/modules}/link-button.tsx | 0 7 files changed, 50 insertions(+), 95 deletions(-) delete mode 100644 apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/link-button.tsx create mode 100644 packages/ui/src/components/ui/custom/education/courses/course-header.tsx rename {apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId] => packages/ui/src/components/ui/custom/education/modules}/link-button.tsx (100%) diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/layout.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/layout.tsx index 14fbca3d58..5abf00e197 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/layout.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/layout.tsx @@ -1,10 +1,7 @@ import { createClient } from '@tuturuuu/supabase/next/server'; import { UserGroup } from '@tuturuuu/types/primitives/UserGroup'; -import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; -import { GraduationCap } from '@tuturuuu/ui/icons'; +import { CourseHeader } from '@tuturuuu/ui/custom/education/courses/course-header'; import { Separator } from '@tuturuuu/ui/separator'; -import { getTranslations } from 'next-intl/server'; -import Link from 'next/link'; import { notFound } from 'next/navigation'; import { ReactNode } from 'react'; @@ -18,28 +15,14 @@ interface Props { } export default async function CourseDetailsLayout({ children, params }: Props) { - const t = await getTranslations(); const { wsId, courseId } = await params; const data = await getData(wsId, courseId); return ( <> - -
- - {t('ws-courses.singular')} -
- - {data.name || t('common.unknown')} - - - } - description={
{data.description || t('common.unknown')}
} + {children} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/layout.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/layout.tsx index a81e545f33..63144034f4 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/layout.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/layout.tsx @@ -1,10 +1,10 @@ -import LinkButton from '../../link-button'; import ModuleToggles from './toggles'; import { createClient, createDynamicClient, } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; +import LinkButton from '@tuturuuu/ui/custom/education/modules/link-button'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; import { BookText, @@ -41,7 +41,7 @@ export default async function CourseDetailsLayout({ children, params }: Props) { const data = await getData(courseId, moduleId); const resources = await getResources({ - path: `${wsId}/courses/${courseId}/modules/${moduleId}/resources/`, + path: `${commonHref}/resources/`, }); const completionStatus = await getCompletionStatus(moduleId); diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/layout.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/layout.tsx index e88136f5fc..293970eb61 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/layout.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/layout.tsx @@ -1,10 +1,7 @@ import { createClient } from '@tuturuuu/supabase/next/server'; import { UserGroup } from '@tuturuuu/types/primitives/UserGroup'; -import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; -import { GraduationCap } from '@tuturuuu/ui/icons'; +import { CourseHeader } from '@tuturuuu/ui/custom/education/courses/course-header'; import { Separator } from '@tuturuuu/ui/separator'; -import { getTranslations } from 'next-intl/server'; -import Link from 'next/link'; import { notFound } from 'next/navigation'; import { ReactNode } from 'react'; @@ -18,27 +15,14 @@ interface Props { } export default async function CourseDetailsLayout({ children, params }: Props) { - const t = await getTranslations(); const { wsId, courseId } = await params; const data = await getData(wsId, courseId); return ( <> - -
- - {t('ws-courses.singular')} -
- - {data.name || t('common.unknown')} - - - } + {children} diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/link-button.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/link-button.tsx deleted file mode 100644 index 7066396a17..0000000000 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/link-button.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; - -import { Button } from '@tuturuuu/ui/button'; -import { cn } from '@tuturuuu/utils/format'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { ReactNode } from 'react'; - -export default function LinkButton({ - href, - title, - icon, - className, - disabled, -}: { - href: string; - title: ReactNode; - icon: ReactNode; - className?: string; - disabled?: boolean; -}) { - const pathname = usePathname(); - - if (href === pathname || disabled) - return ( - - ); - - return ( - - - - ); -} diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/layout.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/layout.tsx index 3b39f15114..e810c7c9b1 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/layout.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/layout.tsx @@ -1,10 +1,10 @@ -import LinkButton from '../../link-button'; import ModuleToggles from './toggles'; import { createClient, createDynamicClient, } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; +import LinkButton from '@tuturuuu/ui/custom/education/modules/link-button'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; import { BookText, @@ -39,7 +39,7 @@ export default async function CourseDetailsLayout({ children, params }: Props) { const data = await getData(courseId, moduleId); const resources = await getResources({ - path: `${wsId}/courses/${courseId}/modules/${moduleId}/resources/`, + path: `${commonHref}/resources/`, }); const flashcards = await getFlashcards(moduleId); diff --git a/packages/ui/src/components/ui/custom/education/courses/course-header.tsx b/packages/ui/src/components/ui/custom/education/courses/course-header.tsx new file mode 100644 index 0000000000..fa83fd8bca --- /dev/null +++ b/packages/ui/src/components/ui/custom/education/courses/course-header.tsx @@ -0,0 +1,38 @@ +import { UserGroup } from '@tuturuuu/types/primitives/UserGroup'; +import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; +import { GraduationCap } from '@tuturuuu/ui/icons'; +import { useTranslations } from 'next-intl'; +import Link from 'next/link'; + +export function CourseHeader({ + href, + data, +}: { + href: string; + data: UserGroup; +}) { + const t = useTranslations(); + return ( + +
+ + {t('ws-courses.singular')} +
+ + {data.name || t('common.unknown')} + + + } + description={ +
+ {data.description || t('ws-courses.no_description_provided')} +
+ } + /> + ); +} diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/link-button.tsx b/packages/ui/src/components/ui/custom/education/modules/link-button.tsx similarity index 100% rename from apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/link-button.tsx rename to packages/ui/src/components/ui/custom/education/modules/link-button.tsx From dfc929d7da3e782b28d02bb95e8f4acf69e61fd6 Mon Sep 17 00:00:00 2001 From: VNOsST Date: Sun, 8 Jun 2025 15:14:33 +0700 Subject: [PATCH 09/15] Update layout.tsx --- .../(dashboard)/[wsId]/education/quiz-sets/[setId]/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/layout.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/layout.tsx index 9c1f1bd199..9f44e67fc4 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/layout.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/quiz-sets/[setId]/layout.tsx @@ -1,6 +1,6 @@ -import LinkButton from '../../courses/[courseId]/link-button'; import { createClient } from '@tuturuuu/supabase/next/server'; import { type WorkspaceQuizSet } from '@tuturuuu/types/db'; +import LinkButton from '@tuturuuu/ui/custom/education/modules/link-button'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; import { Box, Eye, Paperclip } from '@tuturuuu/ui/icons'; import { Separator } from '@tuturuuu/ui/separator'; From 7592faf32376e2109c880d6f2855fb996b13216a Mon Sep 17 00:00:00 2001 From: VNOsST Date: Sun, 8 Jun 2025 15:16:54 +0700 Subject: [PATCH 10/15] Update page.tsx --- .../education/courses/[courseId]/modules/[moduleId]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/page.tsx b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/page.tsx index 29c996662e..54786bab5d 100644 --- a/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/page.tsx +++ b/apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/page.tsx @@ -162,7 +162,7 @@ export default async function UserGroupDetailsPage({ params }: Props) { ) : undefined } - /> */} + /> Date: Sun, 8 Jun 2025 19:52:32 +0700 Subject: [PATCH 11/15] export youtube and module toggle --- .../[courseId]/modules/[moduleId]/layout.tsx | 2 +- .../[courseId]/modules/[moduleId]/page.tsx | 4 +- .../[moduleId]/youtube-links/embed.tsx | 17 --- .../modules/[moduleId]/youtube-links/page.tsx | 6 +- .../[wsId]/courses/course-pagination.tsx | 56 --------- .../(dashboard)/[wsId]/courses/page.tsx | 2 +- .../education/courses/[courseId]/form.tsx | 110 ---------------- .../[courseId]/modules/[moduleId]/layout.tsx | 2 +- .../[courseId]/modules/[moduleId]/page.tsx | 2 +- .../[courseId]/modules/[moduleId]/toggles.tsx | 92 -------------- .../[moduleId]/youtube-links/delete-link.tsx | 65 ---------- .../modules/[moduleId]/youtube-links/form.tsx | 119 ------------------ .../modules/[moduleId]/youtube-links/page.tsx | 6 +- .../education/courses/[courseId]/section.tsx | 65 ---------- .../education/modules/module-toggle.tsx | 2 +- .../modules/youtube/delete-link-button.tsx | 0 .../education/modules/youtube}/embed.tsx | 0 .../education/modules/youtube}/form.tsx | 0 18 files changed, 13 insertions(+), 537 deletions(-) delete mode 100644 apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/youtube-links/embed.tsx delete mode 100644 apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/course-pagination.tsx delete mode 100644 apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/form.tsx delete mode 100644 apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/toggles.tsx delete mode 100644 apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/youtube-links/delete-link.tsx delete mode 100644 apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/youtube-links/form.tsx delete mode 100644 apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/section.tsx rename apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/toggles.tsx => packages/ui/src/components/ui/custom/education/modules/module-toggle.tsx (98%) rename apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/youtube-links/delete-link.tsx => packages/ui/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx (100%) rename {apps/web/src/app/[locale]/(dashboard)/[wsId]/education/courses/[courseId]/modules/[moduleId]/youtube-links => packages/ui/src/components/ui/custom/education/modules/youtube}/embed.tsx (100%) rename {apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/youtube-links => packages/ui/src/components/ui/custom/education/modules/youtube}/form.tsx (100%) diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/layout.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/layout.tsx index 63144034f4..75d83d63a1 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/layout.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/layout.tsx @@ -1,10 +1,10 @@ -import ModuleToggles from './toggles'; import { createClient, createDynamicClient, } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; import LinkButton from '@tuturuuu/ui/custom/education/modules/link-button'; +import { ModuleToggles } from '@tuturuuu/ui/custom/education/modules/module-toggle'; import FeatureSummary from '@tuturuuu/ui/custom/feature-summary'; import { BookText, diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx index ca184a6762..801b904ca0 100644 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx +++ b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/page.tsx @@ -1,8 +1,6 @@ -import { CourseSection } from '@tuturuuu/ui/custom/education/modules/content-section'; import ClientFlashcards from './flashcards/client-flashcards'; import ClientQuizzes from './quizzes/client-quizzes'; import FileDisplay from './resources/file-display'; -import { YoutubeEmbed } from './youtube-links/embed'; import { extractYoutubeId } from '@/utils/url-helper'; import { createClient, @@ -10,6 +8,8 @@ import { } from '@tuturuuu/supabase/next/server'; import { WorkspaceCourseModule } from '@tuturuuu/types/db'; import { Accordion } from '@tuturuuu/ui/accordion'; +import { CourseSection } from '@tuturuuu/ui/custom/education/modules/content-section'; +import { YoutubeEmbed } from '@tuturuuu/ui/custom/education/modules/youtube/embed'; import { BookText, Goal, diff --git a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/youtube-links/embed.tsx b/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/youtube-links/embed.tsx deleted file mode 100644 index ca40c2a106..0000000000 --- a/apps/upskii/src/app/[locale]/(dashboard)/[wsId]/courses/[courseId]/modules/[moduleId]/youtube-links/embed.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useTranslations } from 'next-intl'; - -export const YoutubeEmbed = ({ embedId }: { embedId: string | undefined }) => { - const t = useTranslations(); - - return embedId ? ( -