Implement all basic public page#44
Conversation
feat/basic-page-implementation
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
|||||||||||||||||||||||||
PR Code Suggestions ✨Explore these optional code suggestions:
|
|||||||||||||||
feat/basic-page-implementation
igun997
left a comment
There was a problem hiding this comment.
Jir lah itu cek lagi cok
feat/basic-page-implementation
WalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–75 minutes Areas to focus review on:
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
|
urang full refactor sekalian benerin ssr seo |
There was a problem hiding this comment.
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 isPOSTS_PER_PAGEwhich is12.- * @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 localBlogPostinterface and useGhostPostfrom the Ghost service instead.The local
BlogPostinterface duplicates theGhostPosttype already defined insrc/lib/services/ghost.ts. Sincesrc/app/blog/page.tsximportsGhostPostand passes it directly toBlogGrid, the component should acceptGhostPost[]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_TOKENis quite generic and could be confused with other tokens in the future. Consider renaming toNEXT_PUBLIC_GHOST_CONTENT_API_KEYfor clarity and to match Ghost's documentation terminology.The static analysis warning about quote characters is a false positive for
.env.examplefiles where quotes in placeholder values are conventional.next.config.ts (1)
26-30: Hardcoded hostname inconsistent with environment variable approach.The hostname
blog.javapixa.comis hardcoded here, but the PR also introducesNEXT_PUBLIC_GHOST_URLas 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
remotePatternsdoesn'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!assumingvalidateConfig()was called. While current callers do validate first, this is fragile. Consider validating withinbuildApiUrlor 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.
getGhostPoststhrows on errors (allowing callers to handle), whilegetGhostPostBySlugsilently returnsnulland only logs in development. This inconsistency can make debugging production issues difficult and may hide configuration problems.Consider either:
- Making both functions throw and letting callers decide how to handle errors
- Adding an optional
throwOnErrorparameter for consistencysrc/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 inABOUT_VALUESchanges, 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
MotionDivwrapper is unnecessary and can be replaced with direct usage ofmotion.divin 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_TOKENis overly generic and could conflict with other tokens in the project (auth tokens, CSRF tokens, etc.). Ghost CMS typically uses more descriptive names likeGHOST_CONTENT_API_KEY.Consider renaming to:
NEXT_PUBLIC_GHOST_API_KEYor
NEXT_PUBLIC_GHOST_CONTENT_API_KEYThis 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 genericHeroSectioncomponent that acceptstitleanddescriptionprops.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
MotionH1andMotionPaliases adds no value and reduces code clarity. Framer Motion'smotion.h1andmotion.pare 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 inPRICING_FAQSand theFAQIteminterface.Consider one of these approaches:
- Update
FAQItemto accept readonly properties if that's the mismatch- Remove the const assertion from
PRICING_FAQSif mutability isn't required- 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.questionas 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
idfield toFAQItem:-key={faq.question} +key={index}Or better, if feasible, extend the
FAQIteminterface to include anidfield.
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 Diajukansrc/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 onCONTACT_INFOin 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.sundaywith hardcoded structure. IfCONTACT_HOURSis refactored (e.g., to an array), this breaks.Consider mapping over hours dynamically if
CONTACT_HOURSwere 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 explicitaria-labelfor 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:
- Reducing blur radius
- Using smaller elements
- Using CSS
will-change: transformorcontain: layoutto isolate repaints- Rendering blurs as static images
src/components/about/TeamSection.tsx (3)
8-8: Unnecessary alias for motion.div.The
MotionDivalias doesn't add value and adds an extra lookup step.Remove the alias and use
motion.divdirectly:-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
idfield:-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
BackgroundEffectscomponent 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 becauseSHOWCASE_ITEMSusesas const(making it readonly), butShowcaseItemexpects mutable types.Consider either:
- Using
Readonly<ShowcaseItem>[]orReadonlyArray<ShowcaseItem>for the local type- Removing
as constfromSHOWCASE_ITEMSif 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 ifABOUT_TECH_STACKis already properly typed in the constants file. Based on the relevant code snippets,ABOUT_TECH_STACKis defined withas const, which should be compatible withTechStackItem[].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,TESTIMONIALSis typed withas constand should be compatible withTestimonialItem[].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. SincePRICING_PLANSis defined withas const, you can use a type assertion that preserves safety or adjust the type definition.Consider either:
- Remove
as constfrom the source definition if mutability isn't needed- 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_TOKENis 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 KeyThis would require updating references in
.env.exampleandsrc/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
ias 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 becauseJOB_POSITIONSis defined withas const, which makes the array readonly and narrows string types to literal types (e.g.,'Senior Blockchain Engineer'instead ofstring). The mutableJobPosition[]type cannot accept a readonly array directly, requiring the intermediateunknowncast 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 constassertion, making the array mutable and compatible withJobPosition[], eliminating the double cast inPositionsList.tsx.src/components/blog/BlogGrid.tsx (1)
7-7: Remove unnecessary alias.The
MotionDivalias doesn't provide any value and adds an extra layer of indirection. Usemotion.divdirectly 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 unusedsearchParamsvariable.The
searchParamsconstant is declared but never used in the component, as URLs are constructed manually in thenavigateToPagefunction.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
⛔ Files ignored due to path filters (1)
package-lock.jsonis 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)
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)
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
|
OTW kantor dulu njir, tar lagi |
There was a problem hiding this comment.
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_URLshould append/ghost/api/contenttoNEXT_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 inbuildApiUrl(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.labelis used as the React key. While the labels inABOUT_STATSare 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
idfield to each stat object inABOUT_STATSfor 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 yourtailwind.configtheme 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 theproductsarray 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 theproductsarray 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 responsesrc/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 anonRetrycallback prop and callrouter.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
📒 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 becauseABOUT_STATSis defined as a plain array (withoutas const) andStatItemuses 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
initialto skip the scale animation when motion is reduced (lines 18-22)- Setting
transitionduration to0and removing delays when motion is reduced (lines 25-29)This ensures users with
prefers-reduced-motionpreferences 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-motionpreference. TheuseReducedMotionhook 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
getFileUrlimport 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:retryAfteris now used correctly.The previous bug where
retryAfterwas divided by 1000 twice has been resolved. The value fromcheckRateLimitis already in seconds, and it's now correctly passed to theRetry-Afterheader 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
anytypes 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_URLis properly configured (see earlier comment).
74-116: getGhostPosts implementation is correct.The function properly handles pagination, timeout, and error cases. Note that
slugandhtmlfields (optional inGhostPost) 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
anytypes 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: 1for 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
anytypes and align with the PR's broader type-safety improvements.Also applies to: 325-327, 399-405
There was a problem hiding this comment.
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: Missingrelativeclass on parent container forfilllayout.The
Imagecomponent withfillprop requires its parent to haveposition: 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: Missingrelativeclass on parent container forfilllayout.The
Imagecomponent withfillprop requires its parent to haveposition: 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 = 7comment 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
📒 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
relativepositioning required forfill, andaspect-squaremaintains proper dimensions. Usingunoptimizedis 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
widthandheightprops instead offill, 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
relativepositioning on the parent container for thefilllayout mode.
228-236: LGTM!Consistent implementation with the first Image instance - parent has
relativepositioning and proper fill/cover behavior.src/app/manage/nfts/[id]/page.tsx (1)
191-198: LGTM!The brand logo Image correctly uses fixed
widthandheightprops instead offill, 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.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/components/blog/BlogPagination.tsx (1)
22-27: Good improvement on navigation timing; verify ifrouter.refresh()is needed.You've successfully addressed the race condition from the previous review by awaiting
router.push()and removing thesetTimeoutworkaround. 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 whatpush()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 aroundcurrentPagefor 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
currentPagewith 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
indexin 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-2orellipsis-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-labelto the Previous and Next buttons (lines 75-83, 122-130) with Indonesian text likearia-label="Halaman sebelumnya"andaria-label="Halaman selanjutnya"for consistency with the visible button text.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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
User description
PR Type
Enhancement, Documentation
Description
Implemented 10 new public pages:
features,pricing,showcase,careers,contact,about,blog,privacy,terms, andsecurityIntegrated Ghost CMS blog functionality with
BlogGrid,BlogPaginationcomponents and server-side data fetchingAdded 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
axiosdependency for HTTP requests to Ghost CMS APIUpdated 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
File Walkthrough
2 files
next.config.ts
Configure image domains for Ghost CMS blog integrationnext.config.ts
integration
blog.javapixa.comfor Ghost CMS blog imagesimages.unsplash.comfor Unsplash images used by Ghoststatic.ghost.orgfor Ghost CDN assets.env.example
Environment Variables for Ghost CMS Integration.env.example
NEXT_PUBLIC_GHOST_URLand
NEXT_PUBLIC_TOKEN14 files
index.ts
Add blog component exports and barrel filesrc/components/blog/index.ts
BlogGridcomponent for displaying blog postsBlogPaginationcomponent for pagination controlspage.tsx
Implement security documentation public pagesrc/app/security/page.tsx
sections
infrastructure, application security, compliance, and AI/fraud
detection
responsible disclosure policy
page.tsx
Implement Terms & Conditions public pagesrc/app/terms/page.tsx
activities, third-party services, pricing, liability, and intellectual
property
page.tsx
Implement showcase page with success storiessrc/app/showcase/page.tsx
industries
partners, 50+ countries)
page.tsx
Implement careers page with job listingssrc/app/careers/page.tsx
and design
Excellence)
insurance, remote work
page.tsx
Implement contact page with form integrationsrc/app/contact/page.tsx
Partnership)
expectations
page.tsx
Implement features showcase pagesrc/app/features/page.tsx
features
NFT collectibles
Prisma, NextAuth, Swagger)
page.tsx
Implement about page with company informationsrc/app/about/page.tsx
frameworks)
gas-free NFT minting)
page.tsx
Implement pricing page with subscription planssrc/app/pricing/page.tsx
Professional, Enterprise)
feature lists
table
page.tsx
Privacy Policy Page Implementation with Animated Sectionssrc/app/privacy/page.tsx
sections covering data collection, usage, security, and user rights
(information collection, usage, security, third-party sharing, user
rights, cookies & tracking)
framer-motionand Tailwind CSSNavbarandFootercomponents with background gradienteffects
BlogPagination.tsx
Blog Pagination Component with Smart Page Displaysrc/components/blog/BlogPagination.tsx
navigation buttons
pages with ellipsis for large page counts
query strings
page.tsx
Blog Page with Ghost CMS Integrationsrc/app/blog/page.tsx
NEXT_PUBLIC_TOKENenvironment variablepagination support (12 posts per page)
BlogGridandBlogPaginationcomponents with conditionalrendering based on data availability
BlogGrid.tsx
Blog Grid Component with Post Cardssrc/components/blog/BlogGrid.tsx
columns based on screen size
title, excerpt, and read more link
placeholder with Etags branding
loading.tsx
Blog Page Loading Skeleton UIsrc/app/blog/loading.tsx
placeholder elements
page design
3 files
README.md
Update documentation for public pages and blogREADME.md
and blog pages
README.md
Add blog component documentationsrc/components/blog/README.md
BlogGridandBlogPaginationcomponents with props andinterfaces
CLAUDE.md
Documentation Updates for New Public RoutesCLAUDE.md
/about,/features,/pricing,/showcase,/careers,/contact,/blog,/privacy,/terms, and/securityNEXT_PUBLIC_TOKENenvironment variable documentation for GhostCMS Content API integration
1 files
package.json
Add axios dependency for API requestspackage.json
axiosdependency version^1.13.2for HTTP requestsSummary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.