Skip to content

Add page for tutorial for client#71

Merged
lgdlong merged 25 commits intodevfrom
68-add-page-for-tutorial-client
Sep 6, 2025
Merged

Add page for tutorial for client#71
lgdlong merged 25 commits intodevfrom
68-add-page-for-tutorial-client

Conversation

@lgdlong
Copy link
Copy Markdown
Owner

@lgdlong lgdlong commented Sep 5, 2025

Description

This pull request introduces several enhancements to the tutorials section, including the following changes:

  • TutorialPage Enhancements: Added syntax highlighting, estimated read time, and heading extraction for better user experience.
  • Async Handling: Updated TutorialPage to handle parameters as Promises for asynchronous capabilities.
  • CardSkeleton Component: Introduced a CardSkeleton component and updated TutorialsIndex for an improved loading state display.
  • Markdown Rendering: Added a dynamic tutorial page with support for Markdown rendering through dynamic routing.
  • Tutorial API Refinements: Extended the Tutorial interface with slug and isPublished fields, along with updates to API and DTOs to support slug-based lookups and improve consistency.
  • Seeding Data: Updated the database seeding with additional tutorial data entries.

Additional updates include:

  • Renaming CategoriePage to CategoriesPage for better naming consistency.
  • Dependency updates and cleanup, adding new libraries required for Markdown and syntax highlighting.
  • Removal of unused imports and test files for codebase optimization.

Checklist

  • Code changes are well-documented
  • Code has been tested with accompanying unit tests
  • Related issues are linked

PR Type

Enhancement, Other


Description

  • Add dynamic tutorial pages with markdown rendering

  • Implement slug-based routing and API endpoints

  • Create loading states with skeleton components

  • Enhance tutorial interface with new fields


Diagram Walkthrough

flowchart LR
  A["Tutorial API"] --> B["Slug-based Routing"]
  B --> C["Markdown Rendering"]
  C --> D["Dynamic Tutorial Page"]
  E["Loading States"] --> F["CardSkeleton Component"]
  G["Database"] --> H["Tutorial Entity Updates"]
Loading

File Walkthrough

Relevant files
Enhancement
14 files
tutorialApi.ts
Refactor API with slug endpoints and auth headers               
+65/-62 
tutorials.controller.ts
Add slug-based tutorial lookup endpoint                                   
+19/-35 
tutorials.service.ts
Implement slug generation and findOneBySlug method             
+46/-7   
tutorials.entity.ts
Add slug and isPublished fields to entity                               
+13/-14 
tutorial.ts
Update Tutorial interface with slug and isPublished           
+5/-3     
update-tutorials.dto.ts
Add transform decorators and validation improvements         
+3/-0     
tutorials.dto.ts
Extend DTOs with slug, views, and isPublished                       
+11/-0   
toc.ts
Create table of contents extraction utility                           
+23/-0   
slug.ts
Add slug generation helper function                                           
+22/-0   
page.tsx
Implement dynamic tutorial page with markdown rendering   
+273/-0 
TutorialsIndex.tsx
Create tutorials listing with search and skeleton loading
+115/-0 
hash-scroll.tsx
Add smooth scrolling for hash navigation                                 
+27/-0   
CardSkeleton.tsx
Create skeleton loading component for tutorial cards         
+13/-0   
page.tsx
Create tutorials index page with search params                     
+12/-0   
Miscellaneous
3 files
create-tutorials.dto.ts
Clean up imports and validation decorators                             
+2/-9     
tutorial.tsx
Create mockup tutorial page component                                       
+373/-0 
page.tsx
Rename CategoriePage to CategoriesPage for consistency     
+1/-1     
Configuration changes
1 files
globals.css
Add typography plugin and Prism theme imports                       
+5/-0     
Dependencies
2 files
package.json
Add markdown processing and syntax highlighting dependencies
+15/-0   
package.json
Add slugify dependency to root package                                     
+3/-0     
Additional files
3 files
tutorials.service.spec.ts +0/-115 
dump-dev_wiki_local-202509052111.dump [link]   
pnpm-lock.yaml +1369/-10

