Skip to content

Implement all basic public page#44

Merged
igun997 merged 11 commits into
cds-id:developfrom
inact25:develop
Dec 16, 2025
Merged

Implement all basic public page#44
igun997 merged 11 commits into
cds-id:developfrom
inact25:develop

Conversation

@inact25

@inact25 inact25 commented Dec 15, 2025

Copy link
Copy Markdown
Collaborator

User description

  • implement feature page
  • implement pricing page
  • implement showcase page
  • implement about page
  • implement blog page
  • implement career page
  • implement contact page

PR Type

Enhancement, Documentation


Description

  • Implemented 10 new public pages: features, pricing, showcase, careers, contact, about, blog, privacy, terms, and security

  • Integrated Ghost CMS blog functionality with BlogGrid, BlogPagination components and server-side data fetching

  • Added comprehensive blog component documentation and setup instructions for Ghost CMS API integration

  • Configured image domains for Ghost CMS (blog.javapixa.com, images.unsplash.com, static.ghost.org)

  • Added axios dependency for HTTP requests to Ghost CMS API

  • Updated project documentation (README.md and CLAUDE.md) with new routes, environment variables, and Phase 6 completion

  • Added environment variables for Ghost CMS configuration (NEXT_PUBLIC_GHOST_URL, NEXT_PUBLIC_TOKEN)

  • Implemented loading skeleton UI for blog page with animated placeholders


Diagram Walkthrough

flowchart LR
  A["Public Pages<br/>features, pricing, showcase<br/>careers, contact, about<br/>privacy, terms, security"]
  B["Blog Integration<br/>Ghost CMS API<br/>BlogGrid, BlogPagination"]
  C["Configuration<br/>Image domains<br/>Environment variables<br/>Dependencies"]
  D["Documentation<br/>README.md<br/>CLAUDE.md<br/>Blog component docs"]
  
  A -- "displays" --> E["User-facing<br/>website content"]
  B -- "powers" --> E
  C -- "enables" --> B
  D -- "documents" --> A
  D -- "documents" --> B
Loading

File Walkthrough

Relevant files
Configuration changes
2 files
next.config.ts
Configure image domains for Ghost CMS blog integration     

next.config.ts

  • Added three new image domain configurations for Ghost CMS blog
    integration
  • Configured blog.javapixa.com for Ghost CMS blog images
  • Added images.unsplash.com for Unsplash images used by Ghost
  • Added static.ghost.org for Ghost CDN assets
+15/-0   
.env.example
Environment Variables for Ghost CMS Integration                   

.env.example

  • Added Ghost Blog API configuration variables: NEXT_PUBLIC_GHOST_URL
    and NEXT_PUBLIC_TOKEN
  • Provided example values and comments for Ghost CMS integration setup
+4/-0     
Enhancement
14 files
index.ts
Add blog component exports and barrel file                             

src/components/blog/index.ts

  • Created new barrel export file for blog components
  • Exports BlogGrid component for displaying blog posts
  • Exports BlogPagination component for pagination controls
+2/-0     
page.tsx
Implement security documentation public page                         

src/app/security/page.tsx

  • Implemented comprehensive security documentation page with 7 major
    sections
  • Covers encryption, blockchain security, access control,
    infrastructure, application security, compliance, and AI/fraud
    detection
  • Includes security stats display, certifications section, and
    responsible disclosure policy
  • Features contact section for security inquiries with email link
+381/-0 
page.tsx
Implement Terms & Conditions public page                                 

src/app/terms/page.tsx

  • Created Terms & Conditions page with 8 comprehensive sections
  • Covers acceptance, account registration, service usage, prohibited
    activities, third-party services, pricing, liability, and intellectual
    property
  • Includes legal jurisdiction information and contact details
  • Displays last update date dynamically
+307/-0 
page.tsx
Implement showcase page with success stories                         

src/app/showcase/page.tsx

  • Created showcase page displaying success stories from 5 different
    industries
  • Includes achievement statistics (1M+ verified tags, 100+ brand
    partners, 50+ countries)
  • Features testimonials from brand representatives
  • Displays case studies with industry-specific metrics and use cases
+355/-0 
page.tsx
Implement careers page with job listings                                 

src/app/careers/page.tsx

  • Implemented careers page with 6 open job positions across engineering
    and design
  • Displays company values (Integrity, Innovation, Collaboration,
    Excellence)
  • Lists benefits and perks including competitive salary, health
    insurance, remote work
  • Includes CTA for CV submission and job application links
+351/-0 
page.tsx
Implement contact page with form integration                         

src/app/contact/page.tsx

  • Created contact page with contact form and multiple contact channels
  • Displays three contact reason categories (Sales & Demo, Support,
    Partnership)
  • Shows contact information, operating hours, and response time
    expectations
  • Includes form validation and submission handling
+341/-0 
page.tsx
Implement features showcase page                                                 

src/app/features/page.tsx

  • Implemented features showcase page displaying 12 core platform
    features
  • Includes blockchain stamping, QR code generation, AI fraud detection,
    NFT collectibles
  • Lists 6 technology integrations (Cloudflare R2, MetaMask, Mapbox,
    Prisma, NextAuth, Swagger)
  • Displays 8 key benefits with checkmark indicators
+309/-0 
page.tsx
Implement about page with company information                       

src/app/about/page.tsx

  • Created about page with mission, vision, and company values
  • Displays technology stack used (blockchain, smart contracts, AI/ML,
    frameworks)
  • Features team member profiles with GitHub links for 4 developers
  • Includes company statistics (99.9% uptime, <1 second verification,
    gas-free NFT minting)
+276/-0 
page.tsx
Implement pricing page with subscription plans                     

src/app/pricing/page.tsx

  • Implemented pricing page with three subscription tiers (Starter,
    Professional, Enterprise)
  • Starter plan is free with 1,000 tags/month limit
  • Professional and Enterprise plans have custom pricing with detailed
    feature lists
  • Includes FAQ section with 4 common questions and features comparison
    table
+303/-0 
page.tsx
Privacy Policy Page Implementation with Animated Sections

src/app/privacy/page.tsx

  • Created a new Privacy Policy page component with comprehensive
    sections covering data collection, usage, security, and user rights
  • Implemented Indonesian-language privacy policy with 6 main sections
    (information collection, usage, security, third-party sharing, user
    rights, cookies & tracking)
  • Added animated hero section and styled content cards with icons using
    framer-motion and Tailwind CSS
  • Integrated Navbar and Footer components with background gradient
    effects
+250/-0 
BlogPagination.tsx
Blog Pagination Component with Smart Page Display               

src/components/blog/BlogPagination.tsx

  • Created pagination component for blog posts with previous/next
    navigation buttons
  • Implemented intelligent page number generation showing up to 7 visible
    pages with ellipsis for large page counts
  • Added smooth scroll-to-top behavior when navigating between pages
  • Integrated URL parameter management to persist pagination state in
    query strings
+141/-0 
page.tsx
Blog Page with Ghost CMS Integration                                         

src/app/blog/page.tsx

  • Created main blog page that fetches posts from Ghost CMS API using
    NEXT_PUBLIC_TOKEN environment variable
  • Implemented server-side data fetching with error handling and
    pagination support (12 posts per page)
  • Added metadata for SEO with Indonesian description
  • Integrated BlogGrid and BlogPagination components with conditional
    rendering based on data availability
+125/-0 
BlogGrid.tsx
Blog Grid Component with Post Cards                                           

src/components/blog/BlogGrid.tsx

  • Created responsive blog post grid component displaying posts in 2-4
    columns based on screen size
  • Implemented post card design with featured image, publication date,
    title, excerpt, and read more link
  • Added empty state message when no posts are available
  • Included image optimization with hover scale effect and fallback
    placeholder with Etags branding
+106/-0 
loading.tsx
Blog Page Loading Skeleton UI                                                       

src/app/blog/loading.tsx

  • Created loading skeleton component for blog page with animated
    placeholder elements
  • Implemented skeleton loaders for hero section and 6 blog post cards
  • Used consistent styling with background effects matching the main blog
    page design
+50/-0   
Documentation
3 files
README.md
Update documentation for public pages and blog                     

README.md

  • Added Ghost CMS to technology stack section
  • Updated styling section to include Framer Motion
  • Added new "Public Pages" section documenting landing, company, legal,
    and blog pages
  • Expanded routes documentation with public and admin route tables
  • Updated project structure to include blog and FAQ components
  • Added Phase 6 completion for public pages and blog integration
  • Added Contributing guidelines reference
+67/-20 
README.md
Add blog component documentation                                                 

src/components/blog/README.md

  • Created comprehensive documentation for blog components
  • Documents BlogGrid and BlogPagination components with props and
    interfaces
  • Includes Ghost CMS setup instructions and API key configuration
  • Provides customization guide for posts per page and additional fields
  • Includes API reference, troubleshooting guide, and feature list
+174/-0 
CLAUDE.md
Documentation Updates for New Public Routes                           

CLAUDE.md

  • Added documentation for new public routes including /about, /features,
    /pricing, /showcase, /careers, /contact, /blog, /privacy, /terms, and
    /security
  • Added NEXT_PUBLIC_TOKEN environment variable documentation for Ghost
    CMS Content API integration
+11/-0   
Dependencies
1 files
package.json
Add axios dependency for API requests                                       

package.json

  • Added axios dependency version ^1.13.2 for HTTP requests
  • Used for Ghost CMS API calls in blog functionality
+1/-0     

Summary by CodeRabbit

  • New Features
    • Added many public pages (About, Careers, Contact, Features, Pricing, Showcase, Privacy, Terms, Security) plus a Ghost-powered Blog with loading, error, pagination, and CMS-driven posts; new contact API and form included.
  • UI
    • Numerous animated UI sections and reusable components (heroes, grids, CTAs, lists, testimonials, careers, pricing, showcase).
  • Platform / Config
    • New Ghost CMS env vars, external image hosts enabled, and added an HTTP client dependency.
  • Documentation
    • README and docs updated with routes, setup and roadmap additions.

✏️ Tip: You can customize this high-level summary in your review settings.

@inact25 inact25 requested a review from igun997 December 15, 2025 20:15
@inact25 inact25 self-assigned this Dec 15, 2025
@qodo-code-review

qodo-code-review Bot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Public token exposure

Description: The documentation instructs using NEXT_PUBLIC_TOKEN for the Ghost CMS API key, which makes
the token available to all clients at runtime and can enable unintended third-party use of
your Ghost Content API (and would be critical if a more privileged token is ever placed
there).
README.md [98-104]

Referred Code
# Optional - AI
KOLOSAL_API_KEY

# Optional - Blog
NEXT_PUBLIC_TOKEN="ghost_content_api_key"  # Ghost CMS Content API Key

</details></details></td></tr>
<tr><td><details><summary><strong>PII logging
</strong></summary><br>

<b>Description:</b> The contact form submission handler logs user-supplied PII (<code>name</code>, <code>email</code>, <code>company</code>, <code>subject</code>, <br><code>message</code>) to the browser console via <code>console.log('Form submitted:', formData);</code>, which can <br>leak sensitive data through client-side log collection/monitoring tools or shared devices.<br> <br> <strong><a href='https://github.com/cds-id/etags/pull/44/files#diff-c3f0df4f640dbcbe760f63e67897f06426a14e4917bdff36825c97c52cc1621fR25-R28'>page.tsx [25-28]</a></strong><br>

<details open><summary>Referred Code</summary>

