-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #126 from arnaugomez:feature/CM-151
Feature/CM-151
- Loading branch information
Showing
28 changed files
with
414 additions
and
18 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}</>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { AdminPage } from "@/src/admin/ui/pages/admin-page"; | ||
|
||
export default AdminPage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }, | ||
], | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { textStyles } from "@/src/common/ui/styles/text-styles"; | ||
import { SlidersHorizontalIcon } from "lucide-react"; | ||
|
||
export function AdminGreeting() { | ||
return ( | ||
<div className="px-4"> | ||
<div className="mx-auto max-w-screen-lg"> | ||
<h1 className={textStyles.h2}> | ||
<SlidersHorizontalIcon className="mr-3 inline size-8 -translate-y-1" /> | ||
Panel de administración | ||
</h1> | ||
<div className="h-2" /> | ||
<p className={textStyles.muted}> | ||
Bienvenido al panel de administración. Desde aquí puedes gestionar los | ||
cursos, usuarios y configuración de la plataforma. | ||
</p> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Link | ||
href={`/admin/resources/${resource.type}`} | ||
className="block rounded-lg border border-gray-200 p-4 shadow-sm hover:bg-gray-50" | ||
> | ||
<AdminResourceIcon | ||
adminResourceType={resource.type} | ||
className="text-gray-500" | ||
/> | ||
<div className="h-2" /> | ||
<h4 className={textStyles.h4}> | ||
{translateAdminKey(resource.type, "label")} | ||
</h4> | ||
<div className="h-1" /> | ||
<p className={textStyles.muted}> | ||
{translateAdminKey(resource.type, "description")} | ||
</p> | ||
</Link> | ||
); | ||
} |
Oops, something went wrong.