@lgdlong lgdlong self-assigned this Sep 5, 2025
@lgdlong lgdlong added the feature label Sep 5, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 5, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 68-add-page-for-tutorial-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@lgdlong lgdlong linked an issue Sep 5, 2025 that may be closed by this pull request
@qodo-code-review
Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ✅

68 - PR Code Verified

Compliant requirements:

  • Add a page for tutorial client with dynamic routing by slug
  • Render tutorial content with Markdown support and syntax highlighting
  • Implement loading states with skeleton components for tutorials listing
  • Extend Tutorial data model to include slug and published flag
  • Update API to support slug-based lookups and published endpoints
  • Enhance tutorials index with search/filter and basic metadata (read time optional)

Requires further human verification:

  • Verify end-to-end that slug collisions are handled gracefully in the UI when BE returns 400
  • Visual QA for dark/light mode, typography, and code highlighting across browsers
  • Validate accessibility (focus states, heading structure, link color contrast)
  • Confirm database migrations align with new entity fields (slug unique index, is_published, timestamps)
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Auth Headers

Some GET endpoints still include auth headers while others do not; verify BE requirements and consistency, and ensure Content-Type is set where JSON bodies are sent if fetcher does not add it.

export async function getTutorialTags(tutorialId: number): Promise<Tag[]> {
  return fetcher<Tag[]>(`/tutorials/${tutorialId}/tags`, {
    method: "GET",
    headers: getAuthHeaders(),
  });
}

/**
 * Get a published tutorial by ID
 * GET /tutorials/:id/published
 */
export async function getTutorialPublishedById(id: number): Promise<Tutorial> {
  return fetcher<Tutorial>(`/tutorials/${id}/published`, { method: "GET" });
}

/**
 * Get a published tutorial by slug
 * GET /tutorials/slug/:slug/published
 */
export async function getTutorialPublishedBySlug(slug: string): Promise<Tutorial> {
  return fetcher<Tutorial>(`/tutorials/slug/${slug}/published`, { method: "GET" });
}

// ========== POST, PATCH, DELETE API ==========
/**
 * Create a new tutorial
 * POST /tutorials
 */
export async function createTutorial(
  data: CreateTutorialRequest,
): Promise<Tutorial> {
  const body = {
    title: data.title,
    content: data.content,
    // only include author_id if present; avoid NaN
    ...(data.author_id !== undefined && data.author_id !== null
      ? { author_id: Number(data.author_id) }
      : {}),
    tags: data.tags,
  };

  return fetcher<Tutorial>("/tutorials", {
    method: "POST",
    body: JSON.stringify(body),
    headers: getAuthHeaders(),
  });
}
Error Handling

getTutorialBySlug likely throws on 404; current code assumes null. Add try/catch or handle thrown errors to route to notFound() reliably and avoid an unhandled rejection in RSC.

const { slug } = await params;

const tutorial: Tutorial | null = await getTutorialBySlug(slug);

if (!tutorial) notFound();
Slug Collision