```tsx
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  console.log('Form submitted:', formData);
};
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
PII in console log: The contact form logs formData via console.log, which can include PII (name, email,
message) and may be captured in client logs or monitoring tools.

Referred Code
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  console.log('Form submitted:', formData);
};

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
No audit logging: The PR adds user-input handling (contact form submission) without any evident audit trail
for the action, but it is unclear from the diff whether submissions are
persisted/processed elsewhere.

Referred Code
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  console.log('Form submitted:', formData);
};

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
No submit handling: The contact form handleSubmit only logs the payload and does not handle failures,
validation edge cases, or any real submission workflow, so robustness cannot be confirmed
from this diff.

Referred Code
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  console.log('Form submitted:', formData);
};

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated user input: The contact form collects free-text user input and currently only applies basic HTML
required checks without visible sanitization/validation or secure submission handling in
the diff.

Referred Code
export default function ContactPage() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    company: '',
    subject: '',
    message: '',
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log('Form submitted:', formData);
  };

  const contactInfo = [
    {
      icon: Mail,
      title: 'Email',
      value: 'hello@etags.id',
      link: 'mailto:hello@etags.id',
    },


 ... (clipped 199 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-review Bot commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Use a Headless CMS for all content-heavy pages

Instead of hardcoding content in new pages like 'Features' and 'Careers', use
the already integrated Headless CMS (Ghost) to manage this content. This
centralizes content management and allows for easier updates without code
changes.

Examples:

src/app/features/page.tsx [26-123]
  const features = [
    {
      icon: Shield,
      title: 'Blockchain Stamping',
      description:
        'Setiap tag produk dicatat secara permanen di blockchain Base Sepolia untuk memastikan keaslian yang tidak dapat dipalsukan.',
      color: 'text-[#2B4C7E]',
      bg: 'bg-[#2B4C7E]/10',
    },
    {

 ... (clipped 88 lines)
src/app/careers/page.tsx [23-108]
  const positions = [
    {
      title: 'Senior Blockchain Engineer',
      department: 'Engineering',
      type: 'Full-time',
      location: 'Remote',
      description:
        'Develop and maintain smart contracts on Base Sepolia, optimize gas usage, and implement new blockchain features.',
      requirements: [
        '5+ years experience with Solidity',

 ... (clipped 76 lines)

Solution Walkthrough:

Before:

// src/app/features/page.tsx

export default function FeaturesPage() {
  const features = [
    {
      icon: Shield,
      title: 'Blockchain Stamping',
      description: 'Setiap tag produk dicatat secara permanen di blockchain...',
    },
    {
      icon: QrCode,
      title: 'QR Code Generation',
      description: 'Generate QR code unik untuk setiap produk...',
    },
    // ... more hardcoded features
  ];

  return (
    <main>
      {/* ... UI rendering hardcoded features ... */}
    </main>
  );
}

After:

// src/lib/cms.ts
async function getFeaturesPageContent() {
  // API call to Headless CMS (e.g., Ghost)
  const response = await fetch('...');
  return response.json();
}

// src/app/features/page.tsx
import { getFeaturesPageContent } from '@/lib/cms';

export default async function FeaturesPage() {
  const { features } = await getFeaturesPageContent();

  return (
    <main>
      {/* ... UI rendering features from CMS ... */}
    </main>
  );
}
Suggestion importance[1-10]: 9

__

Why: This is a critical architectural suggestion that addresses a major maintainability issue across all new pages, proposing a scalable solution by leveraging the already-integrated CMS.

High
Possible issue
Validate all required environment variables

Add a check for the NEXT_PUBLIC_GHOST_URL environment variable alongside the
existing check for NEXT_PUBLIC_TOKEN to prevent runtime errors and improve
debugging.

src/app/blog/page.tsx [38-48]

 const getPostData = async (page: number = 1): Promise<GhostResponse> => {
   const token = process.env.NEXT_PUBLIC_TOKEN;
   const url = process.env.NEXT_PUBLIC_GHOST_URL;
-  if (!token) {
-    throw new Error('NEXT_PUBLIC_TOKEN is not set');
+  if (!token || !url) {
+    throw new Error('NEXT_PUBLIC_TOKEN or NEXT_PUBLIC_GHOST_URL is not set');
   }
   const result = await axios.get<GhostResponse>(
     `https://${url}/ghost/api/content/posts/?key=${token}&limit=${POSTS_PER_PAGE}&page=${page}&fields=title,excerpt,url,feature_image,published_at&include=tags,authors`
   );
   return result.data;
 };
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a missing check for the NEXT_PUBLIC_GHOST_URL environment variable, improving robustness and providing clearer error messages for developers during setup.

Medium
General
Refactor data structure for clarity

Refactor the showcase.stats data structure to explicitly define the third
metric's value and label, simplifying the complex and brittle conditional
rendering logic.

src/app/showcase/page.tsx [246-263]

 <div className="text-2xl font-bold text-[#2B4C7E]">
-  {showcase.stats.fraudDetected ||
-    showcase.stats.nftMinted ||
-    showcase.stats.recalled ||
-    showcase.stats.locations ||
-    showcase.stats.alertsSent}
+  {showcase.stats.metric.value}
 </div>
 <div className="text-sm text-[#606060]">
-  {showcase.stats.fraudDetected
-    ? 'Fraud Detected'
-    : showcase.stats.nftMinted
-      ? 'NFT Minted'
-      : showcase.stats.recalled === '0'
-        ? 'Recalls'
-        : showcase.stats.locations
-          ? 'Coverage'
-          : 'Alerts Sent'}
+  {showcase.stats.metric.label}
 </div>
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies brittle conditional logic and proposes a robust data structure refactor, which significantly improves code maintainability and readability.

Low
  • Update

@igun997 igun997 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Jir lah itu cek lagi cok

Comment thread src/app/about/page.tsx Outdated
Comment thread src/app/careers/page.tsx Outdated
Comment thread src/app/blog/page.tsx Outdated
Comment thread src/app/blog/page.tsx
Comment thread src/app/showcase/page.tsx Outdated
Comment thread src/app/contact/page.tsx Outdated
Comment thread src/app/security/page.tsx Outdated
Comment thread src/app/terms/page.tsx Outdated
Comment thread src/components/blog/README.md
Comment thread package.json
@coderabbitai

coderabbitai Bot commented Dec 15, 2025

Copy link
Copy Markdown

Walkthrough

Adds Ghost CMS blog integration, many new public pages and UI components, a contact API with rate limiting and validation, client-side contact form and validation, shared types/constants, a Ghost service module, image remote patterns, and new env/config entries.

Changes

