From 23aa8356c33217753bab42d81d7342615465675f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnau=20G=C3=B3mez?= Date: Sat, 28 Sep 2024 17:12:40 +0200 Subject: [PATCH 1/2] feat: create new admin models and config --- .../data/fixtures/admin-resources-data.ts | 0 .../domain/config/admin-resources-config.ts | 178 ++++++++++++++++++ .../domain/models/admin-resource-model.ts | 50 +++++ src/admin/view/i18n/admin-translations.ts | 4 + src/common/domain/models/option-model.ts | 4 + .../components/form/checkboxes-form-field.tsx | 5 +- .../ui/components/input/checkboxes-input.tsx | 7 +- 7 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 src/admin/data/fixtures/admin-resources-data.ts create mode 100644 src/admin/domain/config/admin-resources-config.ts create mode 100644 src/admin/domain/models/admin-resource-model.ts create mode 100644 src/admin/view/i18n/admin-translations.ts create mode 100644 src/common/domain/models/option-model.ts diff --git a/src/admin/data/fixtures/admin-resources-data.ts b/src/admin/data/fixtures/admin-resources-data.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/admin/domain/config/admin-resources-config.ts b/src/admin/domain/config/admin-resources-config.ts new file mode 100644 index 00000000..cc476e79 --- /dev/null +++ b/src/admin/domain/config/admin-resources-config.ts @@ -0,0 +1,178 @@ +import type { OptionModel } from "@/src/common/domain/models/option-model"; +import { PracticeCardRatingModel } from "@/src/practice/domain/models/practice-card-rating-model"; +import { PracticeCardStateModel } from "@/src/practice/domain/models/practice-card-state-model"; +import { + AdminFieldTypeModel, + AdminResourceTypeModel, + type AdminResourceModel, +} from "../models/admin-resource-model"; + +const practiceCardStateOptions: OptionModel[] = [ + { value: PracticeCardStateModel.learning, label: "Aprendiendo" }, + { value: PracticeCardStateModel.new, label: "Nueva" }, + { value: PracticeCardStateModel.relearning, label: "Reaprendiendo" }, + { value: PracticeCardStateModel.review, label: "Repasar" }, +]; + +const practiceCardRatingOptions: OptionModel[] = [ + { value: PracticeCardRatingModel.again, label: "Repetir" }, + { value: PracticeCardRatingModel.easy, label: "Fácil" }, + { value: PracticeCardRatingModel.good, label: "Bien" }, + { value: PracticeCardRatingModel.hard, label: "Difícil" }, + { value: PracticeCardRatingModel.manual, label: "Manual" }, +]; + +export const adminResourcesConfig: AdminResourceModel[] = [ + { + type: AdminResourceTypeModel.tags, + fields: [{ name: "name", type: AdminFieldTypeModel.string }], + }, + { + type: AdminResourceTypeModel.reviewLogs, + fields: [ + { name: "cardId", type: AdminFieldTypeModel.objectId }, + { name: "courseEnrollmentId", type: AdminFieldTypeModel.objectId }, + { + name: "rating", + type: AdminFieldTypeModel.select, + options: practiceCardRatingOptions, + }, + { + name: "state", + type: AdminFieldTypeModel.select, + options: practiceCardStateOptions, + }, + { name: "due", type: AdminFieldTypeModel.date }, + { name: "stability", type: AdminFieldTypeModel.number }, + { name: "difficulty", type: AdminFieldTypeModel.number }, + { name: "elapsedDays", type: AdminFieldTypeModel.number }, + { name: "lastElapsedDays", type: AdminFieldTypeModel.number }, + { name: "scheduledDays", type: AdminFieldTypeModel.number }, + { name: "review", type: AdminFieldTypeModel.date }, + ], + }, + { + type: AdminResourceTypeModel.rateLimits, + fields: [ + { name: "name", type: AdminFieldTypeModel.string }, + { name: "count", type: AdminFieldTypeModel.number }, + { name: "updatedAt", type: AdminFieldTypeModel.date }, + ], + }, + { + type: AdminResourceTypeModel.profiles, + fields: [ + { name: "userId", type: AdminFieldTypeModel.objectId }, + { name: "displayName", type: AdminFieldTypeModel.string }, + { name: "handle", type: AdminFieldTypeModel.string }, + { name: "bio", type: AdminFieldTypeModel.string }, + { name: "picture", type: AdminFieldTypeModel.string }, + { name: "backgroundPicture", type: AdminFieldTypeModel.string }, + { name: "website", type: AdminFieldTypeModel.string }, + { name: "isPublic", type: AdminFieldTypeModel.boolean }, + { name: "tags", type: AdminFieldTypeModel.tags }, + ], + }, + { + type: AdminResourceTypeModel.practiceCards, + fields: [ + { name: "courseEnrollmentId", type: AdminFieldTypeModel.objectId }, + { name: "noteId", type: AdminFieldTypeModel.objectId }, + { name: "due", type: AdminFieldTypeModel.date }, + { name: "stability", type: AdminFieldTypeModel.number }, + { name: "difficulty", type: AdminFieldTypeModel.number }, + { name: "elapsedDays", type: AdminFieldTypeModel.number }, + { name: "scheduledDays", type: AdminFieldTypeModel.number }, + { name: "reps", type: AdminFieldTypeModel.number }, + { name: "lapses", type: AdminFieldTypeModel.number }, + { + name: "state", + type: AdminFieldTypeModel.select, + options: practiceCardStateOptions, + }, + { name: "lastReview", type: AdminFieldTypeModel.date }, + ], + }, + + { + type: AdminResourceTypeModel.notes, + fields: [ + { name: "courseId", type: AdminFieldTypeModel.objectId }, + { name: "front", type: AdminFieldTypeModel.string }, + { name: "back", type: AdminFieldTypeModel.string }, + { name: "createdAt", type: AdminFieldTypeModel.date }, + ], + }, + { + type: AdminResourceTypeModel.forgotPasswordTokens, + fields: [ + { name: "userId", type: AdminFieldTypeModel.objectId }, + { name: "tokenHash", isReadonly: true, type: AdminFieldTypeModel.string }, + { name: "expiresAt", type: AdminFieldTypeModel.date }, + ], + }, + { + type: AdminResourceTypeModel.emailVerificationCodes, + fields: [ + { name: "userId", type: AdminFieldTypeModel.objectId }, + { name: "code", type: AdminFieldTypeModel.string }, + { name: "expiresAt", type: AdminFieldTypeModel.date }, + ], + }, + { + type: AdminResourceTypeModel.courseEnrollments, + fields: [ + { name: "courseId", type: AdminFieldTypeModel.objectId }, + { name: "profileId", type: AdminFieldTypeModel.objectId }, + { name: "isFavorite", type: AdminFieldTypeModel.boolean }, + { + name: "config", + type: AdminFieldTypeModel.form, + fields: [ + { name: "enableFuzz", type: AdminFieldTypeModel.boolean }, + { name: "maximumInterval", type: AdminFieldTypeModel.number }, + { name: "requestRetention", type: AdminFieldTypeModel.number }, + { name: "dailyNewCardsCount", type: AdminFieldTypeModel.number }, + { + name: "showAdvancedRatingOptions", + type: AdminFieldTypeModel.boolean, + }, + ], + }, + ], + }, + { + type: AdminResourceTypeModel.coursePermissions, + fields: [ + { name: "courseId", type: AdminFieldTypeModel.objectId }, + { name: "profileId", type: AdminFieldTypeModel.objectId }, + { name: "permissionType", type: AdminFieldTypeModel.string }, + ], + }, + { + type: AdminResourceTypeModel.courses, + fields: [ + { name: "name", type: AdminFieldTypeModel.string }, + { name: "description", type: AdminFieldTypeModel.string }, + { name: "picture", type: AdminFieldTypeModel.string }, + { name: "isPublic", type: AdminFieldTypeModel.boolean }, + { name: "tags", type: AdminFieldTypeModel.tags }, + ], + }, + { + type: AdminResourceTypeModel.sessions, + fields: [ + { name: "expires_at", type: AdminFieldTypeModel.date }, + { name: "user_id", type: AdminFieldTypeModel.objectId }, + ], + }, + { + type: AdminResourceTypeModel.users, + fields: [ + { name: "email", type: AdminFieldTypeModel.string }, + { name: "acceptTerms", type: AdminFieldTypeModel.boolean }, + { name: "isEmailVerified", type: AdminFieldTypeModel.boolean }, + { name: "isAdmin", type: AdminFieldTypeModel.boolean }, + ], + }, +]; diff --git a/src/admin/domain/models/admin-resource-model.ts b/src/admin/domain/models/admin-resource-model.ts new file mode 100644 index 00000000..40ae2b96 --- /dev/null +++ b/src/admin/domain/models/admin-resource-model.ts @@ -0,0 +1,50 @@ +import type { OptionModel } from "@/src/common/domain/models/option-model"; + +export enum AdminResourceTypeModel { + courseEnrollments = "courseEnrollments", + coursePermissions = "coursePermissions", + courses = "courses", + emailVerificationCodes = "emailVerificationCodes", + fileUploads = "fileUploads", + forgotPasswordTokens = "forgotPasswordTokens", + notes = "notes", + practiceCards = "practiceCards", + profiles = "profiles", + rateLimits = "rateLimits", + reviewLogs = "reviewLogs", + sessions = "sessions", + tags = "tags", + users = "users", +} + +export interface AdminResourceModel { + type: AdminResourceTypeModel; + fields: AdminFieldModel[]; +} + +interface AdminFieldModel { + name: string; + isReadonly?: boolean; + type: AdminFieldTypeModel; + /** + * Sub-fields of the field. Used when the field type is `AdminFieldTypeModel.form`. + */ + fields?: AdminFieldModel[]; + + /** + * Options for the field. Used when the field type is `AdminFieldTypeModel.select` or similar. + */ + options?: OptionModel[]; +} + +export enum AdminFieldTypeModel { + boolean = "boolean", + date = "date", + form = "form", + number = "number", + objectId = "objectId", + string = "string", + tags = "tags", + richText = "richText", + select = "select", +} diff --git a/src/admin/view/i18n/admin-translations.ts b/src/admin/view/i18n/admin-translations.ts new file mode 100644 index 00000000..f6259b6e --- /dev/null +++ b/src/admin/view/i18n/admin-translations.ts @@ -0,0 +1,4 @@ +const adminTranslations: Record = {}; + +export const translateAdminKey = (key: string): string => + adminTranslations[key] || key; diff --git a/src/common/domain/models/option-model.ts b/src/common/domain/models/option-model.ts new file mode 100644 index 00000000..6a4e1826 --- /dev/null +++ b/src/common/domain/models/option-model.ts @@ -0,0 +1,4 @@ +export interface OptionModel { + label: string; + value: string; +} diff --git a/src/common/ui/components/form/checkboxes-form-field.tsx b/src/common/ui/components/form/checkboxes-form-field.tsx index 6817c906..53de05e3 100644 --- a/src/common/ui/components/form/checkboxes-form-field.tsx +++ b/src/common/ui/components/form/checkboxes-form-field.tsx @@ -1,4 +1,5 @@ -import { CheckboxesInput, type Option } from "../input/checkboxes-input"; +import type { OptionModel } from "@/src/common/domain/models/option-model"; +import { CheckboxesInput } from "../input/checkboxes-input"; import { FormControl, FormField, @@ -10,7 +11,7 @@ interface CheckboxesFormFieldProps { name: string; label: string; description: string; - options: Option[]; + options: OptionModel[]; } export function CheckboxesFormField({ diff --git a/src/common/ui/components/input/checkboxes-input.tsx b/src/common/ui/components/input/checkboxes-input.tsx index bff9c4b9..4d91ea10 100644 --- a/src/common/ui/components/input/checkboxes-input.tsx +++ b/src/common/ui/components/input/checkboxes-input.tsx @@ -1,14 +1,11 @@ +import type { OptionModel } from "@/src/common/domain/models/option-model"; import { FormDescription, FormLabel } from "../shadcn/ui/form"; import { CheckboxInput } from "./checkbox-input"; -export interface Option { - label: string; - value: string; -} interface CheckboxesInputProps { name: string; value: string[]; - options: Option[]; + options: OptionModel[]; onChange: (value: string[]) => void; label?: string; description?: string; From b9ac45ec588cbbcbd03ebba88b5e025cff004034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnau=20G=C3=B3mez?= Date: Sat, 28 Sep 2024 18:19:46 +0200 Subject: [PATCH 2/2] feat: admin home page --- .../courses/detail/[id]/export/anki/route.ts | 0 .../courses/detail/[id]/export/csv/route.ts | 0 .../courses/detail/[id]/export/json/route.ts | 0 .../courses/detail/[id]/page.tsx | 0 .../courses/detail/[id]/practice/loading.tsx | 0 .../courses/detail/[id]/practice/page.tsx | 0 .../error.tsx | 0 .../layout.tsx | 2 +- .../(login-guard)/admin/layout.tsx | 23 +++++++++ .../(login-guard)/admin/page.tsx | 3 ++ app/(navbar-layout)/(login-guard)/layout.tsx | 5 +- app/(navbar-layout)/error.tsx | 2 +- app/auth/error.tsx | 2 +- app/global-error.tsx | 2 +- app/not-found.tsx | 6 +-- src/admin/ui/components/admin-greeting.tsx | 20 ++++++++ .../ui/components/admin-resource-card.tsx | 30 +++++++++++ .../ui/components/admin-resource-icon.tsx | 51 +++++++++++++++++++ .../ui/components/admin-resources-section.tsx | 19 +++++++ src/admin/ui/i18n/admin-translations.ts | 6 +++ src/admin/ui/pages/admin-page.tsx | 15 ++++++ src/admin/view/i18n/admin-translations.ts | 4 -- .../components/steps/practice-step-finish.tsx | 2 +- 23 files changed, 177 insertions(+), 15 deletions(-) rename app/{(admin-layout) => (fullscreen-layout)}/courses/detail/[id]/export/anki/route.ts (100%) rename app/{(admin-layout) => (fullscreen-layout)}/courses/detail/[id]/export/csv/route.ts (100%) rename app/{(admin-layout) => (fullscreen-layout)}/courses/detail/[id]/export/json/route.ts (100%) rename app/{(admin-layout) => (fullscreen-layout)}/courses/detail/[id]/page.tsx (100%) rename app/{(admin-layout) => (fullscreen-layout)}/courses/detail/[id]/practice/loading.tsx (100%) rename app/{(admin-layout) => (fullscreen-layout)}/courses/detail/[id]/practice/page.tsx (100%) rename app/{(admin-layout) => (fullscreen-layout)}/error.tsx (100%) rename app/{(admin-layout) => (fullscreen-layout)}/layout.tsx (91%) create mode 100644 app/(navbar-layout)/(login-guard)/admin/layout.tsx create mode 100644 app/(navbar-layout)/(login-guard)/admin/page.tsx create mode 100644 src/admin/ui/components/admin-greeting.tsx create mode 100644 src/admin/ui/components/admin-resource-card.tsx create mode 100644 src/admin/ui/components/admin-resource-icon.tsx create mode 100644 src/admin/ui/components/admin-resources-section.tsx create mode 100644 src/admin/ui/i18n/admin-translations.ts create mode 100644 src/admin/ui/pages/admin-page.tsx delete mode 100644 src/admin/view/i18n/admin-translations.ts diff --git a/app/(admin-layout)/courses/detail/[id]/export/anki/route.ts b/app/(fullscreen-layout)/courses/detail/[id]/export/anki/route.ts similarity index 100% rename from app/(admin-layout)/courses/detail/[id]/export/anki/route.ts rename to app/(fullscreen-layout)/courses/detail/[id]/export/anki/route.ts diff --git a/app/(admin-layout)/courses/detail/[id]/export/csv/route.ts b/app/(fullscreen-layout)/courses/detail/[id]/export/csv/route.ts similarity index 100% rename from app/(admin-layout)/courses/detail/[id]/export/csv/route.ts rename to app/(fullscreen-layout)/courses/detail/[id]/export/csv/route.ts diff --git a/app/(admin-layout)/courses/detail/[id]/export/json/route.ts b/app/(fullscreen-layout)/courses/detail/[id]/export/json/route.ts similarity index 100% rename from app/(admin-layout)/courses/detail/[id]/export/json/route.ts rename to app/(fullscreen-layout)/courses/detail/[id]/export/json/route.ts diff --git a/app/(admin-layout)/courses/detail/[id]/page.tsx b/app/(fullscreen-layout)/courses/detail/[id]/page.tsx similarity index 100% rename from app/(admin-layout)/courses/detail/[id]/page.tsx rename to app/(fullscreen-layout)/courses/detail/[id]/page.tsx diff --git a/app/(admin-layout)/courses/detail/[id]/practice/loading.tsx b/app/(fullscreen-layout)/courses/detail/[id]/practice/loading.tsx similarity index 100% rename from app/(admin-layout)/courses/detail/[id]/practice/loading.tsx rename to app/(fullscreen-layout)/courses/detail/[id]/practice/loading.tsx diff --git a/app/(admin-layout)/courses/detail/[id]/practice/page.tsx b/app/(fullscreen-layout)/courses/detail/[id]/practice/page.tsx similarity index 100% rename from app/(admin-layout)/courses/detail/[id]/practice/page.tsx rename to app/(fullscreen-layout)/courses/detail/[id]/practice/page.tsx diff --git a/app/(admin-layout)/error.tsx b/app/(fullscreen-layout)/error.tsx similarity index 100% rename from app/(admin-layout)/error.tsx rename to app/(fullscreen-layout)/error.tsx diff --git a/app/(admin-layout)/layout.tsx b/app/(fullscreen-layout)/layout.tsx similarity index 91% rename from app/(admin-layout)/layout.tsx rename to app/(fullscreen-layout)/layout.tsx index 5d98cec3..c65696bb 100644 --- a/app/(admin-layout)/layout.tsx +++ b/app/(fullscreen-layout)/layout.tsx @@ -4,7 +4,7 @@ import { Navbar } from "@/src/common/ui/navbar/components/navbar"; * Layout for pages that have a fixed size that is equal to the width and height * of the browser screen. The main body does not have scroll in these pages. */ -export default function AdminLayout({ +export default function FullscreenLayout({ children, }: Readonly<{ children: React.ReactNode; diff --git a/app/(navbar-layout)/(login-guard)/admin/layout.tsx b/app/(navbar-layout)/(login-guard)/admin/layout.tsx new file mode 100644 index 00000000..8c7ebaec --- /dev/null +++ b/app/(navbar-layout)/(login-guard)/admin/layout.tsx @@ -0,0 +1,23 @@ +import { fetchSession } from "@/src/auth/ui/fetch/fetch-session"; +import { redirect } from "next/navigation"; +import type { PropsWithChildren } from "react"; + +/** + * Checks that the user is an admin. Otherwise, it redirects to the home page. + */ +async function adminGuard() { + const result = await fetchSession(); + if (!result.user?.isAdmin) { + redirect("/home"); + } +} + +/** + * Applies the `loginGuard` guard to the pages inside this layout + */ +export default async function AdminGuardLayout({ + children, +}: PropsWithChildren) { + await adminGuard(); + return <>{children}; +} diff --git a/app/(navbar-layout)/(login-guard)/admin/page.tsx b/app/(navbar-layout)/(login-guard)/admin/page.tsx new file mode 100644 index 00000000..485722b5 --- /dev/null +++ b/app/(navbar-layout)/(login-guard)/admin/page.tsx @@ -0,0 +1,3 @@ +import { AdminPage } from "@/src/admin/ui/pages/admin-page"; + +export default AdminPage; diff --git a/app/(navbar-layout)/(login-guard)/layout.tsx b/app/(navbar-layout)/(login-guard)/layout.tsx index d15fdb8e..cb28bec5 100644 --- a/app/(navbar-layout)/(login-guard)/layout.tsx +++ b/app/(navbar-layout)/(login-guard)/layout.tsx @@ -1,5 +1,6 @@ import { fetchSession } from "@/src/auth/ui/fetch/fetch-session"; import { redirect } from "next/navigation"; +import type { PropsWithChildren } from "react"; /** * Checks that the user is logged in and has verified the email. Otherwise, it @@ -20,9 +21,7 @@ async function loginGuard() { */ export default async function LoginGuardLayout({ children, -}: Readonly<{ - children: React.ReactNode; -}>) { +}: PropsWithChildren) { await loginGuard(); return <>{children}; } diff --git a/app/(navbar-layout)/error.tsx b/app/(navbar-layout)/error.tsx index cccec912..750f2ecc 100644 --- a/app/(navbar-layout)/error.tsx +++ b/app/(navbar-layout)/error.tsx @@ -1,6 +1,6 @@ "use client"; import type { ErrorPageProps } from "@/src/common/ui/models/props-with-error"; -import ErrorPage from "../(admin-layout)/error"; +import ErrorPage from "../(fullscreen-layout)/error"; /** * Displays a page with an error message. Is shown when an error occurs in the pages diff --git a/app/auth/error.tsx b/app/auth/error.tsx index 42d7c1ca..8e02c2d8 100644 --- a/app/auth/error.tsx +++ b/app/auth/error.tsx @@ -1,6 +1,6 @@ "use client"; import type { ErrorPageProps } from "@/src/common/ui/models/props-with-error"; -import ErrorPage from "../(admin-layout)/error"; +import ErrorPage from "../(fullscreen-layout)/error"; /** * Displays a page with an error message. It is shown when an error occurs in diff --git a/app/global-error.tsx b/app/global-error.tsx index bc8c356a..d9069c2b 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -3,7 +3,7 @@ import type { ErrorPageProps } from "@/src/common/ui/models/props-with-error"; import { inter } from "@/src/common/ui/styles/fonts"; import { cn } from "@/src/common/ui/utils/shadcn"; -import ErrorPage from "./(admin-layout)/error"; +import ErrorPage from "./(fullscreen-layout)/error"; /** * Error page that is shown when an error occurs in the `RootLayout` component. diff --git a/app/not-found.tsx b/app/not-found.tsx index 587ea9a9..c18aadf1 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -3,14 +3,14 @@ import { textStyles } from "@/src/common/ui/styles/text-styles"; import { cn } from "@/src/common/ui/utils/shadcn"; import { SearchX } from "lucide-react"; import Link from "next/link"; -import AdminLayout from "./(admin-layout)/layout"; +import FullscreenLayout from "./(fullscreen-layout)/layout"; /** * This page is shown when a user tries to access a route that does not exist. */ export default function NotFoundPage() { return ( - +
@@ -24,6 +24,6 @@ export default function NotFoundPage() {
-
+ ); } diff --git a/src/admin/ui/components/admin-greeting.tsx b/src/admin/ui/components/admin-greeting.tsx new file mode 100644 index 00000000..4d36af88 --- /dev/null +++ b/src/admin/ui/components/admin-greeting.tsx @@ -0,0 +1,20 @@ +import { textStyles } from "@/src/common/ui/styles/text-styles"; +import { SlidersHorizontalIcon } from "lucide-react"; + +export function AdminGreeting() { + return ( +
+
+

+ + Panel de administración +

+
+

+ Bienvenido al panel de administración. Desde aquí puedes gestionar los + cursos, usuarios y configuración de la plataforma. +

+
+
+ ); +} diff --git a/src/admin/ui/components/admin-resource-card.tsx b/src/admin/ui/components/admin-resource-card.tsx new file mode 100644 index 00000000..b8fde110 --- /dev/null +++ b/src/admin/ui/components/admin-resource-card.tsx @@ -0,0 +1,30 @@ +import { textStyles } from "@/src/common/ui/styles/text-styles"; +import Link from "next/link"; +import type { AdminResourceModel } from "../../domain/models/admin-resource-model"; +import { translateAdminKey } from "../i18n/admin-translations"; +import { AdminResourceIcon } from "./admin-resource-icon"; + +interface AdminResourceCardProps { + resource: AdminResourceModel; +} +export function AdminResourceCard({ resource }: AdminResourceCardProps) { + return ( + + +
+

+ {translateAdminKey(resource.type, "label")} +

+
+

+ {translateAdminKey(resource.type, "description")} +

+ + ); +} diff --git a/src/admin/ui/components/admin-resource-icon.tsx b/src/admin/ui/components/admin-resource-icon.tsx new file mode 100644 index 00000000..cd947ffe --- /dev/null +++ b/src/admin/ui/components/admin-resource-icon.tsx @@ -0,0 +1,51 @@ +import type { LucideProps } from "lucide-react"; +import { + BanIcon, + BookIcon, + BookUserIcon, + CalendarClockIcon, + IdCardIcon, + KeyRoundIcon, + LogsIcon, + MailCheckIcon, + MailQuestionIcon, + NotebookPenIcon, + TagIcon, + TicketIcon, + UploadIcon, + UserIcon, +} from "lucide-react"; +import type { ForwardRefExoticComponent } from "react"; +import { AdminResourceTypeModel } from "../../domain/models/admin-resource-model"; + +const map: Record< + AdminResourceTypeModel, + ForwardRefExoticComponent> +> = { + [AdminResourceTypeModel.tags]: TagIcon, + [AdminResourceTypeModel.reviewLogs]: LogsIcon, + [AdminResourceTypeModel.rateLimits]: BanIcon, + [AdminResourceTypeModel.courseEnrollments]: TicketIcon, + [AdminResourceTypeModel.coursePermissions]: BookUserIcon, + [AdminResourceTypeModel.courses]: BookIcon, + [AdminResourceTypeModel.emailVerificationCodes]: MailCheckIcon, + [AdminResourceTypeModel.fileUploads]: UploadIcon, + [AdminResourceTypeModel.forgotPasswordTokens]: MailQuestionIcon, + [AdminResourceTypeModel.notes]: NotebookPenIcon, + [AdminResourceTypeModel.practiceCards]: CalendarClockIcon, + [AdminResourceTypeModel.profiles]: UserIcon, + [AdminResourceTypeModel.sessions]: KeyRoundIcon, + [AdminResourceTypeModel.users]: IdCardIcon, +}; + +interface AdminResourceIconProps extends Omit { + adminResourceType: AdminResourceTypeModel; +} + +export const AdminResourceIcon = ({ + adminResourceType, + ...props +}: AdminResourceIconProps) => { + const Icon = map[adminResourceType]; + return ; +}; diff --git a/src/admin/ui/components/admin-resources-section.tsx b/src/admin/ui/components/admin-resources-section.tsx new file mode 100644 index 00000000..60e64a75 --- /dev/null +++ b/src/admin/ui/components/admin-resources-section.tsx @@ -0,0 +1,19 @@ +import { textStyles } from "@/src/common/ui/styles/text-styles"; +import { adminResourcesConfig } from "../../domain/config/admin-resources-config"; +import { AdminResourceCard } from "./admin-resource-card"; + +export function AdminResourcesSection() { + return ( +
+
+

Gestión de recursos

+
+
+ {adminResourcesConfig.map((resource) => ( + + ))} +
+
+
+ ); +} diff --git a/src/admin/ui/i18n/admin-translations.ts b/src/admin/ui/i18n/admin-translations.ts new file mode 100644 index 00000000..682e914f --- /dev/null +++ b/src/admin/ui/i18n/admin-translations.ts @@ -0,0 +1,6 @@ +const adminTranslations: Record = {}; + +export function translateAdminKey(...keys: string[]): string { + const key = keys.join("."); + return adminTranslations[key] || key; +} diff --git a/src/admin/ui/pages/admin-page.tsx b/src/admin/ui/pages/admin-page.tsx new file mode 100644 index 00000000..0154eefd --- /dev/null +++ b/src/admin/ui/pages/admin-page.tsx @@ -0,0 +1,15 @@ +import { AdminGreeting } from "../components/admin-greeting"; +import { AdminResourcesSection } from "../components/admin-resources-section"; + +export function AdminPage() { + return ( +
+
+ + +
+ + +
+ ); +} diff --git a/src/admin/view/i18n/admin-translations.ts b/src/admin/view/i18n/admin-translations.ts deleted file mode 100644 index f6259b6e..00000000 --- a/src/admin/view/i18n/admin-translations.ts +++ /dev/null @@ -1,4 +0,0 @@ -const adminTranslations: Record = {}; - -export const translateAdminKey = (key: string): string => - adminTranslations[key] || key; diff --git a/src/practice/ui/components/steps/practice-step-finish.tsx b/src/practice/ui/components/steps/practice-step-finish.tsx index 1d072dc6..d4dc5a06 100644 --- a/src/practice/ui/components/steps/practice-step-finish.tsx +++ b/src/practice/ui/components/steps/practice-step-finish.tsx @@ -1,4 +1,4 @@ -import PracticeLoadingPage from "@/app/(admin-layout)/courses/detail/[id]/practice/loading"; +import PracticeLoadingPage from "@/app/(fullscreen-layout)/courses/detail/[id]/practice/loading"; import { Button } from "@/src/common/ui/components/shadcn/ui/button"; import { textStyles } from "@/src/common/ui/styles/text-styles"; import { cn } from "@/src/common/ui/utils/shadcn";