Slug generated from title may change on update; update path lacks slug regeneration or uniqueness checks. Define behavior for title updates and slug immutability or provide update-time slug handling.

  if (finalAuthorId == null)
    throw new BadRequestException('author_id is required');

  // Validate dữ liệu (DTO đã trim, nhưng double-check)
  const titleTrimmed = dto.title.trim();
  const contentTrimmed = dto.content.trim();
  const slugTrimmed = generateSlug(titleTrimmed);

  // Kiểm tra trùng slug
  const duplicated = await this.repo.exists({
    where: { slug: slugTrimmed },
  });
  if (duplicated) {
    throw new BadRequestException(
      'A tutorial with a similar title already exists. Please choose a different title.',
    );
  }

  // Tạo entity & lưu vào DB
  const tutorial = this.repo.create({
    title: titleTrimmed,
    content: contentTrimmed,
    authorId: finalAuthorId, // map snake_case -> camelCase
    views: 0,
    slug: slugTrimmed,
    isPublished: true,
  });

  return await this.repo.save(tutorial);
} catch (e) {
  console.error('TutorialService.create error:', e);

@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review Bot commented Sep 5, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Enforce hashed password storage

Storing plaintext or weakly hashed passwords is a critical security risk. Ensure
password values are hashed using a strong algorithm and, at the DB level,
restrict exposure. Rename the column to a neutral name and add a comment to
enforce hashed content to prevent accidental misuse.

apps/db/dump-dev_wiki_local-202509052111.dump [68-78]

 CREATE TABLE public.accounts (
     id integer NOT NULL,
     email character varying NOT NULL,
     name character varying NOT NULL,
-    password character varying NOT NULL,
+    password_hash character varying NOT NULL,
     avatar_url character varying,
     role public.accounts_role_enum DEFAULT 'user'::public.accounts_role_enum NOT NULL,
     status public.accounts_status_enum DEFAULT 'active'::public.accounts_status_enum NOT NULL,
     "createdAt" timestamp without time zone DEFAULT now() NOT NULL,
     "updatedAt" timestamp without time zone DEFAULT now() NOT NULL
 );
 
+COMMENT ON COLUMN public.accounts.password_hash IS 'Store only strong, salted password hashes (e.g., Argon2/Bcrypt). Never store plaintext.';
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 10

__

Why: The suggestion correctly identifies a critical security risk with the password column and proposes renaming it to password_hash to enforce storing hashed passwords, which is a crucial security best practice.

High
High-level
Clarify slug publishing semantics

The backend unconditionally sets isPublished = true and auto-generates a unique
slug on create, while the API and types expose slug and isPublished as
client-visible fields. This can cause confusion and future inconsistencies if
clients expect to control publish state or supply a slug. Decide on a clear
contract: either keep slug/isPublished fully server-managed (remove them from
create/update DTOs and TS types) or allow client control and implement proper
validation, uniqueness checks, and update paths (including slug changes and
redirects).

Examples:

apps/web/src/types/tutorial.ts [17-22]
  title: string;
  content: string;
  author_id?: number;
  tags?: string[];
  slug?: string;
}
apps/api/src/modules/tutorials/tutorials.service.ts [39-64]
      // Validate dữ liệu (DTO đã trim, nhưng double-check)
      const titleTrimmed = dto.title.trim();
      const contentTrimmed = dto.content.trim();
      const slugTrimmed = generateSlug(titleTrimmed);

      // Kiểm tra trùng slug
      const duplicated = await this.repo.exists({
        where: { slug: slugTrimmed },
      });
      if (duplicated) {

 ... (clipped 16 lines)

Solution Walkthrough:

Before:

// File: apps/web/src/types/tutorial.ts
export interface CreateTutorialRequest {
  title: string;
  content: string;
  slug?: string; // Client can supposedly provide a slug
  // ...
}

// File: apps/api/src/modules/tutorials/tutorials.service.ts
class TutorialService {
  async create(dto: CreateTutorialDto, authorId: number) {
    // ...
    const slugTrimmed = generateSlug(dto.title.trim()); // Ignores any slug from client, generates new one
    const tutorial = this.repo.create({
      // ...
      slug: slugTrimmed,
      isPublished: true, // Always true, not client-controlled
    });
    return await this.repo.save(tutorial);
  }
}

After:

// File: apps/web/src/types/tutorial.ts
export interface CreateTutorialRequest {
  title: string;
  content: string;
  // slug field is removed, making it clear it's server-generated.
  // ...
}

// File: apps/api/src/modules/tutorials/tutorials.service.ts
class TutorialService {
  async create(dto: CreateTutorialDto, authorId: number) {
    // ...
    // The backend logic is now consistent with the clarified API contract.
    const slugTrimmed = generateSlug(dto.title.trim());
    const tutorial = this.repo.create({
      // ...
      slug: slugTrimmed,
      isPublished: true, // Publishing logic remains server-managed.
    });
    return await this.repo.save(tutorial);
  }
}
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a significant design inconsistency in the API contract between the frontend types and the backend implementation, which could lead to future bugs and developer confusion.

Medium
Possible issue
Add JSON content header back

Ensure Content-Type: application/json is included when sending a JSON body,
otherwise some servers may not parse the payload. Merge the auth headers with
the JSON header to avoid regressions from the previous implementation.

apps/web/src/utils/api/tutorialApi.ts [92-96]

 return fetcher<Tutorial>("/tutorials", {
   method: "POST",
   body: JSON.stringify(body),
-  headers: getAuthHeaders(),
+  headers: { "Content-Type": "application/json", ...getAuthHeaders() },
 });
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies that the Content-Type: application/json header was removed during refactoring, which is crucial for the server to parse the JSON body of the POST request and would likely cause the createTutorial call to fail.

Medium
Set JSON header for PATCH

Include Content-Type: application/json for PATCH requests with a JSON body to
prevent unexpected 415/400 errors. Merge it with the auth headers similarly to
POST.

apps/web/src/utils/api/tutorialApi.ts [107-111]

 return fetcher<Tutorial>(`/tutorials/${id}`, {
   method: "PATCH",
   body: JSON.stringify(data),
-  headers: getAuthHeaders(),
+  headers: { "Content-Type": "application/json", ...getAuthHeaders() },
 });
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly points out a regression where the Content-Type: application/json header was removed for a PATCH request with a JSON body, which would likely cause the updateTutorial API call to fail.

Medium
Specify JSON header for tags

Add Content-Type: application/json to this PATCH call since it also sends a JSON
body. Without it, backends may fail to parse tagIds.

apps/web/src/utils/api/tutorialApi.ts [133-137]

 return fetcher<{ success: boolean }>(`/tutorials/${tutorialId}/tags`, {
   method: "PATCH",
   body: JSON.stringify({ tagIds }),
-  headers: getAuthHeaders(),
+  headers: { "Content-Type": "application/json", ...getAuthHeaders() },
 });
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies that the Content-Type: application/json header is missing for a PATCH request with a JSON body, which is a regression that would likely cause the upsertTutorialTags API call to fail.

Medium
Add missing foreign key

uploader_id is a bigint but lacks a foreign key, risking orphaned references and
data inconsistency. Add a foreign key to the appropriate accounts/users table to
maintain referential integrity and enable cascades if needed.

apps/db/dump-dev_wiki_local-202509052111.dump [255-266]

 CREATE TABLE public.videos (
     id integer NOT NULL,
     youtube_id character varying NOT NULL,
     title text NOT NULL,
     description text,
     duration bigint,
     metadata jsonb,
     created_at timestamp without time zone DEFAULT now() NOT NULL,
     uploader_id bigint,
     channel_title text,
     thumbnail_url text
 );
 
+ALTER TABLE ONLY public.videos
+  ADD CONSTRAINT fk_videos_uploader
+  FOREIGN KEY (uploader_id) REFERENCES public.accounts(id);
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that uploader_id lacks a foreign key constraint, which is important for data integrity; however, the uploader_id is a bigint while the referenced accounts.id is an integer, causing a type mismatch.

Medium
Enforce polymorphic integrity

The comments table allows entity_id/entity_type polymorphism without
constraints, which can lead to orphaned references. Add partial foreign keys via
separate linking tables or enforce with triggers to validate entity_id against
the target table for each entity_type.

apps/db/dump-dev_wiki_local-202509052111.dump [111-121]

+-- Example approach using triggers to enforce referential integrity for polymorphic association
 CREATE TABLE public.comments (
     id integer NOT NULL,
     content text NOT NULL,
     author_id integer NOT NULL,
     parent_id integer,
     entity_type public.comments_entity_type_enum NOT NULL,
     entity_id bigint NOT NULL,
     upvotes bigint DEFAULT '0'::bigint NOT NULL,
     created_at timestamp without time zone DEFAULT now() NOT NULL,
     updated_at timestamp without time zone DEFAULT now() NOT NULL
 );
 
+-- Pseudocode: create a trigger function to validate entity references
+CREATE OR REPLACE FUNCTION public.validate_comment_entity() RETURNS trigger AS $$
+BEGIN
+  IF NEW.entity_type = 'tutorial' AND NOT EXISTS (SELECT 1 FROM public.tutorials t WHERE t.id = NEW.entity_id) THEN
+    RAISE EXCEPTION 'Invalid tutorial reference %', NEW.entity_id;
+  ELSIF NEW.entity_type = 'video' AND NOT EXISTS (SELECT 1 FROM public.videos v WHERE v.id = NEW.entity_id) THEN
+    RAISE EXCEPTION 'Invalid video reference %', NEW.entity_id;
+  ELSIF NEW.entity_type = 'product' AND NOT EXISTS (SELECT 1 FROM public.products p WHERE p.id = NEW.entity_id) THEN
+    RAISE EXCEPTION 'Invalid product reference %', NEW.entity_id;
+  END IF;
+  RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trg_validate_comment_entity
+  BEFORE INSERT OR UPDATE ON public.comments
+  FOR EACH ROW EXECUTE FUNCTION public.validate_comment_entity();
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a potential data integrity issue with the polymorphic association in the comments table and proposes a valid trigger-based solution, which improves data consistency.

Low
  • Update

@lgdlong lgdlong changed the title Enhance tutorial features, routing, and loading state Add page for tutorial for client Sep 5, 2025
@lgdlong lgdlong requested a review from Copilot September 5, 2025 20:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request introduces a comprehensive tutorial system enhancement with markdown rendering, slug-based routing, and improved user experience. The changes focus on creating dynamic tutorial pages with full markdown support and implementing better navigation patterns.

  • Adds dynamic tutorial pages with markdown rendering using ReactMarkdown and syntax highlighting
  • Implements slug-based routing and API endpoints for SEO-friendly URLs
  • Creates loading states with skeleton components for improved UX

Reviewed Changes

Copilot reviewed 21 out of 23 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tutorialApi.ts Refactors API with new slug-based endpoints and consolidated auth headers
tutorials.controller.ts Adds slug-based lookup endpoint for tutorials
tutorials.service.ts Implements slug generation and findOneBySlug method with duplicate checking
tutorials.entity.ts Extends entity with slug and isPublished fields
page.tsx (tutorial) Creates dynamic tutorial page with comprehensive markdown rendering
TutorialsIndex.tsx Implements tutorial listing with search and skeleton loading states
CardSkeleton.tsx Provides loading skeleton component for tutorial cards
package.json Adds markdown processing and syntax highlighting dependencies
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread apps/web/src/utils/api/tutorialApi.ts
Comment thread apps/web/src/utils/api/tutorialApi.ts
Comment thread apps/web/src/app/(public)/tutorials/[slug]/page.tsx Outdated
Comment thread apps/web/src/app/(public)/tutorials/[slug]/page.tsx
Comment thread apps/api/src/modules/tutorials/tutorials.service.ts Outdated
Comment thread apps/api/src/modules/tutorials/entities/tutorials.entity.ts
@lgdlong lgdlong merged commit 8d9289c into dev Sep 6, 2025
@lgdlong lgdlong linked an issue Sep 7, 2025 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FR-A5 Listing and Tutorial details Client Add page for tutorial client

2 participants