Cohort / File(s) Summary
Config & Docs
\.env.example, next.config.ts, package.json, README.md, CLAUDE.md
New env vars (NEXT_PUBLIC_GHOST_URL, NEXT_PUBLIC_TOKEN), added axios dependency, appended remote image hosts to image.remotePatterns, and documentation/route/README updates.
App Pages
About / Features / Pricing / Showcase / Careers / Contact / Privacy / Terms / Security
src/app/about/page.tsx, src/app/features/page.tsx, src/app/pricing/page.tsx, src/app/showcase/page.tsx, src/app/careers/page.tsx, src/app/contact/page.tsx, src/app/privacy/page.tsx, src/app/terms/page.tsx, src/app/security/page.tsx
Added multiple app-router pages exporting metadata and default components that compose Navbar, Footer, and page-specific sections with static/localized content.
Blog
src/app/blog/page.tsx, src/app/blog/loading.tsx, src/components/blog/*, src/components/blog/README.md
New Ghost-backed blog index (server component, dynamic='force-dynamic', revalidate=60), loading skeleton, BlogGrid, BlogPagination, BlogError, and component README documenting Ghost setup.
Contact API & UI
src/app/api/contact/route.ts, src/components/contact/*, src/lib/validations/contact.ts
New /api/contact GET/POST with IP rate limiting (3/15min), sanitization/validation and structured errors; client ContactForm with client-side validation, submission handling, toasts, and multiple contact UI components.
Ghost Service
src/lib/services/ghost.ts, src/lib/services/index.ts
New Ghost service with types (GhostPost, pagination), config validation, URL builder, getGhostPosts (timeout + HTTP error mapping), getGhostErrorMessage, and re-export index.
Shared Types & Constants
src/types/common.ts, src/constants/*, src/constants/index.ts
New shared TypeScript interfaces and many constants for About, Features, Pricing, Showcase, Careers, Contact; a central constants barrel and selected lucide-react icon re-exports.
Component Libraries
src/components/{about,careers,features,legal,pricing,showcase}/*, src/components/*/index.ts
Added numerous UI components (Heroes, Lists, Grids, CTAs, Stats, Testimonials, Legal sections, Pricing cards/FAQs) using Framer Motion and barrel re-exports.
Image updates (Next/Image)
src/app/manage/**, src/app/support/**, src/app/verify/**
Replaced plain img with Next.js Image in multiple admin/support/verify pages/components; adjusted layout props and marked many as unoptimized.
Validation, Typing & Minor Refactors
src/lib/actions/*, src/lib/ai-agent.ts, src/lib/fraud-analysis-cache.ts, src/lib/nft-collectible.ts, src/lib/tag-stamping.ts, src/app/api/verify/route.ts
Strengthened TypeScript annotations, small refactors and import reorderings; behavior preserved.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant ContactForm as Contact Form (Client)
    participant Validation as Validation Lib
    participant API as /api/contact (Server)

    User->>Browser: Fill & submit contact form
    Browser->>ContactForm: submit(data)
    ContactForm->>Validation: sanitizeContactForm(data)
    Validation-->>ContactForm: sanitized data
    ContactForm->>Validation: validateContactForm(data)
    alt invalid
        Validation-->>ContactForm: ValidationResult(errors)
        ContactForm->>User: show field errors
    else valid
        ContactForm->>ContactForm: set loading
        ContactForm->>API: POST /api/contact
        alt rate limited
            API-->>ContactForm: 429 RATE_LIMIT_EXCEEDED
            ContactForm->>User: show rate limit toast
        else validation error
            API-->>ContactForm: 400 VALIDATION_ERROR
            ContactForm->>User: show field errors & toast
        else success
            API-->>ContactForm: 200 OK
            ContactForm->>User: show success toast, reset form
        end
        ContactForm->>ContactForm: clear loading
    end
Loading
sequenceDiagram
    participant User
    participant Browser
    participant BlogPage as Blog Page (Server)
    participant GhostSvc as Ghost Service
    participant GhostAPI as Ghost CMS API

    User->>Browser: Navigate to /blog?page=N
    Browser->>BlogPage: request page
    BlogPage->>GhostSvc: getGhostPosts(page, limit)
    GhostSvc->>GhostAPI: GET /content/posts?key=...&fields=...
    alt missing key / auth error
        GhostAPI-->>GhostSvc: error
        GhostSvc-->>BlogPage: error mapping
        BlogPage->>Browser: render BlogError
    else success
        GhostAPI-->>GhostSvc: posts + pagination
        GhostSvc-->>BlogPage: GhostPostsResponse
        BlogPage->>Browser: render BlogGrid + BlogPagination
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–75 minutes

Areas to focus review on:

  • src/lib/services/ghost.ts — API URL construction, timeout handling, error mapping and use of env vars.
  • src/app/api/contact/route.ts — rate-limiting correctness, IP extraction, and consistent error payload shapes.
  • src/components/contact/ContactForm.tsx — client validation mapping to server errors, UX edge cases, and toast/error flows.
  • src/app/blog/page.tsx and blog components — pagination math, server-side fetch flow, and fallback/error rendering.
  • Image changes — Next/Image usage, layout props (fill vs explicit sizes), unoptimized flags, and next.config remotePatterns coverage.
  • src/types/common.ts and constants — ensure types align with components and icons are available.

Poem

🐰 I hopped through files with nimble paws,
Wired forms and blog-post laws.
Pages bloom where Ghost now sings,
Buttons, grids, and fluttering wings.
A little warren of code applause!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.99% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main objective: implementing all basic public pages. It accurately reflects the primary change across the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@inact25

inact25 commented Dec 15, 2025

Copy link
Copy Markdown
Collaborator Author

urang full refactor sekalian benerin ssr seo

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

♻️ Duplicate comments (3)
package.json (1)

48-48: Axios dependency addition acknowledged.

The axios dependency is added for Ghost CMS API integration. The ghost.ts service utilizes axios's timeout configuration and error handling features. Given the past discussion about using native fetch, the choice to use axios is reasonable if you want consistent timeout handling and axios-specific error utilities across the codebase.

src/app/terms/page.tsx (1)

87-102: Remove testnet reference from production terms.

Line 89 mentions "Base Sepolia" (a testnet), which should not appear in production terms and conditions. This was previously flagged in review comments.

Based on learnings, past review indicated this testnet reference should be removed.

Apply this diff to use production blockchain terminology:

             <LegalSection title="3. Blockchain dan NFT" delay={0.2}>
               <p className="mb-4">
-                Etags menggunakan blockchain Base Sepolia untuk verifikasi
+                Etags menggunakan blockchain Base untuk verifikasi
                 produk dan NFT minting. Transaksi blockchain bersifat permanen
                 dan tidak dapat diubah setelah dikonfirmasi.
               </p>
src/app/security/page.tsx (1)

195-197: Remove "Base Sepolia" reference per previous feedback.

The reference to "Base Sepolia network" should be removed based on the previous review comment. Consider using just "Base network" or removing the network specification entirely.

Apply this diff:

-      <p>
-        Blockchain data bersifat permanen dan tersedia di Base Sepolia
-        network.
-      </p>
+      <p>
+        Blockchain data bersifat permanen dan tersedia di Base network.
+      </p>
🟡 Minor comments (4)
src/lib/services/ghost.ts-67-76 (1)

67-76: JSDoc default value is incorrect.

The comment states @param limit - Number of posts per page (default: 9) but the actual default is POSTS_PER_PAGE which is 12.

- * @param limit - Number of posts per page (default: 9)
+ * @param limit - Number of posts per page (default: 12)
src/components/blog/README.md-57-57 (1)

57-57: Add language identifier to code fence.

The code fence on line 57 is missing a language identifier. This causes a markdown linting warning.

Apply this diff:

-   ```
+   ```env
    NEXT_PUBLIC_TOKEN=your_ghost_content_api_key_here
    ```
src/components/blog/README.md-123-123 (1)

123-123: Wrap bare URL in angle brackets or markdown link.

The bare URL on line 123 should be formatted as a proper markdown link to avoid linting warnings.

Apply this diff:

-Ghost Content API documentation: https://ghost.org/docs/content-api/
+Ghost Content API documentation: <https://ghost.org/docs/content-api/>

Or as a proper markdown link:

-Ghost Content API documentation: https://ghost.org/docs/content-api/
+Ghost Content API documentation: [https://ghost.org/docs/content-api/](https://ghost.org/docs/content-api/)
src/components/blog/BlogGrid.tsx-9-19 (1)

9-19: Remove local BlogPost interface and use GhostPost from the Ghost service instead.

The local BlogPost interface duplicates the GhostPost type already defined in src/lib/services/ghost.ts. Since src/app/blog/page.tsx imports GhostPost and passes it directly to BlogGrid, the component should accept GhostPost[] instead of defining its own local type. This eliminates duplication and maintains a single source of truth for Ghost API types.

🧹 Nitpick comments (40)
.env.example (1)

39-41: Consider a more descriptive environment variable name.

NEXT_PUBLIC_TOKEN is quite generic and could be confused with other tokens in the future. Consider renaming to NEXT_PUBLIC_GHOST_CONTENT_API_KEY for clarity and to match Ghost's documentation terminology.

The static analysis warning about quote characters is a false positive for .env.example files where quotes in placeholder values are conventional.

next.config.ts (1)

26-30: Hardcoded hostname inconsistent with environment variable approach.

The hostname blog.javapixa.com is hardcoded here, but the PR also introduces NEXT_PUBLIC_GHOST_URL as an environment variable. This creates a maintenance burden—changing the Ghost instance requires updating both the env var and this config.

Consider extracting the hostname dynamically or documenting that this must be updated alongside the environment variable. Note that remotePatterns doesn't support runtime environment variables, so you may need to use a build-time approach or document this coupling.

src/components/features/FeaturesHero.tsx (1)

5-28: LGTM!

Clean implementation with appropriate use of Framer Motion for entrance animations. The staggered delay creates a nice sequential reveal effect.

Consider extracting the hardcoded color values (#0C2340, #2B4C7E, #606060) to a shared theme or constants file for consistency across the multiple new public pages added in this PR.

src/lib/services/ghost.ts (2)

53-65: Non-null assertion relies on caller discipline.

Line 58 uses GHOST_API_KEY! assuming validateConfig() was called. While current callers do validate first, this is fragile. Consider validating within buildApiUrl or restructuring to avoid the assertion:

 function buildApiUrl(
   endpoint: string,
   params: Record<string, string | number>
 ): string {
+  if (!GHOST_API_KEY) {
+    throw new Error('GHOST_API_KEY_MISSING');
+  }
   const url = new URL(`${GHOST_API_URL}/${endpoint}/`);
-  url.searchParams.set('key', GHOST_API_KEY!);
+  url.searchParams.set('key', GHOST_API_KEY);

139-144: Inconsistent error handling between fetch functions.

getGhostPosts throws on errors (allowing callers to handle), while getGhostPostBySlug silently returns null and only logs in development. This inconsistency can make debugging production issues difficult and may hide configuration problems.

Consider either:

  1. Making both functions throw and letting callers decide how to handle errors
  2. Adding an optional throwOnError parameter for consistency
src/components/about/ValuesSection.tsx (1)

10-13: Consider a more maintainable icon mapping approach.

The current array index-based mapping [Shield, Target, Users, Zap][index] is fragile. If the order or number of values in ABOUT_VALUES changes, the icon-to-value mapping will break silently.

Consider one of these approaches:

Option 1: Add icon keys to ABOUT_VALUES constant

// In src/constants/about.ts
export const ABOUT_VALUES = [
  {
    iconKey: 'shield',
    title: 'Keamanan',
    description: '...',
  },
  // ...
] as const;

// In ValuesSection.tsx
const iconMap = {
  shield: Shield,
  target: Target,
  users: Users,
  zap: Zap,
} as const;

const values = ABOUT_VALUES.map((value) => ({
  ...value,
  icon: iconMap[value.iconKey],
}));

Option 2: Use a Map for explicit pairing

const iconMapping = new Map([
  ['Keamanan', Shield],
  ['Inovasi', Target],
  ['Kolaborasi', Users],
  ['Efisiensi', Zap],
]);

const values = ABOUT_VALUES.map((value) => ({
  ...value,
  icon: iconMapping.get(value.title)!,
}));
src/components/about/StatsSection.tsx (1)

7-7: Consider removing the wrapper constant.

The MotionDiv wrapper is unnecessary and can be replaced with direct usage of motion.div in the JSX, simplifying the code.

Apply this diff:

-const MotionDiv = motion.div;
-
 export function StatsSection() {
   const stats: StatItem[] = ABOUT_STATS as unknown as StatItem[];
   return (
     <div className="grid md:grid-cols-3 gap-8 max-w-4xl mx-auto">
       {stats.map((stat, index) => (
-        <MotionDiv
+        <motion.div
           key={stat.label}
           className="text-center p-6 bg-white border border-[#2B4C7E]/20 rounded-xl shadow-md"
           initial={{ opacity: 0, y: 20 }}
           whileInView={{ opacity: 1, y: 0 }}
           viewport={{ once: true }}
           transition={{ duration: 0.5, delay: index * 0.1 }}
         >
           <div className="text-4xl font-bold text-[#2B4C7E] mb-2">
             {stat.value}
           </div>
           <div className="text-[#606060] font-medium">{stat.label}</div>
-        </MotionDiv>
+        </motion.div>
       ))}
     </div>
   );
src/components/careers/CareersCTA.tsx (1)

22-24: Consider Indonesian subject for language consistency.

The email subject is in English ("General Application") while the entire UI uses Indonesian. For a consistent user experience, consider translating the subject.

       <a
-        href="mailto:careers@etags.id?subject=General Application"
+        href="mailto:careers@etags.id?subject=Lamaran Umum"
         className="inline-flex items-center gap-2 bg-[#2B4C7E] text-white px-8 py-4 rounded-lg font-semibold hover:bg-[#1E3A5F] transition-colors"
       >
CLAUDE.md (1)

262-262: Consider a more specific environment variable name.

The variable name NEXT_PUBLIC_TOKEN is overly generic and could conflict with other tokens in the project (auth tokens, CSRF tokens, etc.). Ghost CMS typically uses more descriptive names like GHOST_CONTENT_API_KEY.

Consider renaming to:

NEXT_PUBLIC_GHOST_API_KEY

or

NEXT_PUBLIC_GHOST_CONTENT_API_KEY

This would make the purpose immediately clear and reduce the risk of naming conflicts. If you proceed with this change, ensure you update:

  • .env.example
  • Any code referencing this variable (likely in src/lib/services/ghost.ts)
  • This documentation
src/components/careers/ValuesCards.tsx (1)

12-12: Consider adding an intermediate grid breakpoint.

The grid jumps from 1 column (mobile) directly to 4 columns (tablet). Adding a 2-column layout for small screens could improve the experience on tablets.

-      <div className="grid md:grid-cols-4 gap-6">
+      <div className="grid sm:grid-cols-2 md:grid-cols-4 gap-6">

However, the current implementation is perfectly valid if the design intentionally wants a single-column layout on smaller devices.

src/components/contact/ContactHero.tsx (1)

1-27: LGTM! Consider a shared hero component for future DRY improvement.

The component is clean and follows the established pattern. It shares a similar structure with ShowcaseHero (and potentially other hero components in this PR). If the hero pattern appears in more pages, consider creating a generic HeroSection component that accepts title and description props.

For future consideration (not blocking):

// src/components/common/HeroSection.tsx
interface HeroSectionProps {
  title: string;
  highlightedWord: string;
  description: string;
}

export function HeroSection({ title, highlightedWord, description }: HeroSectionProps) {
  return (
    <div className="max-w-4xl mx-auto text-center mb-20">
      <motion.h1
        className="text-4xl sm:text-5xl lg:text-6xl font-bold text-[#0C2340] mb-6"
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.5 }}
      >
        {title} <span className="text-[#2B4C7E]">{highlightedWord}</span>
      </motion.h1>
      <motion.p
        className="text-lg text-[#606060] leading-relaxed"
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.5, delay: 0.1 }}
      >
        {description}
      </motion.p>
    </div>
  );
}
src/components/about/AboutHero.tsx (1)

5-6: Remove unnecessary motion component wrappers.

Creating MotionH1 and MotionP aliases adds no value and reduces code clarity. Framer Motion's motion.h1 and motion.p are already concise.

-const MotionH1 = motion.h1;
-const MotionP = motion.p;
-
 export function AboutHero() {
   return (
     <div className="max-w-4xl mx-auto text-center mb-20">
-      <MotionH1
+      <motion.h1
         className="text-4xl sm:text-5xl lg:text-6xl font-bold text-[#0C2340] mb-6"
         initial={{ opacity: 0, y: 20 }}
         animate={{ opacity: 1, y: 0 }}
         transition={{ duration: 0.5 }}
       >
         Tentang <span className="text-[#2B4C7E]">Etags</span>
-      </MotionH1>
+      </motion.h1>
-      <MotionP
+      <motion.p
         className="text-lg text-[#606060] leading-relaxed"
         initial={{ opacity: 0, y: 20 }}
         animate={{ opacity: 1, y: 0 }}
         transition={{ duration: 0.5, delay: 0.1 }}
       >
         Platform verifikasi produk berbasis blockchain yang mengamankan rantai
         pasokan dan memberikan kepercayaan penuh kepada konsumen.
-      </MotionP>
+      </motion.p>
     </div>
   );
 }
src/components/pricing/PricingFAQs.tsx (3)

8-8: Type casting bypasses type safety.

The double cast as unknown as FAQItem[] bypasses TypeScript's type checking and indicates a type mismatch between the const assertion in PRICING_FAQS and the FAQItem interface.

Consider one of these approaches:

  1. Update FAQItem to accept readonly properties if that's the mismatch
  2. Remove the const assertion from PRICING_FAQS if mutability isn't required
  3. Use a type assertion helper function for clarity
const faqs = PRICING_FAQS as readonly FAQItem[];

17-17: Key should be stable and unique.

Using faq.question as a React key is fragile—if questions are ever duplicated or modified, it will break reconciliation and potentially cause rendering bugs.

Use the index or add a unique id field to FAQItem:

-key={faq.question}
+key={index}

Or better, if feasible, extend the FAQItem interface to include an id field.


11-13: Language inconsistency in UI text.

The heading "Frequently Asked Questions" is in English, while the rest of the pricing page content is in Indonesian (e.g., "Harga yang Transparan", FAQ questions/answers in Indonesian).

Consider translating to maintain consistency:

-Frequently Asked Questions
+Pertanyaan yang Sering Diajukan
src/components/features/BenefitsSection.tsx (1)

14-26: Using benefit text as React key is fragile.

Similar to other components in this PR, using the content string (benefit) as the key is fragile if the text ever changes or duplicates.

Use the index for a stable key:

-key={benefit}
+key={index}
src/components/contact/ContactInfo.tsx (3)

9-10: Type casting bypasses type safety.

The double cast as unknown as ContactInfoType[] indicates a type mismatch, likely due to the const assertion on CONTACT_INFO in the constants file.

See similar comment in PricingFAQs.tsx. Consider aligning the types or using a readonly type:

const contactInfo = CONTACT_INFO as readonly ContactInfoType[];

60-83: Rigid structure tightly coupled to CONTACT_HOURS shape.

This code directly accesses CONTACT_HOURS.weekday.days, .saturday, and .sunday with hardcoded structure. If CONTACT_HOURS is refactored (e.g., to an array), this breaks.

Consider mapping over hours dynamically if CONTACT_HOURS were structured as an array:

Object.entries(CONTACT_HOURS).map(([key, value]) => (
  <div key={key} className="flex justify-between">
    <span className="text-sm text-[#606060]">{value.days}</span>
    <span className="text-sm font-semibold text-[#0C2340]">{value.hours}</span>
  </div>
))

This would be more maintainable if additional days are added.


36-45: Links need accessible labels for screen readers.

The links use the visible text (info.value) as content, which is good, but email/phone links could benefit from explicit aria-label for clarity.

For better accessibility:

<a
  href={info.link}
+ aria-label={`${info.title}: ${info.value}`}
  className="text-sm text-[#606060] hover:text-[#2B4C7E] transition-colors"
>
  {info.value}
</a>
src/app/pricing/page.tsx (1)

33-36: Large blur effects may impact paint performance.

The fixed background divs use very large dimensions (50vw × 50vw) with 120px blur radius. On lower-end devices, this could cause paint/composite performance issues.

Monitor real-world performance. If issues arise, consider:

  1. Reducing blur radius
  2. Using smaller elements
  3. Using CSS will-change: transform or contain: layout to isolate repaints
  4. Rendering blurs as static images
src/components/about/TeamSection.tsx (3)

8-8: Unnecessary alias for motion.div.

The MotionDiv alias doesn't add value and adds an extra lookup step.

Remove the alias and use motion.div directly:

-const MotionDiv = motion.div;
-
 export function TeamSection() {
   const team: TeamMember[] = ABOUT_TEAM as unknown as TeamMember[];
   return (
     <div className="text-center mb-20">
       ...
       {team.map((member, index) => (
-        <MotionDiv
+        <motion.div

11-11: Type casting bypasses type safety.

The double cast as unknown as TeamMember[] indicates a const assertion mismatch. See similar comments in other reviewed files.

Consider using as readonly TeamMember[] or fixing the type definition.


22-22: Using name as key is fragile.

If two team members have the same name (unlikely but possible), React reconciliation will fail.

Use index or add a unique id field:

-key={member.name}
+key={index}
src/components/pricing/IncludedFeatures.tsx (1)

21-38: Using feature text as key is fragile.

Using the feature string as the React key is fragile if features are reworded or duplicated.

Use the index for stability:

-key={feature}
+key={index}
src/app/api/contact/route.ts (2)

102-109: TODO acknowledged - simulated delay should be removed when implementing actual submission.

The placeholder delay is fine for development, but remember to remove it when implementing the actual email/database integration to avoid unnecessary latency.

Would you like me to help scaffold the email sending implementation using a service like Resend or SendGrid?


30-34: In-memory rate limiting resets on serverless cold starts.

The rate limiter uses an in-memory Map (line 34, @/lib/rate-limit), which is already documented as a limitation (see rate-limit.ts line 3). This approach works for development but will not persist rate-limit counters across serverless invocations in production environments like Vercel, causing the rate limit to reset on each cold start.

For production, migrate to a persistent store such as Redis, Upstash, Vercel KV, or DynamoDB to maintain rate-limit state across invocations.

src/app/about/page.tsx (1)

26-31: Consider extracting background effects to a shared component.

This background effects pattern (fixed blurred circles) is duplicated across multiple pages (about, features, and likely others). Extracting to a shared BackgroundEffects component would reduce duplication.

// Example: src/components/shared/BackgroundEffects.tsx
export function BackgroundEffects() {
  return (
    <div className="fixed inset-0 pointer-events-none z-0">
      <div className="absolute top-[-10%] left-[-5%] w-[50vw] h-[50vw] rounded-full bg-[#2B4C7E]/10 blur-[120px]" />
      <div className="absolute bottom-[-10%] right-[-10%] w-[40vw] h-[40vw] rounded-full bg-[#A8A8A8]/20 blur-[120px]" />
    </div>
  );
}
src/components/showcase/ShowcaseGrid.tsx (2)

8-8: Avoid double type assertion (as unknown as).

The as unknown as ShowcaseItem[] pattern bypasses type safety. This occurs because SHOWCASE_ITEMS uses as const (making it readonly), but ShowcaseItem expects mutable types.

Consider either:

  1. Using Readonly<ShowcaseItem>[] or ReadonlyArray<ShowcaseItem> for the local type
  2. Removing as const from SHOWCASE_ITEMS if mutability constraints aren't needed
-  const items: ShowcaseItem[] = SHOWCASE_ITEMS as unknown as ShowcaseItem[];
+  const items = SHOWCASE_ITEMS as ReadonlyArray<ShowcaseItem>;

Or update the type import to handle readonly:

const items: readonly ShowcaseItem[] = SHOWCASE_ITEMS;

44-55: Stats rendering with dynamic keys is flexible but could benefit from explicit ordering.

Using Object.entries(item.stats) works but JavaScript object key order isn't guaranteed in all environments. If stat display order matters, consider defining an explicit order array.

const STAT_ORDER = ['tags', 'scans', 'fraudDetected', 'nftMinted', 'recalled', 'locations', 'alertsSent'] as const;

// Then filter and map in order:
{STAT_ORDER
  .filter(key => item.stats[key])
  .map(key => (
    <div key={key} className="text-center">
      <div className="text-2xl font-bold text-[#0C2340] mb-1">
        {item.stats[key]}
      </div>
      <div className="text-xs text-[#606060] capitalize">
        {key.replace(/([A-Z])/g, ' $1').trim()}
      </div>
    </div>
  ))}
src/components/about/TechnologyStack.tsx (1)

10-11: Simplify type casting.

The double cast as unknown as TechStackItem[] is unnecessary if ABOUT_TECH_STACK is already properly typed in the constants file. Based on the relevant code snippets, ABOUT_TECH_STACK is defined with as const, which should be compatible with TechStackItem[].

Apply this diff to remove the unnecessary cast:

-  const techStack: TechStackItem[] =
-    ABOUT_TECH_STACK as unknown as TechStackItem[];
+  const techStack: readonly TechStackItem[] = ABOUT_TECH_STACK;

Or if direct usage is preferred:

-  const techStack: TechStackItem[] =
-    ABOUT_TECH_STACK as unknown as TechStackItem[];
   return (
     <MotionDiv
       className="bg-gradient-to-br from-[#2B4C7E]/5 to-white border-2 border-[#2B4C7E]/20 rounded-2xl p-8 lg:p-12 mb-20"
@@ -26,7 +24,7 @@
       </p>
       <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
-        {techStack.map((tech) => (
+        {ABOUT_TECH_STACK.map((tech) => (
src/components/showcase/TestimonialsSection.tsx (1)

9-10: Simplify type casting.

The double cast as unknown as TestimonialItem[] is unnecessary. Based on the constants definition, TESTIMONIALS is typed with as const and should be compatible with TestimonialItem[].

Apply this diff to simplify:

-  const testimonials: TestimonialItem[] =
-    TESTIMONIALS as unknown as TestimonialItem[];
+  const testimonials: readonly TestimonialItem[] = TESTIMONIALS;

Or use the constant directly:

-  const testimonials: TestimonialItem[] =
-    TESTIMONIALS as unknown as TestimonialItem[];
   return (
     <div className="mb-20">
@@ -15,7 +13,7 @@
       </h2>
       <div className="grid md:grid-cols-3 gap-6">
-        {testimonials.map((testimonial, index) => (
+        {TESTIMONIALS.map((testimonial, index) => (
src/components/pricing/PricingCards.tsx (1)

10-10: Avoid unsafe double type assertion.

The as unknown as PricingPlan[] cast bypasses TypeScript's type checking entirely. Since PRICING_PLANS is defined with as const, you can use a type assertion that preserves safety or adjust the type definition.

Consider either:

  1. Remove as const from the source definition if mutability isn't needed
  2. Use a cleaner assertion:
-  const plans: PricingPlan[] = PRICING_PLANS as unknown as PricingPlan[];
+  const plans = PRICING_PLANS as readonly PricingPlan[];

Or simply iterate directly without reassignment:

-  const plans: PricingPlan[] = PRICING_PLANS as unknown as PricingPlan[];
   return (
     <div className="grid md:grid-cols-3 gap-8 mb-20">
-      {plans.map((plan, index) => {
+      {(PRICING_PLANS as readonly PricingPlan[]).map((plan, index) => {
src/app/contact/page.tsx (1)

35-38: Consider reduced motion preferences for background effects.

The large blur effects are decorative and won't cause issues, but for consistency with the animation-heavy components in this PR, you might want to ensure these don't cause performance issues on lower-end devices.

README.md (1)

101-103: Consider a more descriptive environment variable name.

NEXT_PUBLIC_TOKEN is generic and could be confused with other API tokens. For clarity and maintainability, consider renaming to match the Ghost-specific context.

 # Optional - Blog
-NEXT_PUBLIC_TOKEN="ghost_content_api_key"  # Ghost CMS Content API Key
+NEXT_PUBLIC_GHOST_CONTENT_API_KEY="ghost_content_api_key"  # Ghost CMS Content API Key

This would require updating references in .env.example and src/lib/services/ghost.ts.

src/components/legal/LegalHero.tsx (1)

30-39: Consider extracting the hardcoded label for i18n consistency.

The "Terakhir diperbarui:" string is hardcoded in Indonesian. If internationalization is planned, consider making this a prop or using a constants file.

 interface LegalHeroProps {
   title: string;
   description: string;
   lastUpdated?: string;
+  lastUpdatedLabel?: string;
 }

-export function LegalHero({ title, description, lastUpdated }: LegalHeroProps) {
+export function LegalHero({ 
+  title, 
+  description, 
+  lastUpdated,
+  lastUpdatedLabel = 'Terakhir diperbarui:'
+}: LegalHeroProps) {
src/components/careers/PositionsList.tsx (2)

62-70: Use stable keys for list items.

Using array index i as the key for requirement list items can cause React reconciliation issues if the requirements array is ever reordered or modified dynamically.

Apply this diff to use the requirement text as the key:

-                {position.requirements.map((req, i) => (
+                {position.requirements.map((req) => (
                   <li
-                    key={i}
+                    key={req}
                     className="flex items-start gap-2 text-sm text-[#606060]"
                   >

9-9: Consider updating the JOB_POSITIONS constant to remove the need for double type assertion.

The cast as unknown as JobPosition[] is currently necessary because JOB_POSITIONS is defined with as const, which makes the array readonly and narrows string types to literal types (e.g., 'Senior Blockchain Engineer' instead of string). The mutable JobPosition[] type cannot accept a readonly array directly, requiring the intermediate unknown cast to escape TypeScript's type system.

The cleanest solution is to update the constant definition in src/constants/careers.ts:

-export const JOB_POSITIONS = [
+export const JOB_POSITIONS: JobPosition[] = [
   {

This removes the as const assertion, making the array mutable and compatible with JobPosition[], eliminating the double cast in PositionsList.tsx.

src/components/blog/BlogGrid.tsx (1)

7-7: Remove unnecessary alias.

The MotionDiv alias doesn't provide any value and adds an extra layer of indirection. Use motion.div directly in the JSX.

Apply this diff:

-const MotionDiv = motion.div;

And update line 45:

-        <MotionDiv
+        <motion.div
           key={post.url}
           ...
-        </MotionDiv>
+        </motion.div>
src/components/blog/BlogPagination.tsx (1)

20-21: Remove unused searchParams variable.

The searchParams constant is declared but never used in the component, as URLs are constructed manually in the navigateToPage function.

Apply this diff:

 export function BlogPagination({
   currentPage,
   totalPages,
   hasNext,
   hasPrev,
 }: BlogPaginationProps) {
   const router = useRouter();
-  const searchParams = useSearchParams();
src/lib/validations/contact.ts (1)

31-36: Consider more comprehensive sanitization.

The current sanitization only removes angle brackets and limits length. While this prevents basic HTML injection, consider using a dedicated sanitization library like DOMPurify for more comprehensive XSS prevention, especially if the data is displayed anywhere without proper escaping.

That said, the current approach is acceptable for server-side validation where the data will be properly escaped when rendered.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abc84ca and c1406dc.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (72)
  • .env.example (1 hunks)
  • CLAUDE.md (2 hunks)
  • README.md (5 hunks)
  • next.config.ts (1 hunks)
  • package.json (1 hunks)
  • src/app/about/page.tsx (1 hunks)
  • src/app/api/contact/route.ts (1 hunks)
  • src/app/blog/loading.tsx (1 hunks)
  • src/app/blog/page.tsx (1 hunks)
  • src/app/careers/page.tsx (1 hunks)
  • src/app/contact/page.tsx (1 hunks)
  • src/app/features/page.tsx (1 hunks)
  • src/app/pricing/page.tsx (1 hunks)
  • src/app/privacy/page.tsx (1 hunks)
  • src/app/security/page.tsx (1 hunks)
  • src/app/showcase/page.tsx (1 hunks)
  • src/app/terms/page.tsx (1 hunks)
  • src/components/about/AboutHero.tsx (1 hunks)
  • src/components/about/MissionVision.tsx (1 hunks)
  • src/components/about/StatsSection.tsx (1 hunks)
  • src/components/about/TeamSection.tsx (1 hunks)
  • src/components/about/TechnologyStack.tsx (1 hunks)
  • src/components/about/ValuesSection.tsx (1 hunks)
  • src/components/about/index.ts (1 hunks)
  • src/components/blog/BlogGrid.tsx (1 hunks)
  • src/components/blog/BlogPagination.tsx (1 hunks)
  • src/components/blog/README.md (1 hunks)
  • src/components/blog/index.ts (1 hunks)
  • src/components/careers/BenefitsGrid.tsx (1 hunks)
  • src/components/careers/CareersCTA.tsx (1 hunks)
  • src/components/careers/CareersHero.tsx (1 hunks)
  • src/components/careers/PositionsList.tsx (1 hunks)
  • src/components/careers/ValuesCards.tsx (1 hunks)
  • src/components/careers/index.ts (1 hunks)
  • src/components/contact/ContactCTA.tsx (1 hunks)
  • src/components/contact/ContactForm.tsx (1 hunks)
  • src/components/contact/ContactHero.tsx (1 hunks)
  • src/components/contact/ContactInfo.tsx (1 hunks)
  • src/components/contact/ContactReasons.tsx (1 hunks)
  • src/components/contact/index.ts (1 hunks)
  • src/components/features/BenefitsSection.tsx (1 hunks)
  • src/components/features/FeaturesCTA.tsx (1 hunks)
  • src/components/features/FeaturesHero.tsx (1 hunks)
  • src/components/features/FeaturesList.tsx (1 hunks)
  • src/components/features/IntegrationsGrid.tsx (1 hunks)
  • src/components/features/index.ts (1 hunks)
  • src/components/legal/LegalHero.tsx (1 hunks)
  • src/components/legal/LegalSection.tsx (1 hunks)
  • src/components/legal/index.ts (1 hunks)
  • src/components/pricing/IncludedFeatures.tsx (1 hunks)
  • src/components/pricing/PricingCTA.tsx (1 hunks)
  • src/components/pricing/PricingCards.tsx (1 hunks)
  • src/components/pricing/PricingFAQs.tsx (1 hunks)
  • src/components/pricing/PricingHero.tsx (1 hunks)
  • src/components/pricing/index.ts (1 hunks)
  • src/components/showcase/AchievementStats.tsx (1 hunks)
  • src/components/showcase/ShowcaseCTA.tsx (1 hunks)
  • src/components/showcase/ShowcaseGrid.tsx (1 hunks)
  • src/components/showcase/ShowcaseHero.tsx (1 hunks)
  • src/components/showcase/TestimonialsSection.tsx (1 hunks)
  • src/components/showcase/index.ts (1 hunks)
  • src/constants/about.ts (1 hunks)
  • src/constants/careers.ts (1 hunks)
  • src/constants/contact.ts (1 hunks)
  • src/constants/features.ts (1 hunks)
  • src/constants/index.ts (1 hunks)
  • src/constants/pricing.ts (1 hunks)
  • src/constants/showcase.ts (1 hunks)
  • src/lib/services/ghost.ts (1 hunks)
  • src/lib/services/index.ts (1 hunks)
  • src/lib/validations/contact.ts (1 hunks)
  • src/types/common.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (43)
src/components/careers/BenefitsGrid.tsx (3)
src/components/careers/index.ts (1)
  • BenefitsGrid (3-3)
src/types/common.ts (1)
  • BenefitItem (82-86)
src/constants/careers.ts (1)
  • CAREER_BENEFITS (94-126)
src/components/showcase/ShowcaseGrid.tsx (3)
src/components/showcase/index.ts (1)
  • ShowcaseGrid (3-3)
src/types/common.ts (1)
  • ShowcaseItem (48-64)
src/constants/showcase.ts (1)
  • SHOWCASE_ITEMS (16-87)
src/components/careers/CareersHero.tsx (1)
src/components/careers/index.ts (1)
  • CareersHero (1-1)
src/app/pricing/page.tsx (5)
src/components/pricing/PricingHero.tsx (1)
  • PricingHero (5-27)
src/components/pricing/PricingCards.tsx (1)
  • PricingCards (9-77)
src/components/pricing/IncludedFeatures.tsx (1)
  • IncludedFeatures (7-43)
src/components/pricing/PricingFAQs.tsx (1)
  • PricingFAQs (7-33)
src/components/pricing/PricingCTA.tsx (1)
  • PricingCTA (7-40)
src/components/showcase/ShowcaseHero.tsx (1)
src/components/showcase/index.ts (1)
  • ShowcaseHero (1-1)
src/components/showcase/AchievementStats.tsx (2)
src/components/showcase/index.ts (1)
  • AchievementStats (2-2)
src/constants/showcase.ts (1)
  • ACHIEVEMENTS (113-134)
src/components/about/AboutHero.tsx (1)
src/components/about/index.ts (1)
  • AboutHero (1-1)
src/components/features/FeaturesList.tsx (2)
src/components/features/index.ts (1)
  • FeaturesList (2-2)
src/constants/features.ts (1)
  • FEATURES_LIST (20-117)
src/components/contact/ContactCTA.tsx (1)
src/components/contact/index.ts (1)
  • ContactCTA (5-5)
src/components/about/TechnologyStack.tsx (3)
src/components/about/index.ts (1)
  • TechnologyStack (4-4)
src/types/common.ts (1)
  • TechStackItem (19-22)
src/constants/about.ts (1)
  • ABOUT_TECH_STACK (28-35)
src/components/pricing/PricingCTA.tsx (1)
src/components/pricing/index.ts (1)
  • PricingCTA (5-5)
src/components/careers/CareersCTA.tsx (2)
src/components/careers/index.ts (1)
  • CareersCTA (5-5)
src/constants/index.ts (1)
  • Mail (39-39)
src/components/legal/LegalHero.tsx (1)
src/components/legal/index.ts (1)
  • LegalHero (1-1)
src/components/about/MissionVision.tsx (1)
src/components/about/index.ts (1)
  • MissionVision (2-2)
src/app/security/page.tsx (2)
src/components/legal/LegalHero.tsx (1)
  • LegalHero (11-42)
src/components/legal/LegalSection.tsx (1)
  • LegalSection (12-31)
src/components/contact/ContactReasons.tsx (3)
src/components/contact/index.ts (1)
  • ContactReasons (2-2)
src/types/common.ts (1)
  • ContactReason (95-100)
src/constants/contact.ts (1)
  • CONTACT_REASONS (28-49)
src/components/pricing/PricingHero.tsx (1)
src/components/pricing/index.ts (1)
  • PricingHero (1-1)
src/components/pricing/PricingCards.tsx (2)
src/types/common.ts (1)
  • PricingPlan (29-41)
src/constants/pricing.ts (1)
  • PRICING_PLANS (7-74)
src/components/contact/ContactForm.tsx (1)
src/lib/validations/contact.ts (4)
  • ContactFormData (5-11)
  • sanitizeContactForm (140-148)
  • validateContactForm (83-135)
  • ValidationError (13-16)
src/components/showcase/TestimonialsSection.tsx (3)
src/components/showcase/index.ts (1)
  • TestimonialsSection (4-4)
src/types/common.ts (1)
  • TestimonialItem (66-71)
src/constants/showcase.ts (1)
  • TESTIMONIALS (89-111)
src/components/pricing/IncludedFeatures.tsx (3)
src/components/pricing/index.ts (1)
  • IncludedFeatures (3-3)
src/constants/pricing.ts (1)
  • PRICING_INCLUDED_FEATURES (99-108)
src/constants/index.ts (1)
  • CheckCircle2 (28-28)
src/components/pricing/PricingFAQs.tsx (3)
src/components/pricing/index.ts (1)
  • PricingFAQs (4-4)
src/types/common.ts (1)
  • FAQItem (43-46)
src/constants/pricing.ts (1)
  • PRICING_FAQS (76-97)
src/components/showcase/ShowcaseCTA.tsx (2)
src/components/showcase/index.ts (1)
  • ShowcaseCTA (5-5)
src/constants/index.ts (1)
  • Sparkles (27-27)
src/components/features/BenefitsSection.tsx (3)
src/components/features/index.ts (1)
  • BenefitsSection (3-3)
src/constants/features.ts (1)
  • FEATURES_BENEFITS (128-137)
src/constants/index.ts (1)
  • CheckCircle2 (28-28)
src/components/contact/ContactInfo.tsx (4)
src/components/contact/index.ts (1)
  • ContactInfo (4-4)
src/types/common.ts (1)
  • ContactInfo (88-93)
src/constants/contact.ts (3)
  • CONTACT_INFO (7-26)
  • CONTACT_HOURS (51-55)
  • RESPONSE_TIME (57-58)
src/constants/index.ts (1)
  • Clock (42-42)
src/components/careers/PositionsList.tsx (4)
src/components/careers/index.ts (1)
  • PositionsList (4-4)
src/types/common.ts (1)
  • JobPosition (73-80)
src/constants/careers.ts (1)
  • JOB_POSITIONS (7-92)
src/constants/index.ts (3)
  • Briefcase (35-35)
  • Clock (42-42)
  • MapPin (24-24)
src/app/api/contact/route.ts (2)
src/lib/rate-limit.ts (1)
  • checkRateLimit (41-84)
src/lib/validations/contact.ts (3)
  • ContactFormData (5-11)
  • sanitizeContactForm (140-148)
  • validateContactForm (83-135)
src/components/about/TeamSection.tsx (2)
src/types/common.ts (1)
  • TeamMember (13-17)
src/constants/about.ts (1)
  • ABOUT_TEAM (37-58)
src/components/features/FeaturesCTA.tsx (1)
src/components/features/index.ts (1)
  • FeaturesCTA (5-5)
src/app/about/page.tsx (6)
src/components/about/AboutHero.tsx (1)
  • AboutHero (8-30)
src/components/about/MissionVision.tsx (1)
  • MissionVision (7-41)
src/components/about/ValuesSection.tsx (1)
  • ValuesSection (15-43)
src/components/about/TechnologyStack.tsx (1)
  • TechnologyStack (9-37)
src/components/about/TeamSection.tsx (1)
  • TeamSection (10-47)
src/components/about/StatsSection.tsx (1)
  • StatsSection (9-30)
src/constants/pricing.ts (1)
src/constants/index.ts (3)
  • Zap (18-18)
  • Sparkles (27-27)
  • Crown (43-43)
src/components/careers/ValuesCards.tsx (2)
src/components/careers/index.ts (1)
  • ValuesCards (2-2)
src/constants/careers.ts (1)
  • CAREER_VALUES (128-151)
src/constants/contact.ts (1)
src/constants/index.ts (5)
  • Mail (39-39)
  • Phone (40-40)
  • MapPin (24-24)
  • MessageSquare (41-41)
  • Clock (42-42)
src/app/features/page.tsx (7)
src/components/landing/Navbar.tsx (1)
  • Navbar (10-174)
src/components/features/FeaturesHero.tsx (1)
  • FeaturesHero (5-28)
src/components/features/FeaturesList.tsx (1)
  • FeaturesList (6-39)
src/components/features/BenefitsSection.tsx (1)
  • BenefitsSection (7-30)
src/components/features/IntegrationsGrid.tsx (1)
  • IntegrationsGrid (6-31)
src/components/features/FeaturesCTA.tsx (1)
  • FeaturesCTA (7-40)
src/components/landing/Footer.tsx (1)
  • Footer (5-159)
src/app/careers/page.tsx (5)
src/components/careers/CareersHero.tsx (1)
  • CareersHero (5-28)
src/components/careers/ValuesCards.tsx (1)
  • ValuesCards (6-39)
src/components/careers/BenefitsGrid.tsx (1)
  • BenefitsGrid (7-41)
src/components/careers/PositionsList.tsx (1)
  • PositionsList (8-78)
src/components/careers/CareersCTA.tsx (1)
  • CareersCTA (6-31)
src/app/contact/page.tsx (5)
src/components/landing/Navbar.tsx (1)
  • Navbar (10-174)
src/components/contact/ContactForm.tsx (1)
  • ContactForm (17-308)
src/components/contact/ContactInfo.tsx (1)
  • ContactInfo (8-93)
src/types/common.ts (1)
  • ContactInfo (88-93)
src/components/landing/Footer.tsx (1)
  • Footer (5-159)
src/app/blog/loading.tsx (2)
src/components/landing/Navbar.tsx (1)
  • Navbar (10-174)
src/components/landing/Footer.tsx (1)
  • Footer (5-159)
src/components/about/ValuesSection.tsx (3)
src/constants/about.ts (1)
  • ABOUT_VALUES (5-26)
src/constants/index.ts (4)
  • Shield (15-15)
  • Target (16-16)
  • Users (17-17)
  • Zap (18-18)
src/components/about/index.ts (1)
  • ValuesSection (3-3)
src/components/about/StatsSection.tsx (3)
src/components/about/index.ts (1)
  • StatsSection (6-6)
src/types/common.ts (1)
  • StatItem (24-27)
src/constants/about.ts (1)
  • ABOUT_STATS (60-64)
src/constants/showcase.ts (1)
src/constants/index.ts (8)
  • ShoppingBag (29-29)
  • Watch (30-30)
  • Pill (31-31)
  • Shirt (32-32)
  • Package (33-33)
  • Users (17-17)
  • Globe (26-26)
  • TrendingUp (34-34)
src/components/legal/LegalSection.tsx (1)
src/components/legal/index.ts (1)
  • LegalSection (2-2)
src/constants/features.ts (1)
src/constants/index.ts (12)
  • Shield (15-15)
  • QrCode (19-19)
  • Smartphone (20-20)
  • BarChart3 (21-21)
  • Brain (22-22)
  • Wallet (23-23)
  • MapPin (24-24)
  • Lock (25-25)
  • Globe (26-26)
  • Zap (18-18)
  • Sparkles (27-27)
  • CheckCircle2 (28-28)
src/types/common.ts (2)
src/components/contact/ContactInfo.tsx (1)
  • ContactInfo (8-93)
src/components/contact/index.ts (1)
  • ContactInfo (4-4)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 40-40: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 41-41: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🪛 GitHub Check: Lint
src/components/blog/BlogPagination.tsx

[warning] 21-21:
'searchParams' is assigned a value but never used

🪛 markdownlint-cli2 (0.18.1)
src/components/blog/README.md

57-57: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


123-123: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Comment thread src/app/api/contact/route.ts Outdated
Comment thread src/app/blog/loading.tsx
Comment thread src/app/blog/page.tsx Outdated
Comment thread src/components/about/AboutHero.tsx
Comment thread src/components/about/StatsSection.tsx Outdated
Comment thread src/components/contact/ContactReasons.tsx
Comment thread src/components/features/IntegrationsGrid.tsx
Comment thread src/components/pricing/PricingCTA.tsx
Comment thread src/lib/services/ghost.ts
Comment thread src/lib/services/ghost.ts Outdated
@inact25

inact25 commented Dec 15, 2025

Copy link
Copy Markdown
Collaborator Author

OTW kantor dulu njir, tar lagi

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/ai-agent.ts (1)

1-734: Unrelated changes in this PR.

This file contains only type annotation updates to an AI agent service, which appears unrelated to the PR's stated objective of implementing public pages and blog integration. While the PR comment mentions a "full refactor," mixing unrelated changes can complicate review and obscure the PR's primary intent.

Consider isolating type-system improvements in a dedicated PR to maintain clearer change history and easier rollback if needed.

♻️ Duplicate comments (1)
src/lib/services/ghost.ts (1)

36-39: Past review comment remains unresolved: Ghost API URL needs path suffix.

The previous review flagged that GHOST_API_URL should append /ghost/api/content to NEXT_PUBLIC_GHOST_URL. This issue is still present in the current code. Line 37 assigns the environment variable directly without appending the required API path, which will cause incorrect endpoint construction in buildApiUrl (line 57).

Refer to the past review comment for the suggested fix.

🧹 Nitpick comments (14)
src/components/about/StatsSection.tsx (1)

14-21: Consider using index as the key for better robustness.

Currently, stat.label is used as the React key. While the labels in ABOUT_STATS are unique today, this approach is fragile—if the data is ever updated with duplicate or changing labels, React will issue warnings or exhibit unexpected behavior.

Apply this diff to use the index as the key:

-        <MotionDiv
-          key={stat.label}
+        <MotionDiv
+          key={index}
           className="text-center p-6 bg-white border border-[#2B4C7E]/20 rounded-xl shadow-md"

Alternatively, add a unique id field to each stat object in ABOUT_STATS for even better semantics.

src/components/pricing/PricingCTA.tsx (1)

11-36: Consider extracting color values to Tailwind theme.

The component uses hard-coded arbitrary color values (#2B4C7E, #1E3A5F, #0C2340, #606060) in multiple locations. For better maintainability and consistency, consider defining these in your tailwind.config theme and referencing them as semantic color names.

Example theme configuration:

// tailwind.config.ts
theme: {
  extend: {
    colors: {
      brand: {
        primary: '#2B4C7E',
        'primary-dark': '#1E3A5F',
        navy: '#0C2340',
      },
      gray: {
        medium: '#606060',
      }
    }
  }
}

Then use:

-className="... bg-[#2B4C7E] ... hover:bg-[#1E3A5F] ..."
+className="... bg-brand-primary ... hover:bg-brand-primary-dark ..."
src/lib/tag-stamping.ts (1)

93-111: TypeScript can infer this type automatically.

The explicit type annotation (product: (typeof products)[number]) is redundant. TypeScript's type inference automatically determines the element type from the products array being mapped over, making the annotation unnecessary.

Apply this diff to simplify:

-  const productInfos: TagProductInfo[] = products.map(
-    (product: (typeof products)[number]) => {
+  const productInfos: TagProductInfo[] = products.map((product) => {
       const metadata = product.metadata as ProductMetadata;
       return {
         id: product.id,
src/lib/nft-collectible.ts (1)

290-290: TypeScript can infer this type automatically.

The explicit type annotation (p: (typeof products)[number]) is redundant. TypeScript's type inference automatically determines the element type from the products array being mapped over.

Apply this diff to simplify:

-      products: products.map((p: (typeof products)[number]) => ({
+      products: products.map((p) => ({
         code: p.code,
src/app/api/contact/route.ts (4)

30-33: Consider a more unique fallback for rate limiting.

The 'anonymous' fallback means all clients without IP headers share the same rate limit bucket. This could cause legitimate users to be rate-limited due to others' requests, or conversely allow bypass if headers are spoofed.

Consider using additional entropy (e.g., user-agent hash) or returning an error when no IP is available:

     const identifier =
       request.headers.get('x-forwarded-for') ||
       request.headers.get('x-real-ip') ||
-      'anonymous';
+      `anon-${request.headers.get('user-agent')?.slice(0, 50) || 'unknown'}`;

100-112: TODO noted: Email/storage integration pending.

The placeholder implementation is reasonable for initial development. The artificial delay (line 107) should be removed once actual email sending or database storage is implemented.

Would you like me to help implement the email sending integration (e.g., using Resend, SendGrid, or Nodemailer) or open an issue to track this task?


123-142: LGTM with a suggestion for production observability.

Good security practice returning generic errors to clients. Consider adding production error logging to a monitoring service (e.g., Sentry) for visibility into failures:

// Example addition for production
if (process.env.NODE_ENV === 'production') {
  // Log to monitoring service
  // captureException(error);
}

150-164: Consider deriving validation rules from the source of truth.

The validation rules in this response are hardcoded strings that could drift from the actual validation logic in contact.ts. Consider exporting validation constants from the validation module and using them here to maintain consistency.

// In contact.ts, export constants:
export const VALIDATION_RULES = {
  name: { min: 2, max: 100, pattern: 'letters only' },
  // ...
};

// In route.ts GET handler:
import { VALIDATION_RULES } from '@/lib/validations/contact';
// Use VALIDATION_RULES to build the response
src/lib/actions/support-tickets.ts (1)

26-39: Refactoring looks good; consider batching product queries.

The direct return and explicit type annotation (typeof nfts)[number] improve code clarity and type safety.

However, the current implementation exhibits an N+1 query pattern: for each NFT, a separate prisma.product.findMany() call is made. If a wallet owns many NFTs, this could result in numerous sequential database queries, degrading performance.

Consider this optimization:

return await Promise.all(
  nfts.map(async (nft: (typeof nfts)[number]) => {
    const productIds = (nft.tag.product_ids as number[]) || [];
    const products = await prisma.product.findMany({
      where: { id: { in: productIds } },
      include: { brand: true },
    });
    return {
      ...nft,
      products,
    };
  })
);

Refactor to:

// Collect all unique product IDs
const allProductIds = Array.from(
  new Set(
    nfts.flatMap((nft) => ((nft.tag.product_ids as number[]) || []))
  )
);

// Fetch all products in one query
const allProducts = await prisma.product.findMany({
  where: { id: { in: allProductIds } },
  include: { brand: true },
});

// Build product lookup map
const productMap = new Map(allProducts.map((p) => [p.id, p]));

// Map products back to NFTs
return nfts.map((nft) => {
  const productIds = (nft.tag.product_ids as number[]) || [];
  const products = productIds
    .map((id) => productMap.get(id))
    .filter((p): p is NonNullable<typeof p> => p != null);
  return {
    ...nft,
    products,
  };
});

This reduces database round-trips from O(N) to O(1).

src/lib/ai-agent.ts (3)

30-30: Consider removing redundant type annotations.

TypeScript can infer callback parameter types from Prisma's generated types, making these explicit annotations unnecessary. They add maintenance overhead if the schema evolves.

For example, at line 30:

-    .filter((tag: { id: number; product_ids: unknown }) => {
+    .filter((tag) => {

Apply similar changes at lines 36, 350, and 497.

Also applies to: 36-36, 350-350, 497-497


216-217: Indexed access type annotations are redundant.

The (typeof array)[number] pattern is valid but unnecessary here—TypeScript already infers element types from Prisma results. Removing these annotations simplifies the code and maintains consistency.

Example at line 216:

-    .filter((g: (typeof locationGroups)[number]) => g.location_name)
+    .filter((g) => g.location_name)

Apply similar changes at lines 217, 412, 419, 465, 474, and 481.

Also applies to: 412-412, 419-419, 465-465, 474-474, 481-481


459-461: Simplify by removing redundant annotations.

Both type annotations in this promise chain are unnecessary. TypeScript infers the types from Prisma's query result.

-      .then((products: { id: number }[]) =>
-        products.map((p: { id: number }) => p.id)
-      );
+      .then((products) => products.map((p) => p.id));
src/app/api/verify/route.ts (1)

317-325: Consider extracting the location filter predicate (optional).

The filter logic for valid location names is duplicated here (lines 321-325) and earlier (lines 254-258). Extracting it to a small helper would improve maintainability.

Example refactor:

// At the top of the function or in a separate utils file
const isValidLocationName = (name: unknown): name is string =>
  typeof name === 'string' && name.length > 0;

// Then use it:
const uniqueRecentLocations = new Set(
  recentScans.map((s) => s.location_name).filter(isValidLocationName)
);

This keeps the logic DRY and makes the intent clearer.

src/components/blog/BlogError.tsx (1)

13-18: Consider using a retry callback instead of full page reload.

window.location.reload() triggers a full page reload, discarding all client state, form inputs, and scroll position. For better UX, accept an onRetry callback prop and call router.refresh() (Next.js 13+) or let the parent component handle retry logic.

Apply this diff:

 'use client';

 import React from 'react';
+import { useRouter } from 'next/navigation';

 interface BlogErrorProps {
   message: string;
+  onRetry?: () => void;
 }

-export function BlogError({ message }: BlogErrorProps) {
+export function BlogError({ message, onRetry }: BlogErrorProps) {
+  const router = useRouter();
+
+  const handleRetry = () => {
+    if (onRetry) {
+      onRetry();
+    } else {
+      router.refresh();
+    }
+  };
+
   return (
     <div className="bg-red-50 border border-red-200 rounded-xl p-8 text-center max-w-2xl mx-auto">
       <p className="text-red-600">{message}</p>
       <button
-        onClick={() => window.location.reload()}
+        onClick={handleRetry}
         className="mt-4 px-6 py-2 bg-[#2B4C7E] text-white rounded-lg hover:bg-[#1E3A5F] transition-colors"
       >
         Coba Lagi
       </button>
     </div>
   );
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c1406dc and a93e09a.

📒 Files selected for processing (20)
  • src/app/api/contact/route.ts (1 hunks)
  • src/app/api/verify/route.ts (2 hunks)
  • src/app/blog/page.tsx (1 hunks)
  • src/components/about/AboutHero.tsx (1 hunks)
  • src/components/about/StatsSection.tsx (1 hunks)
  • src/components/blog/BlogError.tsx (1 hunks)
  • src/components/contact/ContactReasons.tsx (1 hunks)
  • src/components/features/IntegrationsGrid.tsx (1 hunks)
  • src/components/pricing/PricingCTA.tsx (1 hunks)
  • src/constants/about.ts (1 hunks)
  • src/lib/actions/dashboard.ts (2 hunks)
  • src/lib/actions/onboarding.ts (3 hunks)
  • src/lib/actions/products.ts (1 hunks)
  • src/lib/actions/support-tickets.ts (1 hunks)
  • src/lib/actions/tags.ts (9 hunks)
  • src/lib/ai-agent.ts (7 hunks)
  • src/lib/fraud-analysis-cache.ts (1 hunks)
  • src/lib/nft-collectible.ts (2 hunks)
  • src/lib/services/ghost.ts (1 hunks)
  • src/lib/tag-stamping.ts (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/lib/fraud-analysis-cache.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/constants/about.ts
  • src/app/blog/page.tsx
  • src/components/contact/ContactReasons.tsx
🧰 Additional context used
🧬 Code graph analysis (8)
src/lib/actions/products.ts (1)
src/lib/db.ts (1)
  • prisma (7-14)
src/lib/tag-stamping.ts (1)
src/lib/product-templates.ts (1)
  • ProductMetadata (204-210)
src/app/api/contact/route.ts (2)
src/lib/rate-limit.ts (1)
  • checkRateLimit (41-84)
src/lib/validations/contact.ts (2)
  • ContactFormData (5-11)
  • validateContactForm (83-135)
src/components/features/IntegrationsGrid.tsx (2)
src/components/features/index.ts (1)
  • IntegrationsGrid (4-4)
src/constants/features.ts (1)
  • FEATURES_INTEGRATIONS (119-126)
src/components/about/StatsSection.tsx (3)
src/components/about/index.ts (1)
  • StatsSection (6-6)
src/types/common.ts (1)
  • StatItem (24-27)
src/constants/about.ts (1)
  • ABOUT_STATS (60-64)
src/components/pricing/PricingCTA.tsx (1)
src/components/pricing/index.ts (1)
  • PricingCTA (5-5)
src/lib/actions/dashboard.ts (1)
src/lib/db.ts (1)
  • prisma (7-14)
src/components/about/AboutHero.tsx (1)
src/components/about/index.ts (1)
  • AboutHero (1-1)
🪛 GitHub Actions: CI
src/lib/actions/dashboard.ts

[warning] 108-108: ESLint: Unused eslint-disable directive (no problems were reported from '@typescript-eslint/no-explicit-any')


[error] 110-110: ESLint: Unexpected any. Specify a different type (@typescript-eslint/no-explicit-any)

🔇 Additional comments (17)
src/components/about/StatsSection.tsx (1)

10-10: Type safety issue resolved—well done!

The unsafe double type assertion (as unknown as StatItem[]) from the previous review has been correctly fixed. The direct assignment now works because ABOUT_STATS is defined as a plain array (without as const) and StatItem uses mutable properties.

src/components/features/IntegrationsGrid.tsx (1)

1-40: Accessibility concern resolved — implementation looks good!

The previous review comment flagged missing motion preference support. The current implementation correctly addresses this by:

  • Importing and calling useReducedMotion() (lines 3, 7)
  • Conditionally setting initial to skip the scale animation when motion is reduced (lines 18-22)
  • Setting transition duration to 0 and removing delays when motion is reduced (lines 25-29)

This ensures users with prefers-reduced-motion preferences won't experience disorienting animations. The component is well-structured and follows best practices.

src/components/pricing/PricingCTA.tsx (1)

8-17: Excellent accessibility fix!

The implementation now correctly respects user motion preferences using useReducedMotion(). When reduced motion is preferred, the component renders without animation (initial state matches final state, duration is 0). This fully addresses the previous major accessibility concern.

src/components/about/AboutHero.tsx (1)

3-3: Accessibility concern successfully addressed!

The implementation now correctly respects the user's prefers-reduced-motion preference. The useReducedMotion hook is imported and applied to both motion elements, ensuring animations are disabled (zero duration, no initial offset) when users request reduced motion. This resolves the accessibility concern from the previous review.

Also applies to: 9-9, 14-16, 22-26

src/lib/nft-collectible.ts (1)

8-8: Good cleanup of unused import.

Removing the unused getFileUrl import keeps the codebase clean.

src/app/api/contact/route.ts (4)

1-16: LGTM!

Imports are appropriate and the rate limit configuration (3 requests per 15 minutes) is reasonable for protecting against contact form spam.


46-51: Confirmed fix: retryAfter is now used correctly.

The previous bug where retryAfter was divided by 1000 twice has been resolved. The value from checkRateLimit is already in seconds, and it's now correctly passed to the Retry-After header without additional conversion.


56-81: LGTM!

Good defensive parsing with proper error handling for malformed JSON. The subsequent validation will catch any structural issues with the data.


83-98: LGTM!

Correct approach: sanitize input before validation, and return structured field-level errors that enable good client-side UX.

src/lib/actions/support-tickets.ts (1)

17-40: Verify that this change belongs in this PR.

This file handles support ticket actions and NFT ownership, but the PR objectives focus on implementing public pages (features, pricing, blog, security, terms, etc.) and Ghost CMS integration. There's no mention of support tickets or NFT-related changes in the PR description.

Is this change intentional for this PR, or should it be moved to a separate pull request focused on support ticket improvements?

src/lib/actions/tags.ts (1)

55-55: LGTM! Type annotations improve type safety.

The explicit type annotations on map/filter callbacks throughout this file eliminate implicit any types and improve type inference. These changes are consistent with the PR's broader type-safety enhancements and introduce no behavioral changes.

Also applies to: 112-115, 123-137, 783-813, 859-933

src/lib/services/ghost.ts (3)

53-65: buildApiUrl implementation is sound.

The URL construction logic is correct, assuming GHOST_API_URL is properly configured (see earlier comment).


74-116: getGhostPosts implementation is correct.

The function properly handles pagination, timeout, and error cases. Note that slug and html fields (optional in GhostPost) are not fetched by default, which is appropriate for listing views.


121-141: LGTM! User-friendly error messages.

The Indonesian error messages are clear and appropriate for end users.

src/lib/actions/products.ts (1)

411-419: LGTM! Type annotations improve type safety.

Explicit type annotations eliminate implicit any types and align with the PR's broader type-safety enhancements.

src/lib/actions/dashboard.ts (1)

115-116: Verify the hardcoded brands count for brand users.

Line 116 now always returns brands: 1 for brand users. This is a functional change from previous behavior. Confirm this is the intended UX (brand users always see "1 brand" in their dashboard stats) rather than an unintended side effect of the refactoring.

src/lib/actions/onboarding.ts (1)

91-91: LGTM! Type annotations improve type safety.

Explicit type annotations on map/find callbacks eliminate implicit any types and align with the PR's broader type-safety improvements.

Also applies to: 325-327, 399-405

Comment thread src/app/api/verify/route.ts
Comment thread src/app/api/verify/route.ts
Comment thread src/lib/actions/dashboard.ts
Comment thread src/lib/actions/dashboard.ts Outdated
Comment thread src/lib/services/ghost.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app/manage/nfts/page.tsx (1)

119-133: Missing relative class on parent container for fill layout.

The Image component with fill prop requires its parent to have position: relative. The container div is missing this class, which may cause the image to position incorrectly or overflow.

 <TableCell>
-  <div className="w-12 h-12 rounded-lg overflow-hidden bg-gradient-to-br from-purple-500/20 to-pink-500/20">
+  <div className="w-12 h-12 rounded-lg overflow-hidden bg-gradient-to-br from-purple-500/20 to-pink-500/20 relative">
     {nft.imageUrl ? (
       <Image
         src={nft.imageUrl}
         alt={`NFT #${nft.tokenId}`}
         fill
         className="object-cover"
         unoptimized
       />
src/app/manage/nfts/[id]/page.tsx (1)

68-80: Missing relative class on parent container for fill layout.

The Image component with fill prop requires its parent to have position: relative. The container div at line 68 is missing this class.

 <CardContent className="p-0">
-  <div className="aspect-square bg-gradient-to-br from-purple-500/20 to-pink-500/20 flex items-center justify-center">
+  <div className="aspect-square bg-gradient-to-br from-purple-500/20 to-pink-500/20 flex items-center justify-center relative">
     {nft.imageUrl ? (
       <Image
         src={nft.imageUrl}
         alt={`NFT #${nft.tokenId}`}
         fill
         className="object-cover"
         unoptimized
       />
🧹 Nitpick comments (1)
src/components/blog/BlogPagination.tsx (1)

39-77: Pagination logic is solid; minor comment inaccuracy.

The ellipsis-based pagination algorithm correctly handles start, middle, and end cases. However, the maxVisible = 7 comment is slightly misleading—near-start and near-end cases display 6 buttons, while only the middle case shows 7.

Consider clarifying the comment:

-   const maxVisible = 7; // Maximum number of page buttons to show
+   const maxVisible = 7; // Maximum number of page buttons to show (middle position)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a93e09a and 728a0a6.

📒 Files selected for processing (7)
  • src/app/manage/nfts/[id]/page.tsx (3 hunks)
  • src/app/manage/nfts/page.tsx (2 hunks)
  • src/app/manage/tickets/components/sidebar-cards.tsx (2 hunks)
  • src/app/support/components/new-ticket-form.tsx (2 hunks)
  • src/app/verify/[code]/components/nft-claim-card.tsx (3 hunks)
  • src/components/blog/BlogPagination.tsx (1 hunks)
  • src/lib/actions/dashboard.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/actions/dashboard.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/blog/BlogPagination.tsx (2)
src/components/blog/index.ts (1)
  • BlogPagination (2-2)
src/components/ui/button.tsx (1)
  • Button (60-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
src/app/manage/tickets/components/sidebar-cards.tsx (1)

169-177: LGTM!

The Next.js Image migration is correctly implemented. The parent container has relative positioning required for fill, and aspect-square maintains proper dimensions. Using unoptimized is appropriate for external NFT images.

src/app/support/components/new-ticket-form.tsx (1)

125-132: LGTM!

The Image component is correctly configured with explicit width and height props instead of fill, which is a valid approach for fixed-size thumbnails. The existing styling classes are properly preserved.

src/app/verify/[code]/components/nft-claim-card.tsx (2)

190-198: LGTM!

The Image migration is correctly implemented with proper relative positioning on the parent container for the fill layout mode.


228-236: LGTM!

Consistent implementation with the first Image instance - parent has relative positioning and proper fill/cover behavior.

src/app/manage/nfts/[id]/page.tsx (1)

191-198: LGTM!

The brand logo Image correctly uses fixed width and height props instead of fill, so no relative positioning is needed on the parent.

src/components/blog/BlogPagination.tsx (1)

81-141: Clean pagination UI with proper disabled states.

The render logic is well-structured with clear visual feedback for active/inactive states and proper disabling of Previous/Next buttons at boundaries. The Indonesian labels ("Sebelumnya", "Selanjutnya") align with the localization evident throughout the PR.

Comment thread src/components/blog/BlogPagination.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/components/blog/BlogPagination.tsx (1)

22-27: Good improvement on navigation timing; verify if router.refresh() is needed.

You've successfully addressed the race condition from the previous review by awaiting router.push() and removing the setTimeout workaround. The navigation flow is now more predictable.

However, router.refresh() on Line 25 may be redundant. According to Next.js documentation, router.push() already triggers re-rendering of Server Components for the new route. Unless you have a specific need to force server-cache revalidation beyond what push() provides, consider removing this call to simplify the code.

Run the following test to verify whether removing router.refresh() affects your blog pagination behavior:

#!/bin/bash
# Description: Check if blog posts use server-side revalidation or time-based cache settings
# that might require explicit refresh() calls.

# Search for revalidation configuration in blog-related files
rg -n "revalidate|cache" --type ts --type tsx -g "**/blog/**" -C 2

# Check for any fetch calls with cache options in blog pages
rg -n "fetch.*cache" --type ts --type tsx -g "**/blog/**" -C 2
🧹 Nitpick comments (3)
src/components/blog/BlogPagination.tsx (3)

43-64: Consider centering page ranges around currentPage for consistency.

The pagination logic uses fixed ranges for "near start" (pages 2-4) and "near end" (pages totalPages-3 to totalPages-1) cases, while the "middle" case dynamically centers around currentPage. This means when a user is on page 3, they see [1, 2, 3, 4, ..., N], but when they move to page 4, the range suddenly jumps to [1, ..., 3, 4, 5, ..., N].

For more consistent UX, consider making all cases center around currentPage with a fixed window size, similar to how the middle case works.

Example refactor:

  if (totalPages <= maxVisible) {
    // Show all pages if total is small
    for (let i = 1; i <= totalPages; i++) {
      pages.push(i);
    }
  } else {
    // Always show first page
    pages.push(1);
    
-   if (currentPage <= 3) {
-     // Near the start
-     for (let i = 2; i <= 4; i++) {
-       pages.push(i);
-     }
-     pages.push('...');
-     pages.push(totalPages);
-   } else if (currentPage >= totalPages - 2) {
-     // Near the end
-     pages.push('...');
-     for (let i = totalPages - 3; i <= totalPages; i++) {
-       pages.push(i);
-     }
-   } else {
-     // In the middle
+   const showFirst = currentPage <= 4;
+   const showLast = currentPage >= totalPages - 3;
+   
+   if (!showFirst) {
      pages.push('...');
+   }
+   
+   const start = Math.max(2, currentPage - 1);
+   const end = Math.min(totalPages - 1, currentPage + 1);
+   
+   for (let i = start; i <= end; i++) {
+     pages.push(i);
+   }
+   
+   if (!showLast) {
+     pages.push('...');
+   }
+   
+   if (totalPages > 1) {
+     pages.push(totalPages);
-     pages.push(currentPage - 1);
-     pages.push(currentPage);
-     pages.push(currentPage + 1);
-     pages.push('...');
-     pages.push(totalPages);
    }
  }

86-97: Use more stable keys for ellipsis elements.

The ellipsis elements use index in their keys (ellipsis-${index}), which can change as the pagination structure shifts between pages. This may cause React to unnecessarily unmount and remount these elements when navigating.

Apply this diff for more stable keys:

  if (page === '...') {
    return (
      <span
-       key={`ellipsis-${index}`}
+       key={`ellipsis-${pageNumbers[index - 1]}-${pageNumbers[index + 1]}`}
        className="px-3 py-2 text-[#606060]"
      >
        ...
      </span>
    );
  }

This creates keys based on the adjacent page numbers (e.g., ellipsis-1-2 or ellipsis-5-10), which remain stable as long as the ellipsis appears between the same pages.


102-117: Add accessibility attributes for screen reader support.

The page number buttons lack semantic indicators for assistive technologies. Screen reader users won't know which page is currently active or what each button does without visual context.

Apply this diff to improve accessibility:

  return (
    <button
      key={pageNum}
      onClick={() => navigateToPage(pageNum)}
+     aria-label={isActive ? `Page ${pageNum}, current page` : `Go to page ${pageNum}`}
+     aria-current={isActive ? 'page' : undefined}
      className={`
        min-w-[40px] h-[40px] rounded-lg font-medium transition-all
        ${
          isActive
            ? 'bg-[#2B4C7E] text-white shadow-lg'
            : 'bg-white border border-[#A8A8A8]/30 text-[#0C2340] hover:border-[#2B4C7E]/50 hover:bg-[#2B4C7E]/5'
        }
      `}
    >
      {pageNum}
    </button>
  );

Similarly, add aria-label to the Previous and Next buttons (lines 75-83, 122-130) with Indonesian text like aria-label="Halaman sebelumnya" and aria-label="Halaman selanjutnya" for consistency with the visible button text.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 728a0a6 and ab1b0a5.

📒 Files selected for processing (1)
  • src/components/blog/BlogPagination.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

@inact25 inact25 requested a review from igun997 December 16, 2025 03:56
@igun997 igun997 merged commit ecbd1a1 into cds-id:develop Dec 16, 2025
6 checks passed
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.

2 participants