diff --git a/README.md b/README.md index 5eec78a..468294b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # LAB68DEV CV Builder -> A brutalist SaaS resume builder — engineered for speed, clarity, and pixel-perfect PDF output. +> A hand-drawn, doodle-style resume builder — engineered for speed, clarity, and pixel-perfect PDF output. lab68dev CV Builder trên Unikorn.vn @@ -39,12 +39,12 @@ ### Templates -| Template | Style | -| -------------------------- | --------------------------------------------------------------------- | -| **Harvard Style** | Classic academic — left date column with structured content blocks | -| **Creative / Portfolio** | Brutalist sidebar — black 2.5-inch sidebar with optional avatar photo | -| **ATS-Optimized** | Two-column clean layout optimized for Applicant Tracking Systems | -| **Executive / Leadership** | Traditional top-down with professional summary header | +| Template | Style | +| -------------------------- | ------------------------------------------------------------------ | +| **Harvard Style** | Classic academic — left date column with structured content blocks | +| **Creative / Portfolio** | Bold sidebar — black 2.5-inch sidebar with optional avatar photo | +| **ATS-Optimized** | Two-column clean layout optimized for Applicant Tracking Systems | +| **Executive / Leadership** | Traditional top-down with professional summary header | ### PDF Export @@ -63,33 +63,32 @@ ## Design Philosophy -Built on a **brutalist aesthetic** — raw, functional, and intentional: +The app looks like a **sketchbook**, while the CV stays clean and professional: -- Zero border radius — square corners everywhere -- Monochrome palette — black and white only -- Visible 1px borders on all interactive elements -- Inverted focus states — black background, white text on focus -- Monospace labels in uppercase with wide letter-spacing +- Wobbly hand-drawn borders with hard offset shadows +- Warm paper and ink colors, with blue pen and yellow sticky notes +- Shantell Sans for headings and buttons, IBM Plex Sans for body text +- Buttons that press down over their shadow when clicked -No smooth gradients. No rounded pills. Pure function. +CV templates and PDFs are kept plain for recruiters and ATS software. Anything inside `.cv-sheet` stays out of the doodle theme. --- ## Tech Stack -| Layer | Technology | -| ------------ | -------------------------------------------- | -| Framework | Next.js 16 (App Router, React 19, Turbopack) | -| Language | TypeScript (strict) | -| Database | Neon (Serverless Postgres) | -| ORM | Drizzle ORM | -| Auth | Auth.js v5 — Google sign-in | -| Styling | Tailwind CSS 4 | -| State | Zustand | -| PDF | @react-pdf/renderer | -| Image Upload | Cloudinary | -| i18n | next-intl (EN / VI) | -| Font | IBM Plex Sans (UI) + 11 Google Fonts (CV) | +| Layer | Technology | +| ------------ | -------------------------------------------------------- | +| Framework | Next.js 16 (App Router, React 19, Turbopack) | +| Language | TypeScript (strict) | +| Database | Neon (Serverless Postgres) | +| ORM | Drizzle ORM | +| Auth | Auth.js v5 — Google sign-in | +| Styling | Tailwind CSS 4 | +| State | Zustand | +| PDF | @react-pdf/renderer | +| Image Upload | Cloudinary | +| i18n | next-intl (EN / VI) | +| Font | Shantell Sans + IBM Plex Sans (UI), 11 Google Fonts (CV) | --- @@ -134,4 +133,4 @@ Licensed under the [Apache License 2.0](LICENSE). --- -Built with brutalist design principles by lab68dev. +Built by lab68dev. diff --git a/messages/en.json b/messages/en.json index 04b77b3..f291f31 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,171 +1,172 @@ { - "Index": { - "label": "SYS.INIT // LAB68CV.BUILDER STUDIO", - "title": "resume.build()", - "description": "Unlock your potential. Build your future. The ultimate Resume Builder engineered for professionals to create clean, structured, and ATS-friendly CVs in seconds.", - "enterLab": "Start Building →", - "tryFree": "Try Free →", - "viewTemplates": "View Templates", - "Features": { - "title": "SYSTEM SPECS", - "pdfTitle": "Instant PDF Export", - "pdfDesc": "Client-side rendering. Zero waiting time. Pixel-perfect brutalist PDFs generated directly in your browser.", - "atsTitle": "ATS-Optimized", - "atsDesc": "Clean semantic structures guaranteed to pass automated parsers and Applicant Tracking Systems.", - "templatesTitle": "Premium Layouts", - "templatesDesc": "Four professionally crafted architectural templates tailored for different career stages.", - "freeTitle": "Always Open", - "freeDesc": "No paywalls. No watermarks. Export your professional resume completely free, forever." - }, - "Import": { - "label": "PROFILE_IMPORT // NEW", - "title": "Import from Your Profiles", - "subtitle": "Start faster by pulling verified profile data into your CV draft.", - "githubTitle": "GitHub Import", - "githubDesc": "Fetch profile basics, top repositories, and inferred skills from your GitHub account.", - "linkedinTitle": "LinkedIn ZIP Import", - "linkedinDesc": "Upload your LinkedIn data export ZIP and map it into your resume fields instantly.", - "behanceTitle": "Behance Import", - "behanceDesc": "Bring portfolio projects and profile details from Behance to jumpstart your creative CV.", - "cta": "Try Import →" - }, - "Achievements": { - "label": "MILESTONES // VERIFIED", - "title": "#1 PLATFORM WINS", - "unikornRank": "#1 Product of the Day", - "forgRank": "#1 Product of the Week" - }, - "Templates": { - "title": "AVAILABLE TEMPLATES", - "executiveTitle": "Executive", - "executiveDesc": "Dense, high-information layout for senior roles.", - "harvardTitle": "Harvard Style", - "harvardDesc": "Traditional, academic, highly respected format.", - "creativeTitle": "Creative", - "creativeDesc": "Bold typography with avatar support for designers.", - "atsTitle": "ATS Strict", - "atsDesc": "Maximized compatibility for automated screening." - }, - "Testimonials": { - "title": "What Users Say", - "subtitle": "Real feedback — 5 stars across the board", - "review1Author": "Nguyen Thi Tho", - "review1Title": "Professional CV Creation", - "review1Body": "Creating a CV is very easy with a clear and professional template.", - "review2Author": "fector_dev", - "review2Title": "Excellent for Academic Profiles", - "review2Body": "Minimalist interface, content-focused. Exactly the Harvard standard I was looking for. Very satisfied.", - "review3Author": "Phu Parser", - "review3Title": "Write a Professional CV", - "review3Body": "The website is excellent for writing a professional CV — precise and to the point.", - "review4Author": "Kim Huy", - "review4Title": "Minimalist CV Tool", - "review4Body": "Neo-brutalism interface paired with email login makes the whole workflow incredibly focused and seamless.", - "review5Author": "Thu", - "review5Title": "Easy to Use", - "review5Body": "The website is easy to use for creating a CV to apply for jobs." - }, - "AI": { - "label": "NEW // AI WRITING ASSISTANT", - "title": "Write Smarter, Not Harder", - "subtitle": "AI-powered suggestions help you craft compelling resume content — instantly.", - "feature1Title": "Smart Suggestions", - "feature1Desc": "Get context-aware content suggestions tailored to each section of your resume.", - "feature2Title": "Professional Tone", - "feature2Desc": "AI rewrites your bullet points in strong, action-oriented language that recruiters notice.", - "feature3Title": "Instant Generation", - "feature3Desc": "Generate full resume sections from a brief description in seconds.", - "cta": "Try AI Assistant →" - }, - "CTA": { - "title": "Ready to build?", - "subtitle": "Compile your professional history into a pixel-perfect document." - }, - "Sponsors": { - "label": "COMMUNITY // SPONSORS", - "title": "BACKED BY YOU", - "subtitle": "lab68cv.builder is an open-source project. If it has helped you land a job or save time, consider supporting our ongoing development.", - "cta": "Sponsor lab68cv.builder →", - "emptyTitle": "Fuel the Lab", - "emptyDesc": "Be the first to get your logo proudly displayed here as an official supporter of an open-source tool." - }, - "Ecosystem": { - "label": "ECOSYSTEM // GROWTH", - "title": "Launch on Forg", - "subtitle": "Ready to showcase your product? Forg is the minimalist launchpad for hackers. Join the community and get your project noticed.", - "cta": "Upload to Forg →" - } + "Index": { + "label": "lab68cv.builder studio", + "title": "resume.build()", + "description": "Unlock your potential. Build your future. The ultimate Resume Builder engineered for professionals to create clean, structured, and ATS-friendly CVs in seconds.", + "enterLab": "Start Building →", + "tryFree": "Try Free →", + "tryHint": "No sign-up needed", + "viewTemplates": "View Templates", + "Features": { + "title": "What you get", + "pdfTitle": "Instant PDF Export", + "pdfDesc": "Client-side rendering. Zero waiting time. Pixel-perfect PDFs generated directly in your browser.", + "atsTitle": "ATS-Optimized", + "atsDesc": "Clean semantic structures guaranteed to pass automated parsers and Applicant Tracking Systems.", + "templatesTitle": "Premium Layouts", + "templatesDesc": "Four professionally crafted architectural templates tailored for different career stages.", + "freeTitle": "Always Open", + "freeDesc": "No paywalls. No watermarks. Export your professional resume completely free, forever." }, - "Common": { - "login": "LOG IN", - "cvBuilder": "RESUME.BUILD()", - "announcementText": "AI Writing Assistant is now live", - "announcementCta": "Try it free →" + "Import": { + "label": "Profile import", + "title": "Import from Your Profiles", + "subtitle": "Start faster by pulling verified profile data into your CV draft.", + "githubTitle": "GitHub Import", + "githubDesc": "Fetch profile basics, top repositories, and inferred skills from your GitHub account.", + "linkedinTitle": "LinkedIn ZIP Import", + "linkedinDesc": "Upload your LinkedIn data export ZIP and map it into your resume fields instantly.", + "behanceTitle": "Behance Import", + "behanceDesc": "Bring portfolio projects and profile details from Behance to jumpstart your creative CV.", + "cta": "Try Import →" }, - "BuilderImport": { - "previewLabel": "IMPORT_PREVIEW", - "close": "Close", - "personalFields": "PERSONAL_FIELDS", - "sections": "SECTIONS", - "fields": { - "fullName": "Full Name", - "email": "Email", - "location": "Location", - "website": "Website", - "linkedin": "LinkedIn", - "github": "GitHub", - "summary": "Summary" - }, - "noPersonalFields": "No personal fields detected from this profile.", - "skills": "Skills", - "projects": "Projects", - "experience": "Experience", - "certifications": "Certifications", - "skillsFound": "{count} category drafts found", - "projectsFound": "{count} project drafts found", - "experienceFound": "{count} experience entries found", - "certificationsFound": "{count} certification entries found", - "noSkills": "No skills found", - "noProjects": "No projects found", - "noExperience": "No experience found", - "noCertifications": "No certifications found", - "confidenceConfirmed": "CONFIRMED", - "confidenceInferred": "INFERRED", - "mergeMode": "Merge", - "replaceMode": "Replace", - "selectHint": "Select what to merge into your current resume.", - "applySelected": "Apply Selected", - "applying": "Applying...", - "previewImport": "PREVIEW IMPORT", - "importing": "IMPORTING...", - "linkedinPlaceholder": "linkedin.com/in/johndoe or johndoe", - "githubPlaceholder": "github.com/johndoe or johndoe", - "behancePlaceholder": "behance.net/johndoe or johndoe", - "forgPlaceholder": "forg.to/products/your-id, forg.to/@username, @username, or your-id", - "githubHint": "Import preview includes profile fields, top repositories, and inferred skills.", - "behanceHint": "Import preview extracts public profile details and portfolio projects when available.", - "previewReady": "Preview ready from {source} @{username}. Choose fields to merge.", - "mergedSelected": "Merged selected data from {source} @{username}.", - "githubMissingInput": "Add your GitHub username or profile URL first.", - "behanceMissingInput": "Add your Behance URL or handle first.", - "forgMissingInput": "Add your forg.to product ID, profile URL, or @username first.", - "githubFailed": "GitHub import failed.", - "behanceFailed": "Behance import failed.", - "forgFailed": "Forg import failed.", - "forgMultiFound": "Multiple products found for {username}. Pick one and import.", - "forgPickerTitle": "Select Forg Product", - "forgPickerHint": "Found multiple strong matches on this profile. Pick one to import in one click.", - "forgUseSelected": "Use Selected", - "forgDevNotice": "Forg ID now working, if there are any issues please write a ticket to developer below in footer", - "linkedinUrlLabel": "LINKEDIN URL", - "linkedinUrlHint": "Shown in your CV header.", - "linkedinImportLabel": "Import from LinkedIn", - "linkedinZipButton": "Upload LinkedIn Export", - "linkedinZipParsing": "Parsing ZIP...", - "linkedinZipHint": "Free & full data — LinkedIn → Settings → Data Privacy → Get a copy", - "linkedinZipInvalidFile": "Please select a .zip file from your LinkedIn data export.", - "linkedinZipFailed": "Failed to parse LinkedIn ZIP. Make sure it's a valid LinkedIn data export.", - "importSectionTitle": "IMPORT_PROFILE // Optional", - "importSectionHint": "Autofill your profile from a social source. Pick one to get started." + "Achievements": { + "label": "Milestones", + "title": "#1 platform wins", + "unikornRank": "#1 Product of the Day", + "forgRank": "#1 Product of the Week" + }, + "Templates": { + "title": "Available templates", + "executiveTitle": "Executive", + "executiveDesc": "Dense, high-information layout for senior roles.", + "harvardTitle": "Harvard Style", + "harvardDesc": "Traditional, academic, highly respected format.", + "creativeTitle": "Creative", + "creativeDesc": "Bold typography with avatar support for designers.", + "atsTitle": "ATS Strict", + "atsDesc": "Maximized compatibility for automated screening." + }, + "Testimonials": { + "title": "What Users Say", + "subtitle": "Real feedback — 5 stars across the board", + "review1Author": "Nguyen Thi Tho", + "review1Title": "Professional CV Creation", + "review1Body": "Creating a CV is very easy with a clear and professional template.", + "review2Author": "fector_dev", + "review2Title": "Excellent for Academic Profiles", + "review2Body": "Minimalist interface, content-focused. Exactly the Harvard standard I was looking for. Very satisfied.", + "review3Author": "Phu Parser", + "review3Title": "Write a Professional CV", + "review3Body": "The website is excellent for writing a professional CV — precise and to the point.", + "review4Author": "Kim Huy", + "review4Title": "Minimalist CV Tool", + "review4Body": "Neo-brutalism interface paired with email login makes the whole workflow incredibly focused and seamless.", + "review5Author": "Thu", + "review5Title": "Easy to Use", + "review5Body": "The website is easy to use for creating a CV to apply for jobs." + }, + "AI": { + "label": "New: AI writing assistant", + "title": "Write Smarter, Not Harder", + "subtitle": "AI-powered suggestions help you craft compelling resume content — instantly.", + "feature1Title": "Smart Suggestions", + "feature1Desc": "Get context-aware content suggestions tailored to each section of your resume.", + "feature2Title": "Professional Tone", + "feature2Desc": "AI rewrites your bullet points in strong, action-oriented language that recruiters notice.", + "feature3Title": "Instant Generation", + "feature3Desc": "Generate full resume sections from a brief description in seconds.", + "cta": "Try AI Assistant →" + }, + "CTA": { + "title": "Ready to build?", + "subtitle": "Compile your professional history into a pixel-perfect document." + }, + "Sponsors": { + "label": "Community sponsors", + "title": "Backed by you", + "subtitle": "lab68cv.builder is an open-source project. If it has helped you land a job or save time, consider supporting our ongoing development.", + "cta": "Sponsor lab68cv.builder →", + "emptyTitle": "Fuel the Lab", + "emptyDesc": "Be the first to get your logo proudly displayed here as an official supporter of an open-source tool." + }, + "Ecosystem": { + "label": "Ecosystem", + "title": "Launch on Forg", + "subtitle": "Ready to showcase your product? Forg is the minimalist launchpad for hackers. Join the community and get your project noticed.", + "cta": "Upload to Forg →" } -} \ No newline at end of file + }, + "Common": { + "login": "Log in", + "cvBuilder": "resume.build()", + "announcementText": "AI Writing Assistant is now live", + "announcementCta": "Try it free →" + }, + "BuilderImport": { + "previewLabel": "Import preview", + "close": "Close", + "personalFields": "Personal fields", + "sections": "Sections", + "fields": { + "fullName": "Full Name", + "email": "Email", + "location": "Location", + "website": "Website", + "linkedin": "LinkedIn", + "github": "GitHub", + "summary": "Summary" + }, + "noPersonalFields": "No personal fields detected from this profile.", + "skills": "Skills", + "projects": "Projects", + "experience": "Experience", + "certifications": "Certifications", + "skillsFound": "{count} category drafts found", + "projectsFound": "{count} project drafts found", + "experienceFound": "{count} experience entries found", + "certificationsFound": "{count} certification entries found", + "noSkills": "No skills found", + "noProjects": "No projects found", + "noExperience": "No experience found", + "noCertifications": "No certifications found", + "confidenceConfirmed": "Confirmed", + "confidenceInferred": "Inferred", + "mergeMode": "Merge", + "replaceMode": "Replace", + "selectHint": "Select what to merge into your current resume.", + "applySelected": "Apply Selected", + "applying": "Applying...", + "previewImport": "Preview import", + "importing": "Importing...", + "linkedinPlaceholder": "linkedin.com/in/johndoe or johndoe", + "githubPlaceholder": "github.com/johndoe or johndoe", + "behancePlaceholder": "behance.net/johndoe or johndoe", + "forgPlaceholder": "forg.to/products/your-id, forg.to/@username, @username, or your-id", + "githubHint": "Import preview includes profile fields, top repositories, and inferred skills.", + "behanceHint": "Import preview extracts public profile details and portfolio projects when available.", + "previewReady": "Preview ready from {source} @{username}. Choose fields to merge.", + "mergedSelected": "Merged selected data from {source} @{username}.", + "githubMissingInput": "Add your GitHub username or profile URL first.", + "behanceMissingInput": "Add your Behance URL or handle first.", + "forgMissingInput": "Add your forg.to product ID, profile URL, or @username first.", + "githubFailed": "GitHub import failed.", + "behanceFailed": "Behance import failed.", + "forgFailed": "Forg import failed.", + "forgMultiFound": "Multiple products found for {username}. Pick one and import.", + "forgPickerTitle": "Select Forg Product", + "forgPickerHint": "Found multiple strong matches on this profile. Pick one to import in one click.", + "forgUseSelected": "Use Selected", + "forgDevNotice": "Forg ID now working, if there are any issues please write a ticket to developer below in footer", + "linkedinUrlLabel": "LinkedIn URL", + "linkedinUrlHint": "Shown in your CV header.", + "linkedinImportLabel": "Import from LinkedIn", + "linkedinZipButton": "Upload LinkedIn Export", + "linkedinZipParsing": "Parsing ZIP...", + "linkedinZipHint": "Free & full data — LinkedIn → Settings → Data Privacy → Get a copy", + "linkedinZipInvalidFile": "Please select a .zip file from your LinkedIn data export.", + "linkedinZipFailed": "Failed to parse LinkedIn ZIP. Make sure it's a valid LinkedIn data export.", + "importSectionTitle": "Import profile (optional)", + "importSectionHint": "Autofill your profile from a social source. Pick one to get started." + } +} diff --git a/messages/vi.json b/messages/vi.json index 2331451..bf5d8f1 100644 --- a/messages/vi.json +++ b/messages/vi.json @@ -1,157 +1,172 @@ { - "Index": { - "label": "SYS.INIT // LAB68CV.BUILDER STUDIO", - "title": "resume.build()", - "description": "Tinh gọn. Cấu trúc chuẩn xác. Hồ sơ chuyên nghiệp dành riêng cho lập trình viên và nhà thiết kế.", - "enterLab": "Khởi Tạo →", - "tryFree": "Dùng Thử →", - "viewTemplates": "Xem Mẫu CV", - "Features": { - "title": "TÍNH NĂNG", - "pdfTitle": "Xuất PDF Siêu Tốc", - "pdfDesc": "Kết xuất (render) ngay trên trình duyệt. Không cần chờ đợi. Các bản PDF rõ nét, hoàn hảo tới từng pixel.", - "atsTitle": "Tối Ưu ATS", - "atsDesc": "Cấu trúc dữ liệu chuẩn xác vượt qua mọi bộ lọc tự động và Hệ thống Tuyển dụng tự động (ATS).", - "templatesTitle": "Giao Diện Cao Cấp", - "templatesDesc": "Bốn giao diện thiết kế theo chuẩn kiến trúc chuyên nghiệp, phù hợp nhiều giai đoạn sự nghiệp.", - "freeTitle": "Mở Hoàn Toàn", - "freeDesc": "Không thu phí. Không đóng dấu mờ (watermark). Xuất file CV của bạn hoàn toàn miễn phí, mãi mãi." - }, - "Import": { - "label": "NHẬP_HỒ_SƠ // MỚI", - "title": "Nhập Từ Hồ Sơ Của Bạn", - "subtitle": "Bắt đầu nhanh hơn bằng cách lấy dữ liệu hồ sơ đã xác thực vào bản nháp CV.", - "githubTitle": "Nhập GitHub", - "githubDesc": "Lấy thông tin cơ bản, repository nổi bật và kỹ năng suy luận từ tài khoản GitHub của bạn.", - "linkedinTitle": "Nhập ZIP LinkedIn", - "linkedinDesc": "Tải lên file ZIP dữ liệu xuất từ LinkedIn và ánh xạ vào các trường CV ngay lập tức.", - "behanceTitle": "Nhập Behance", - "behanceDesc": "Đưa dự án portfolio và thông tin hồ sơ từ Behance để khởi tạo CV sáng tạo nhanh hơn.", - "cta": "Dùng Nhập Liệu →" - }, - "Achievements": { - "label": "CỘT_MỐC // ĐÃ_XÁC_THỰC", - "title": "THÀNH TÍCH #1", - "unikornRank": "#1 Sản phẩm của ngày", - "forgRank": "#1 Sản phẩm của tuần" - }, - "Templates": { - "title": "CÁC LOẠI GIAO DIỆN", - "executiveTitle": "Executive", - "executiveDesc": "Bố cục dày đặc thông tin, phù hợp cho cấp quản lý.", - "harvardTitle": "Kiểu Harvard", - "harvardDesc": "Định dạng truyền thống, mang tính hàn lâm, uy tín cao.", - "creativeTitle": "Creative (Sáng Tạo)", - "creativeDesc": "Typography phá cách, hỗ trợ ảnh đại diện cho thiết kế.", - "atsTitle": "ATS Chuẩn Xác", - "atsDesc": "Đạt độ tương thích tối đa để hệ thống tuyển dụng đọc." - }, - "Testimonials": { - "title": "Người Dùng Nói Gì", - "subtitle": "Đánh giá thực tế — 5 sao từ mọi người", - "review1Author": "Nguyễn Thị Thơ", - "review1Title": "Tạo CV Chuyên Nghiệp", - "review1Body": "Việc tạo CV rất dễ dàng với template rõ ràng và chuyên nghiệp.", - "review2Author": "fector_dev", - "review2Title": "Xuất Sắc Cho Hồ Sơ Học Thuật", - "review2Body": "Giao diện tối giản, tập trung vào nội dung. Đúng chuẩn mực Harvard mà mình đang tìm kiếm. Rất hài lòng.", - "review3Author": "Phu Parser", - "review3Title": "Viết CV Chuyên Nghiệp", - "review3Body": "Website rất tốt trong việc viết một CV chuyên nghiệp, đúng vào trọng tâm.", - "review4Author": "Kim Huy", - "review4Title": "Công Cụ Tạo CV Tối Giản", - "review4Body": "Giao diện mang phong cách neo-brutalism cùng hệ thống đăng nhập qua email giúp quá trình thao tác vô cùng tập trung và liền mạch.", - "review5Author": "Thư", - "review5Title": "Web dễ dùng", - "review5Body": "Web dễ dùng cho việc tạo cv để xin việc." - }, - "AI": { - "label": "MỚI // TRỢ LÝ VIẾT AI", - "title": "Viết Thông Minh Hơn", - "subtitle": "Gợi ý từ AI giúp bạn tạo nội dung CV chuyên nghiệp — ngay lập tức.", - "feature1Title": "Gợi Ý Thông Minh", - "feature1Desc": "Nhận gợi ý nội dung phù hợp ngữ cảnh cho từng mục trong CV của bạn.", - "feature2Title": "Phong Cách Chuyên Nghiệp", - "feature2Desc": "AI viết lại các gạch đầu dòng của bạn theo ngôn ngữ chủ động, mạnh mẽ mà nhà tuyển dụng yêu thích.", - "feature3Title": "Tạo Nội Dung Tức Thì", - "feature3Desc": "Tạo toàn bộ mục CV từ một mô tả ngắn chỉ trong vài giây.", - "cta": "Dùng Thử AI →" - }, - "CTA": { - "title": "Sẵn sàng khởi tạo?", - "subtitle": "Đóng gói toàn bộ kinh nghiệm của bạn thành một tài liệu hoàn hảo." - } + "Index": { + "label": "lab68cv.builder studio", + "title": "resume.build()", + "description": "Tinh gọn. Cấu trúc chuẩn xác. Hồ sơ chuyên nghiệp dành riêng cho lập trình viên và nhà thiết kế.", + "enterLab": "Khởi Tạo →", + "tryFree": "Dùng Thử →", + "tryHint": "Không cần đăng ký", + "viewTemplates": "Xem Mẫu CV", + "Sponsors": { + "label": "Nhà tài trợ cộng đồng", + "title": "Đồng hành cùng bạn", + "subtitle": "lab68cv.builder là dự án mã nguồn mở. Nếu công cụ này giúp bạn có việc làm hoặc tiết kiệm thời gian, hãy cân nhắc ủng hộ dự án.", + "cta": "Tài trợ lab68cv.builder →", + "emptyTitle": "Tiếp sức cho Lab", + "emptyDesc": "Hãy là người đầu tiên đặt logo tại đây với tư cách nhà tài trợ chính thức của công cụ mã nguồn mở này." }, - "Common": { - "login": "ĐĂNG NHẬP", - "cvBuilder": "RESUME.BUILD()", - "announcementText": "Trợ lý viết AI đã ra mắt", - "announcementCta": "Dùng thử miễn phí →" + "Ecosystem": { + "label": "Hệ sinh thái", + "title": "Ra mắt trên Forg", + "subtitle": "Sẵn sàng giới thiệu sản phẩm của bạn? Forg là bệ phóng tối giản dành cho lập trình viên. Tham gia cộng đồng và để dự án của bạn được chú ý.", + "cta": "Đăng lên Forg →" }, - "BuilderImport": { - "previewLabel": "XEM_TRUOC_NHAP", - "close": "Đóng", - "personalFields": "THONG_TIN_CA_NHAN", - "sections": "CAC_MUC", - "fields": { - "fullName": "Họ và tên", - "email": "Email", - "location": "Địa điểm", - "website": "Website", - "linkedin": "LinkedIn", - "github": "GitHub", - "summary": "Tóm tắt" - }, - "noPersonalFields": "Không tìm thấy thông tin cá nhân từ hồ sơ này.", - "skills": "Kỹ năng", - "projects": "Dự án", - "experience": "Kinh nghiệm", - "certifications": "Chứng chỉ", - "skillsFound": "Tìm thấy {count} nhóm kỹ năng", - "projectsFound": "Tìm thấy {count} dự án", - "experienceFound": "Tìm thấy {count} mục kinh nghiệm", - "certificationsFound": "Tìm thấy {count} mục chứng chỉ", - "noSkills": "Không có kỹ năng", - "noProjects": "Không có dự án", - "noExperience": "Không có kinh nghiệm", - "noCertifications": "Không có chứng chỉ", - "confidenceConfirmed": "XÁC THỰC", - "confidenceInferred": "SUY LUẬN", - "mergeMode": "Gộp", - "replaceMode": "Thay thế", - "selectHint": "Chọn nội dung bạn muốn gộp vào CV hiện tại.", - "applySelected": "Áp dụng mục đã chọn", - "applying": "Đang áp dụng...", - "previewImport": "XEM TRƯỚC NHẬP", - "importing": "ĐANG NHẬP...", - "linkedinPlaceholder": "linkedin.com/in/johndoe hoặc johndoe", - "githubPlaceholder": "github.com/johndoe hoặc johndoe", - "behancePlaceholder": "behance.net/johndoe hoặc johndoe", - "forgPlaceholder": "forg.to/products/your-id, forg.to/@username, @username hoặc your-id", - "githubHint": "Xem trước gồm thông tin hồ sơ, repo nổi bật và kỹ năng suy luận.", - "behanceHint": "Xem trước trích xuất hồ sơ công khai và dự án portfolio khi có thể.", - "previewReady": "Đã sẵn sàng xem trước từ {source} @{username}. Hãy chọn trường để gộp.", - "mergedSelected": "Đã gộp dữ liệu đã chọn từ {source} @{username}.", - "githubMissingInput": "Hãy nhập username hoặc URL GitHub trước.", - "behanceMissingInput": "Hãy nhập URL hoặc handle Behance trước.", - "forgMissingInput": "Hãy nhập ID sản phẩm, URL hồ sơ forg.to hoặc @username trước.", - "githubFailed": "Nhập GitHub thất bại.", - "behanceFailed": "Nhập Behance thất bại.", - "forgFailed": "Nhập Forg thất bại.", - "forgMultiFound": "Tìm thấy nhiều sản phẩm cho {username}. Hãy chọn một sản phẩm để nhập.", - "forgPickerTitle": "Chọn sản phẩm Forg", - "forgPickerHint": "Tìm thấy nhiều lựa chọn phù hợp trên hồ sơ này. Chọn một sản phẩm để nhập nhanh.", - "forgUseSelected": "Dùng mục đã chọn", - "forgDevNotice": "Tính năng nhập bằng ID Forg hiện đã hoạt động, nếu có bất kỳ vấn đề nào vui lòng gửi yêu cầu hỗ trợ cho nhà phát triển ở dưới chân trang", - "linkedinUrlLabel": "LINKEDIN URL", - "linkedinUrlHint": "Hiển thị trong phần tiêu đề CV của bạn.", - "linkedinImportLabel": "Nhập dữ liệu từ LinkedIn", - "linkedinZipButton": "Tải lên file xuất LinkedIn", - "linkedinZipParsing": "Đang đọc ZIP...", - "linkedinZipHint": "Miễn phí & đầy đủ dữ liệu — LinkedIn → Cài đặt → Quyền riêng tư → Lấy bản sao", - "linkedinZipInvalidFile": "Vui lòng chọn file .zip từ dữ liệu xuất của LinkedIn.", - "linkedinZipFailed": "Không thể đọc file ZIP. Hãy chắc chắn đây là file xuất hợp lệ từ LinkedIn.", - "importSectionTitle": "NHẬP_HỒ_SƠ // Tùy chọn", - "importSectionHint": "Tự động điền hồ sơ từ mạng xã hội. Chọn một nguồn để bắt đầu." + "Features": { + "title": "Tính năng", + "pdfTitle": "Xuất PDF Siêu Tốc", + "pdfDesc": "Kết xuất (render) ngay trên trình duyệt. Không cần chờ đợi. Các bản PDF rõ nét, hoàn hảo tới từng pixel.", + "atsTitle": "Tối Ưu ATS", + "atsDesc": "Cấu trúc dữ liệu chuẩn xác vượt qua mọi bộ lọc tự động và Hệ thống Tuyển dụng tự động (ATS).", + "templatesTitle": "Giao Diện Cao Cấp", + "templatesDesc": "Bốn giao diện thiết kế theo chuẩn kiến trúc chuyên nghiệp, phù hợp nhiều giai đoạn sự nghiệp.", + "freeTitle": "Mở Hoàn Toàn", + "freeDesc": "Không thu phí. Không đóng dấu mờ (watermark). Xuất file CV của bạn hoàn toàn miễn phí, mãi mãi." + }, + "Import": { + "label": "Nhập hồ sơ", + "title": "Nhập Từ Hồ Sơ Của Bạn", + "subtitle": "Bắt đầu nhanh hơn bằng cách lấy dữ liệu hồ sơ đã xác thực vào bản nháp CV.", + "githubTitle": "Nhập GitHub", + "githubDesc": "Lấy thông tin cơ bản, repository nổi bật và kỹ năng suy luận từ tài khoản GitHub của bạn.", + "linkedinTitle": "Nhập ZIP LinkedIn", + "linkedinDesc": "Tải lên file ZIP dữ liệu xuất từ LinkedIn và ánh xạ vào các trường CV ngay lập tức.", + "behanceTitle": "Nhập Behance", + "behanceDesc": "Đưa dự án portfolio và thông tin hồ sơ từ Behance để khởi tạo CV sáng tạo nhanh hơn.", + "cta": "Dùng Nhập Liệu →" + }, + "Achievements": { + "label": "Cột mốc", + "title": "Thành tích #1", + "unikornRank": "#1 Sản phẩm của ngày", + "forgRank": "#1 Sản phẩm của tuần" + }, + "Templates": { + "title": "Các loại giao diện", + "executiveTitle": "Executive", + "executiveDesc": "Bố cục dày đặc thông tin, phù hợp cho cấp quản lý.", + "harvardTitle": "Kiểu Harvard", + "harvardDesc": "Định dạng truyền thống, mang tính hàn lâm, uy tín cao.", + "creativeTitle": "Creative (Sáng Tạo)", + "creativeDesc": "Typography phá cách, hỗ trợ ảnh đại diện cho thiết kế.", + "atsTitle": "ATS Chuẩn Xác", + "atsDesc": "Đạt độ tương thích tối đa để hệ thống tuyển dụng đọc." + }, + "Testimonials": { + "title": "Người Dùng Nói Gì", + "subtitle": "Đánh giá thực tế — 5 sao từ mọi người", + "review1Author": "Nguyễn Thị Thơ", + "review1Title": "Tạo CV Chuyên Nghiệp", + "review1Body": "Việc tạo CV rất dễ dàng với template rõ ràng và chuyên nghiệp.", + "review2Author": "fector_dev", + "review2Title": "Xuất Sắc Cho Hồ Sơ Học Thuật", + "review2Body": "Giao diện tối giản, tập trung vào nội dung. Đúng chuẩn mực Harvard mà mình đang tìm kiếm. Rất hài lòng.", + "review3Author": "Phu Parser", + "review3Title": "Viết CV Chuyên Nghiệp", + "review3Body": "Website rất tốt trong việc viết một CV chuyên nghiệp, đúng vào trọng tâm.", + "review4Author": "Kim Huy", + "review4Title": "Công Cụ Tạo CV Tối Giản", + "review4Body": "Giao diện mang phong cách neo-brutalism cùng hệ thống đăng nhập qua email giúp quá trình thao tác vô cùng tập trung và liền mạch.", + "review5Author": "Thư", + "review5Title": "Web dễ dùng", + "review5Body": "Web dễ dùng cho việc tạo cv để xin việc." + }, + "AI": { + "label": "Mới: Trợ lý viết AI", + "title": "Viết Thông Minh Hơn", + "subtitle": "Gợi ý từ AI giúp bạn tạo nội dung CV chuyên nghiệp — ngay lập tức.", + "feature1Title": "Gợi Ý Thông Minh", + "feature1Desc": "Nhận gợi ý nội dung phù hợp ngữ cảnh cho từng mục trong CV của bạn.", + "feature2Title": "Phong Cách Chuyên Nghiệp", + "feature2Desc": "AI viết lại các gạch đầu dòng của bạn theo ngôn ngữ chủ động, mạnh mẽ mà nhà tuyển dụng yêu thích.", + "feature3Title": "Tạo Nội Dung Tức Thì", + "feature3Desc": "Tạo toàn bộ mục CV từ một mô tả ngắn chỉ trong vài giây.", + "cta": "Dùng Thử AI →" + }, + "CTA": { + "title": "Sẵn sàng khởi tạo?", + "subtitle": "Đóng gói toàn bộ kinh nghiệm của bạn thành một tài liệu hoàn hảo." } -} \ No newline at end of file + }, + "Common": { + "login": "Đăng nhập", + "cvBuilder": "resume.build()", + "announcementText": "Trợ lý viết AI đã ra mắt", + "announcementCta": "Dùng thử miễn phí →" + }, + "BuilderImport": { + "previewLabel": "Xem trước khi nhập", + "close": "Đóng", + "personalFields": "Thông tin cá nhân", + "sections": "Các mục", + "fields": { + "fullName": "Họ và tên", + "email": "Email", + "location": "Địa điểm", + "website": "Website", + "linkedin": "LinkedIn", + "github": "GitHub", + "summary": "Tóm tắt" + }, + "noPersonalFields": "Không tìm thấy thông tin cá nhân từ hồ sơ này.", + "skills": "Kỹ năng", + "projects": "Dự án", + "experience": "Kinh nghiệm", + "certifications": "Chứng chỉ", + "skillsFound": "Tìm thấy {count} nhóm kỹ năng", + "projectsFound": "Tìm thấy {count} dự án", + "experienceFound": "Tìm thấy {count} mục kinh nghiệm", + "certificationsFound": "Tìm thấy {count} mục chứng chỉ", + "noSkills": "Không có kỹ năng", + "noProjects": "Không có dự án", + "noExperience": "Không có kinh nghiệm", + "noCertifications": "Không có chứng chỉ", + "confidenceConfirmed": "Xác thực", + "confidenceInferred": "Suy luận", + "mergeMode": "Gộp", + "replaceMode": "Thay thế", + "selectHint": "Chọn nội dung bạn muốn gộp vào CV hiện tại.", + "applySelected": "Áp dụng mục đã chọn", + "applying": "Đang áp dụng...", + "previewImport": "Xem trước nhập", + "importing": "Đang nhập...", + "linkedinPlaceholder": "linkedin.com/in/johndoe hoặc johndoe", + "githubPlaceholder": "github.com/johndoe hoặc johndoe", + "behancePlaceholder": "behance.net/johndoe hoặc johndoe", + "forgPlaceholder": "forg.to/products/your-id, forg.to/@username, @username hoặc your-id", + "githubHint": "Xem trước gồm thông tin hồ sơ, repo nổi bật và kỹ năng suy luận.", + "behanceHint": "Xem trước trích xuất hồ sơ công khai và dự án portfolio khi có thể.", + "previewReady": "Đã sẵn sàng xem trước từ {source} @{username}. Hãy chọn trường để gộp.", + "mergedSelected": "Đã gộp dữ liệu đã chọn từ {source} @{username}.", + "githubMissingInput": "Hãy nhập username hoặc URL GitHub trước.", + "behanceMissingInput": "Hãy nhập URL hoặc handle Behance trước.", + "forgMissingInput": "Hãy nhập ID sản phẩm, URL hồ sơ forg.to hoặc @username trước.", + "githubFailed": "Nhập GitHub thất bại.", + "behanceFailed": "Nhập Behance thất bại.", + "forgFailed": "Nhập Forg thất bại.", + "forgMultiFound": "Tìm thấy nhiều sản phẩm cho {username}. Hãy chọn một sản phẩm để nhập.", + "forgPickerTitle": "Chọn sản phẩm Forg", + "forgPickerHint": "Tìm thấy nhiều lựa chọn phù hợp trên hồ sơ này. Chọn một sản phẩm để nhập nhanh.", + "forgUseSelected": "Dùng mục đã chọn", + "forgDevNotice": "Tính năng nhập bằng ID Forg hiện đã hoạt động, nếu có bất kỳ vấn đề nào vui lòng gửi yêu cầu hỗ trợ cho nhà phát triển ở dưới chân trang", + "linkedinUrlLabel": "LinkedIn URL", + "linkedinUrlHint": "Hiển thị trong phần tiêu đề CV của bạn.", + "linkedinImportLabel": "Nhập dữ liệu từ LinkedIn", + "linkedinZipButton": "Tải lên file xuất LinkedIn", + "linkedinZipParsing": "Đang đọc ZIP...", + "linkedinZipHint": "Miễn phí & đầy đủ dữ liệu — LinkedIn → Cài đặt → Quyền riêng tư → Lấy bản sao", + "linkedinZipInvalidFile": "Vui lòng chọn file .zip từ dữ liệu xuất của LinkedIn.", + "linkedinZipFailed": "Không thể đọc file ZIP. Hãy chắc chắn đây là file xuất hợp lệ từ LinkedIn.", + "importSectionTitle": "Nhập hồ sơ (tùy chọn)", + "importSectionHint": "Tự động điền hồ sơ từ mạng xã hội. Chọn một nguồn để bắt đầu." + } +} diff --git a/next.config.ts b/next.config.ts index aded053..3f935f5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,5 +1,5 @@ import type { NextConfig } from "next"; -import createNextIntlPlugin from 'next-intl/plugin'; +import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin(); @@ -23,16 +23,6 @@ const nextConfig: NextConfig = { }, async headers() { return [ - // Immutable cache for versioned static assets (JS/CSS/fonts) - { - source: "/_next/static/(.*)", - headers: [ - { - key: "Cache-Control", - value: "public, max-age=31536000, immutable", - }, - ], - }, // Long-lived cache for public assets (logo, favicon, etc.) { source: "/design-mode/(.*)", @@ -46,9 +36,7 @@ const nextConfig: NextConfig = { // DNS prefetch for external services { source: "/(.*)", - headers: [ - { key: "X-DNS-Prefetch-Control", value: "on" }, - ], + headers: [{ key: "X-DNS-Prefetch-Control", value: "on" }], }, ]; }, diff --git a/src/app/[locale]/applications/page.tsx b/src/app/[locale]/applications/page.tsx index c2757ff..9507780 100644 --- a/src/app/[locale]/applications/page.tsx +++ b/src/app/[locale]/applications/page.tsx @@ -91,14 +91,12 @@ export default async function ApplicationsPage({
- - APPLICATIONS // TRACKER - + Applications tracker

Job Pipeline

Back to Dashboard @@ -106,7 +104,7 @@ export default async function ApplicationsPage({
-
+

Create Job

{ @@ -154,17 +152,17 @@ export default async function ApplicationsPage({ />
-
+

Create Application

{createError ? ( -

+

{createError}

) : null} @@ -231,7 +229,7 @@ export default async function ApplicationsPage({ > {APPLICATION_STATUSES.map((status) => ( ))} @@ -244,7 +242,7 @@ export default async function ApplicationsPage({ /> @@ -257,7 +255,7 @@ export default async function ApplicationsPage({ {applications.length} records {coverError ? ( -

+

{coverError}

) : null} @@ -291,7 +289,7 @@ export default async function ApplicationsPage({ {APPLICATION_STATUSES.map((status) => ( ))} @@ -316,13 +314,13 @@ export default async function ApplicationsPage({
Clear @@ -364,7 +362,7 @@ export default async function ApplicationsPage({ ) : null}
-

ATS RECOMMENDATIONS

+

ATS recommendations

{(() => { const ats = getAtsRecommendations( item.resumeData, @@ -402,7 +400,7 @@ export default async function ApplicationsPage({
-

COVER LETTER STUDIO

+

Cover letter studio

{ "use server"; @@ -445,7 +443,7 @@ export default async function ApplicationsPage({ @@ -514,14 +512,14 @@ export default async function ApplicationsPage({
@@ -583,20 +581,20 @@ export default async function ApplicationsPage({ > {APPLICATION_STATUSES.map((status) => ( ))} Practice Interview @@ -614,7 +612,7 @@ export default async function ApplicationsPage({ /> diff --git a/src/app/[locale]/builder/[id]/error.tsx b/src/app/[locale]/builder/[id]/error.tsx index 7c83891..6c2afec 100644 --- a/src/app/[locale]/builder/[id]/error.tsx +++ b/src/app/[locale]/builder/[id]/error.tsx @@ -12,12 +12,15 @@ export default function BuilderError({ return (
- ERROR // BUILDER + + Builder error +

Builder Crashed

- {error.message || "An unexpected error occurred in the resume builder."} + {error.message || + "An unexpected error occurred in the resume builder."}

Go Home diff --git a/src/app/[locale]/dashboard/page.tsx b/src/app/[locale]/dashboard/page.tsx index 7dffd6f..737ba6b 100644 --- a/src/app/[locale]/dashboard/page.tsx +++ b/src/app/[locale]/dashboard/page.tsx @@ -34,10 +34,8 @@ export default async function DashboardPage({
- DASHBOARD - - lab68cv.builder - + Dashboard + lab68cv.builder
@@ -47,12 +45,12 @@ export default async function DashboardPage({
Applications
- USER + User

{session.user.email}

@@ -75,10 +73,10 @@ export default async function DashboardPage({ {/* Main content */}
{shouldMigrateGuestDraft ? : null} -
+
- MAJOR_UPDATE // QUICK_ACCESS + Quick access

Jump to the most important tools from the major update. Each card @@ -89,10 +87,10 @@ export default async function DashboardPage({

-

01 // TRACK

-

+

1. Track

+

Applications Tracker

@@ -102,10 +100,10 @@ export default async function DashboardPage({ -

02 // IMPORT

-

+

2. Import

+

Profile Import

@@ -115,10 +113,10 @@ export default async function DashboardPage({ -

03 // RESTORE

-

+

3. Restore

+

Version History

@@ -128,10 +126,10 @@ export default async function DashboardPage({ -

04 // POLISH

-

+

4. Polish

+

AI + Export

@@ -146,7 +144,7 @@ export default async function DashboardPage({

- YOUR_RESUMES // {resumes.length} TOTAL + Your resumes ({resumes.length})

CV Archive @@ -162,14 +160,14 @@ export default async function DashboardPage({ // Empty state
- EMPTY_STATE + Nothing here yet

No Resumes Yet

- Create your first resume to get started. Choose from our brutalist - templates and build something raw. + Create your first resume to get started. Pick a template and start + writing.

diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 2d948ac..3708b28 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,9 +1,9 @@ import type { Metadata } from "next"; -import { IBM_Plex_Sans } from "next/font/google"; +import { IBM_Plex_Sans, Shantell_Sans } from "next/font/google"; import "../globals.css"; -import { NextIntlClientProvider } from 'next-intl'; -import { getMessages } from 'next-intl/server'; -import { Analytics } from '@vercel/analytics/next'; +import { NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; +import { Analytics } from "@vercel/analytics/next"; const ibmPlexSans = IBM_Plex_Sans({ variable: "--font-ibm-plex-sans", @@ -12,7 +12,15 @@ const ibmPlexSans = IBM_Plex_Sans({ weight: ["100", "200", "300", "400", "500", "600", "700"], }); -const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://lab68cvbuilder.vercel.app/'; +// Hand-drawn look with Vietnamese letters, which Kalam and Caveat do not have +const shantellSans = Shantell_Sans({ + variable: "--font-shantell-sans", + subsets: ["latin", "vietnamese"], + display: "swap", +}); + +const siteUrl = + process.env.NEXT_PUBLIC_SITE_URL ?? "https://lab68cvbuilder.vercel.app/"; export const metadata: Metadata = { metadataBase: new URL(siteUrl), @@ -21,10 +29,15 @@ export const metadata: Metadata = { template: "%s | lab68cv.builder", }, description: - "Raw. Structured. ATS-optimized brutalist resumes engineered for developers and designers. Export pixel-perfect PDFs for free.", + "Friendly, ATS-optimized resumes made for developers and designers. Export pixel-perfect PDFs for free.", keywords: [ - "CV builder", "resume builder", "ATS resume", "free resume", - "PDF resume", "developer resume", "brutalist design", "lab68cv.builder", + "CV builder", + "resume builder", + "ATS resume", + "free resume", + "PDF resume", + "developer resume", + "lab68cv.builder", ], authors: [{ name: "lab68cv.builder" }], creator: "lab68cv.builder", @@ -33,7 +46,7 @@ export const metadata: Metadata = { siteName: "lab68cv.builder", title: "lab68cv.builder — Build Your Resume", description: - "Raw. Structured. ATS-optimized brutalist resumes engineered for developers and designers.", + "Friendly, ATS-optimized resumes made for developers and designers.", locale: "en_US", alternateLocale: ["vi_VN"], url: siteUrl, @@ -42,7 +55,7 @@ export const metadata: Metadata = { card: "summary_large_image", title: "lab68cv.builder — Build Your Resume", description: - "Raw. Structured. ATS-optimized brutalist resumes engineered for developers and designers.", + "Friendly, ATS-optimized resumes made for developers and designers.", creator: "@lab68dev", }, verification: { @@ -77,7 +90,7 @@ export const metadata: Metadata = { export default async function RootLayout({ children, - params + params, }: Readonly<{ children: React.ReactNode; params: Promise<{ locale: string }>; @@ -86,10 +99,11 @@ export default async function RootLayout({ const messages = await getMessages(); return ( - - + + {children} diff --git a/src/app/[locale]/login/page.tsx b/src/app/[locale]/login/page.tsx index 724acf3..91b82ed 100644 --- a/src/app/[locale]/login/page.tsx +++ b/src/app/[locale]/login/page.tsx @@ -4,11 +4,36 @@ import { useState, Suspense } from "react"; import { signIn } from "next-auth/react"; import { useSearchParams } from "next/navigation"; import { Link } from "@/i18n/routing"; +import { DoodleArrow, ScribbleUnderline } from "@/components/doodles"; import { GUEST_MIGRATION_PARAM, GUEST_STORAGE_KEY } from "@/lib/constants"; import { getSafeCallbackUrl } from "@/lib/url-helpers"; const SIGN_IN_ERROR = "Google sign-in did not work. Please try again."; +// Google's branding rules require these exact shapes and colors. +function GoogleLogo({ className }: { className?: string }) { + return ( + + ); +} + function LoginForm() { const [isLoading, setIsLoading] = useState(false); const searchParams = useSearchParams(); @@ -43,107 +68,77 @@ function LoginForm() { return (
- {/* ═══════════════════════════════════════════════════════ */} - {/* LEFT PANEL — BRUTALIST TYPOGRAPHY */} - {/* ═══════════════════════════════════════════════════════ */} -
- {/* Grid overlay */} -
-
-
- -
- {/* Technical label */} - - AUTH.MODULE // V5.0.0 - -
+ {/* Left page of the open notebook */} +
+ lab68cv.builder -
- {/* Hero heading */} -

- ENTER -
- THE +
+

+ Enter
- LAB + the lab

+ - {/* Divider */} -
- - {/* Description */} -

- Sign in with your Google account. -
- No passwords. No friction. Pure access. -

+
+

+ Sign in with your Google account. No passwords to remember. +

+
-
- - LAB68DEV STUDIO // {new Date().getFullYear()} - -
+ + Lab68dev Studio, {new Date().getFullYear()} + + +
- {/* ═══════════════════════════════════════════════════════ */} - {/* RIGHT PANEL — GOOGLE SIGN-IN */} - {/* ═══════════════════════════════════════════════════════ */} -
+ {/* Right page: sign-in */} +
{/* Mobile header */} -
-

- ENTER -
- THE LAB +
+

+ Enter the lab

-
+
- {/* Form header */} -
- - STEP_01 // AUTHENTICATION - -

Welcome

-

+

+

Welcome

+

Log in or create an account with Google. If you already have an account, use the same email address to keep your resumes.

+ + {/* Error message */} + {error && ( +
+

{error}

+
+ )} + +
- {/* Error message */} - {error && ( -
- ERROR -

{error}

-
- )} - - - -
+
- ← BACK_TO_HOME + Back to home
diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 76cd627..6149e9c 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,12 +1,13 @@ import { Link } from "@/i18n/routing"; +import { DoodleArrow, ScribbleUnderline } from "@/components/doodles"; import Image from "next/image"; import { Header } from "@/components/layout/header"; import { Footer } from "@/components/layout/footer"; import { useTranslations } from "next-intl"; -// Sponsor data. Leave array empty [] to show the fallback "Be the first" UI, +// Sponsor data. Leave array empty [] to show the fallback "Be the first" UI, // or fill with { name, url, logoUrl } to populate the grid. -const SPONSORS: Array<{name: string, url: string, logoUrl: string}> = []; +const SPONSORS: Array<{ name: string; url: string; logoUrl: string }> = []; // Cache this fully-static page for 24 hours (ISR) export const revalidate = 86400; @@ -18,44 +19,47 @@ export default function HomePage() { <>
- {/* Grid overlay effect - global */} -
-
-
- {/* HERO SECTION */}
{t("label")} -

+

{t("title")}

-
-

+ +

{t.rich("description", { - b: (chunks) => {chunks}, + b: (chunks) => ( + + {chunks} + + ), })}

{t("enterLab")} {t("tryFree")} {t("viewTemplates")}
+
+ +

{t("tryHint")}

+
@@ -63,10 +67,19 @@ export default function HomePage() {
- Featured In + + Featured In +
@@ -100,27 +178,27 @@ export default function HomePage() {
- {t("Achievements.label")} -

+ + {t("Achievements.label")} + +

{t("Achievements.title")}

-

- UNIKORN +

+ Unikorn

-

+

{t("Achievements.unikornRank")}

-

- FORG -

-

+

Forg

+

{t("Achievements.forgRank")}

@@ -131,26 +209,42 @@ export default function HomePage() {
-

+

{t("Features.title")}

-

01. {t("Features.pdfTitle")}

-

{t("Features.pdfDesc")}

+

+ 01. {t("Features.pdfTitle")} +

+

+ {t("Features.pdfDesc")} +

-

02. {t("Features.atsTitle")}

-

{t("Features.atsDesc")}

+

+ 02. {t("Features.atsTitle")} +

+

+ {t("Features.atsDesc")} +

-

03. {t("Features.templatesTitle")}

-

{t("Features.templatesDesc")}

+

+ 03. {t("Features.templatesTitle")} +

+

+ {t("Features.templatesDesc")} +

-

04. {t("Features.freeTitle")}

-

{t("Features.freeDesc")}

+

+ 04. {t("Features.freeTitle")} +

+

+ {t("Features.freeDesc")} +

@@ -161,8 +255,10 @@ export default function HomePage() {
- {t("Import.label")} -

+ + {t("Import.label")} + +

{t("Import.title")}

@@ -173,36 +269,54 @@ export default function HomePage() {

-
01 // GITHUB
-

{t("Import.githubTitle")}

-

{t("Import.githubDesc")}

+
+ 1. GitHub +
+

+ {t("Import.githubTitle")} +

+

+ {t("Import.githubDesc")} +

{t("Import.cta")}
-
02 // LINKEDIN
-

{t("Import.linkedinTitle")}

-

{t("Import.linkedinDesc")}

+
+ 2. LinkedIn +
+

+ {t("Import.linkedinTitle")} +

+

+ {t("Import.linkedinDesc")} +

{t("Import.cta")}
-
03 // BEHANCE
-

{t("Import.behanceTitle")}

-

{t("Import.behanceDesc")}

+
+ 3. Behance +
+

+ {t("Import.behanceTitle")} +

+

+ {t("Import.behanceDesc")} +

{t("Import.cta")} @@ -217,8 +331,10 @@ export default function HomePage() { {/* Left label panel */}
- {t("AI.label")} -

+ + {t("AI.label")} + +

{t("AI.title")}

@@ -227,7 +343,7 @@ export default function HomePage() {

{t("AI.cta")} @@ -236,39 +352,90 @@ export default function HomePage() {
- + + +
-

01. {t("AI.feature1Title")}

-

{t("AI.feature1Desc")}

+

+ 01. {t("AI.feature1Title")} +

+

+ {t("AI.feature1Desc")} +

- + + +
-

02. {t("AI.feature2Title")}

-

{t("AI.feature2Desc")}

+

+ 02. {t("AI.feature2Title")} +

+

+ {t("AI.feature2Desc")} +

- + + +
-

03. {t("AI.feature3Title")}

-

{t("AI.feature3Desc")}

+

+ 03. {t("AI.feature3Title")} +

+

+ {t("AI.feature3Desc")} +

{/* TEMPLATES SHOWCASE */} -
+
-

+

{t("Templates.title")}

{/* Executive */}
-
+
{/* Wireframe representation */}
@@ -284,13 +451,17 @@ export default function HomePage() {
-

{t("Templates.executiveTitle")}

-

{t("Templates.executiveDesc")}

+

+ {t("Templates.executiveTitle")} +

+

+ {t("Templates.executiveDesc")} +

{/* Harvard */}
-
+
{/* Wireframe representation */}
@@ -300,8 +471,12 @@ export default function HomePage() {
-

{t("Templates.harvardTitle")}

-

{t("Templates.harvardDesc")}

+

+ {t("Templates.harvardTitle")} +

+

+ {t("Templates.harvardDesc")} +

{/* Creative */} @@ -319,13 +494,17 @@ export default function HomePage() {
-

{t("Templates.creativeTitle")}

-

{t("Templates.creativeDesc")}

+

+ {t("Templates.creativeTitle")} +

+

+ {t("Templates.creativeDesc")} +

{/* ATS */}
-
+
{/* Wireframe representation */}
@@ -337,15 +516,19 @@ export default function HomePage() {
-

{t("Templates.atsTitle")}

-

{t("Templates.atsDesc")}

+

+ {t("Templates.atsTitle")} +

+

+ {t("Templates.atsDesc")} +

{t("enterLab")} @@ -359,8 +542,10 @@ export default function HomePage() { {/* Left label panel */}
- {t("Sponsors.label")} -

+ + {t("Sponsors.label")} + +

{t("Sponsors.title")}

@@ -371,12 +556,12 @@ export default function HomePage() { href="https://github.com/sponsors/lab68dev" target="_blank" rel="noopener noreferrer" - className="self-start border border-white px-8 py-3 text-xs font-bold uppercase tracking-widest hover:bg-white hover:text-black transition-colors duration-150 flex items-center gap-2" + className="self-start border border-white px-8 py-3 text-xs font-bold hover:bg-white hover:text-black transition-colors duration-150 flex items-center gap-2" > {t("Sponsors.cta")}

- + {/* Right Sponsors Grid */}
{SPONSORS.length > 0 ? ( @@ -388,19 +573,19 @@ export default function HomePage() { target="_blank" rel="noopener noreferrer" className={`border-black flex items-center justify-center p-8 hover:bg-white transition-colors duration-200 ${ - index % 3 !== 2 ? 'border-r' : '' // No right border on last item of row + index % 3 !== 2 ? "border-r" : "" // No right border on last item of row } ${ - index < SPONSORS.length - 3 ? 'border-b' : '' // No bottom border on last row + index < SPONSORS.length - 3 ? "border-b" : "" // No bottom border on last row }`} title={`Sponsored by ${sponsor.name}`} > {/* Using unoptimized Image or img tag depending on implementation, fallback to alt text if logo fails to load */} {/* eslint-disable-next-line @next/next/no-img-element */} - {sponsor.name} ))} @@ -408,11 +593,23 @@ export default function HomePage() { ) : (
- - + +
-

{t("Sponsors.emptyTitle")}

+

+ {t("Sponsors.emptyTitle")} +

{t("Sponsors.emptyDesc")}

@@ -426,8 +623,10 @@ export default function HomePage() {
- {t("Ecosystem.label")} -

+ + {t("Ecosystem.label")} + +

{t("Ecosystem.title")}

@@ -437,7 +636,7 @@ export default function HomePage() { href="https://forg.to/ref/duongphudong" target="_blank" rel="noopener noreferrer" - className="inline-block border border-white bg-white text-black px-10 py-4 text-sm font-bold uppercase tracking-widest hover:bg-transparent hover:text-white transition-all duration-150" + className="inline-block border border-white bg-white text-black px-10 py-4 text-sm font-bold hover:bg-transparent hover:text-white transition-all duration-150" > {t("Ecosystem.cta")} @@ -445,13 +644,17 @@ export default function HomePage() {

- - - + + +
-

TARGET: FORG.TO

-

Ready to launch your project?

+

+ Target: forg.to +

+

+ Ready to launch your project? +

@@ -462,74 +665,138 @@ export default function HomePage() {
-

{t("Testimonials.title")}

-

{t("Testimonials.subtitle")}

+

+ {t("Testimonials.title")} +

+

+ {t("Testimonials.subtitle")} +

{/* Testimonial 1 */}
{[...Array(5)].map((_, i) => ( - + + + ))}
-

{t("Testimonials.review1Title")}

-

{t("Testimonials.review1Body")}

+

+ {t("Testimonials.review1Title")} +

+

+ {t("Testimonials.review1Body")} +

-

{t("Testimonials.review1Author")}

+

+ {t("Testimonials.review1Author")} +

{/* Testimonial 2 */}
{[...Array(5)].map((_, i) => ( - + + + ))}
-

{t("Testimonials.review2Title")}

-

{t("Testimonials.review2Body")}

+

+ {t("Testimonials.review2Title")} +

+

+ {t("Testimonials.review2Body")} +

-

{t("Testimonials.review2Author")}

+

+ {t("Testimonials.review2Author")} +

{/* Testimonial 3 */}
{[...Array(5)].map((_, i) => ( - + + + ))}
-

{t("Testimonials.review3Title")}

-

{t("Testimonials.review3Body")}

+

+ {t("Testimonials.review3Title")} +

+

+ {t("Testimonials.review3Body")} +

-

{t("Testimonials.review3Author")}

+

+ {t("Testimonials.review3Author")} +

{/* Testimonial 4 */}
{[...Array(5)].map((_, i) => ( - + + + ))}
-

{t("Testimonials.review4Title")}

-

{t("Testimonials.review4Body")}

+

+ {t("Testimonials.review4Title")} +

+

+ {t("Testimonials.review4Body")} +

-

{t("Testimonials.review4Author")}

+

+ {t("Testimonials.review4Author")} +

{/* Testimonial 5 */}
{[...Array(5)].map((_, i) => ( - + + + ))}
-

{t("Testimonials.review5Title")}

-

{t("Testimonials.review5Body")}

+

+ {t("Testimonials.review5Title")} +

+

+ {t("Testimonials.review5Body")} +

-

{t("Testimonials.review5Author")}

+

+ {t("Testimonials.review5Author")} +

@@ -541,12 +808,12 @@ export default function HomePage() {

{t("CTA.title")}

-

+

{t("CTA.subtitle")}

{t("enterLab")} diff --git a/src/app/[locale]/r/[id]/page.tsx b/src/app/[locale]/r/[id]/page.tsx index ef0021e..829daaa 100644 --- a/src/app/[locale]/r/[id]/page.tsx +++ b/src/app/[locale]/r/[id]/page.tsx @@ -42,7 +42,7 @@ export default async function PublicResumePage({ HarvardTemplate; return ( -
+
diff --git a/src/app/globals.css b/src/app/globals.css index 07ec9bd..47d1e95 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,8 +1,9 @@ @import "tailwindcss"; -@theme inline { - --color-black: #000000; - --color-white: #ffffff; +/* static: .cv-sheet overrides these, so they must exist as runtime variables */ +@theme static { + --color-black: #2d2d2d; /* ink */ + --color-white: #fdfbf7; /* paper */ --color-gray-100: #f5f5f5; --color-gray-200: #e5e5e5; --color-gray-300: #d4d4d4; @@ -12,270 +13,327 @@ --color-gray-700: #404040; --color-gray-800: #262626; --color-gray-900: #171717; + --color-pen: #2d5da1; + --color-note: #fff9c4; + --color-marker: #ff4d4d; /* marks only: about 3:1 on paper, too low for text */ + --color-notebook-line: #dfe6ee; --font-sans: var(--font-ibm-plex-sans); --font-mono: var(--font-ibm-plex-sans); -} - -/* ===== UTILITY CLASSES ===== */ - -.grid-overlay { - background-image: - linear-gradient(#000 1px, transparent 1px), - linear-gradient(90deg, #000 1px, transparent 1px); - background-size: 40px 40px; -} - -/* ===== LAB68DEV BRUTALIST RESET ===== */ - -* { - border-radius: 0 !important; - -webkit-tap-highlight-color: transparent; -} - -html { - background-color: #ffffff; - color: #000000; - scroll-behavior: smooth; - -webkit-text-size-adjust: 100%; - text-size-adjust: 100%; - font-feature-settings: - "kern" 1, - "liga" 1; - text-rendering: optimizeLegibility; -} - -body { - font-family: var(--font-sans), system-ui, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: #ffffff; - color: #000000; -} - -/* Brutalist selection */ -::selection { - background-color: #000000; - color: #ffffff; -} - -/* Brutalist focus states */ -*:focus-visible { - outline: 2px solid #000000; - outline-offset: 2px; -} - -/* Brutalist scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: #ffffff; - border-left: 1px solid #000000; -} - -::-webkit-scrollbar-thumb { - background: #000000; -} - -/* Brutalist input reset */ -input, -textarea, -select { - border: 1px solid #000000; - background: transparent; - color: #000000; - font-family: var(--font-sans), system-ui, sans-serif; - padding: 0.75rem 1rem; - transition: all 0.15s ease; -} - -input:focus, -textarea:focus, -select:focus { - background-color: #ffffff; - color: #000000; - border-color: #000000; - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); - outline: none; -} - -input::placeholder, -textarea::placeholder { - color: #737373; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 0.05em; -} - -/* Keep selection clearly visible inside form fields, especially on black focus state */ -input[type="text"]::selection, -input[type="email"]::selection, -input[type="url"]::selection, -input[type="tel"]::selection, -input[type="search"]::selection, -input[type="password"]::selection, -textarea::selection { - background-color: #facc15 !important; - color: #000000 !important; -} - -input[type="text"]::-moz-selection, -input[type="email"]::-moz-selection, -input[type="url"]::-moz-selection, -input[type="tel"]::-moz-selection, -input[type="search"]::-moz-selection, -input[type="password"]::-moz-selection, -textarea::-moz-selection { - background-color: #facc15 !important; - color: #000000 !important; -} - -input:focus::placeholder, -textarea:focus::placeholder { - color: #a3a3a3; -} - -input[type="text"]:focus::selection, -input[type="email"]:focus::selection, -input[type="url"]:focus::selection, -input[type="tel"]:focus::selection, -input[type="search"]:focus::selection, -input[type="password"]:focus::selection, -textarea:focus::selection { - background-color: #22d3ee !important; - color: #000000 !important; -} - -input[type="text"]:focus::-moz-selection, -input[type="email"]:focus::-moz-selection, -input[type="url"]:focus::-moz-selection, -input[type="tel"]:focus::-moz-selection, -input[type="search"]:focus::-moz-selection, -input[type="password"]:focus::-moz-selection, -textarea:focus::-moz-selection { - background-color: #22d3ee !important; - color: #000000 !important; -} - -/* Keep date/month picker icon visible on macOS WebKit browsers */ -input[type="date"], -input[type="month"] { - color-scheme: light; - padding-right: 2rem; -} - -input[type="date"]::-webkit-calendar-picker-indicator, -input[type="month"]::-webkit-calendar-picker-indicator { - opacity: 1; - cursor: pointer; -} - -input[type="date"]:focus::-webkit-calendar-picker-indicator, -input[type="month"]:focus::-webkit-calendar-picker-indicator { - filter: invert(1); -} - -/* macOS/Safari: keep select controls visually consistent with custom theme */ -select { - -webkit-appearance: none; - appearance: none; - background-image: - linear-gradient(45deg, transparent 50%, #000000 50%), - linear-gradient(135deg, #000000 50%, transparent 50%); - background-position: - calc(100% - 16px) calc(50% - 2px), - calc(100% - 10px) calc(50% - 2px); - background-size: - 6px 6px, - 6px 6px; - background-repeat: no-repeat; - padding-right: 2.25rem; -} - -select:focus { - background-image: - linear-gradient(45deg, transparent 50%, #ffffff 50%), - linear-gradient(135deg, #ffffff 50%, transparent 50%); -} - -/* macOS/Safari: custom checkbox styling (Safari ignores border-radius without appearance reset) */ -input[type="checkbox"] { - -webkit-appearance: none; - appearance: none; - width: 1rem; - height: 1rem; - border: 1px solid #000000; - background: transparent; - cursor: pointer; - position: relative; - flex-shrink: 0; - padding: 0; - vertical-align: middle; -} - -input[type="checkbox"]:checked { - background-color: #000000; -} - -input[type="checkbox"]:checked::after { - content: ""; - position: absolute; - left: 3px; - top: 0px; - width: 5px; - height: 9px; - border: 2px solid #ffffff; - border-top: none; - border-left: none; - transform: rotate(45deg); -} - -input[type="checkbox"]:focus-visible { - outline: 2px solid #000000; - outline-offset: 2px; -} - -/* scrollbar-none utility — hides scrollbars while preserving scroll on macOS overlay-scrollbar mode */ -.scrollbar-none { - scrollbar-width: none; -} -.scrollbar-none::-webkit-scrollbar { - display: none; -} - -/* Brutalist button base */ -button { - cursor: pointer; - border: 1px solid #000000; - background: transparent; - color: #000000; - font-family: var(--font-sans), system-ui, sans-serif; - text-transform: uppercase; - letter-spacing: 0.05em; - font-size: 0.75rem; - font-weight: 700; - padding: 0.75rem 1.5rem; - transition: all 0.15s ease; -} - -button:hover { - background-color: #000000; - color: #ffffff; -} - -/* Monospace label utility */ -.label-mono { - font-family: var(--font-mono), monospace; - font-size: 0.625rem; - text-transform: uppercase; - letter-spacing: 0.1em; - color: #737373; -} - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + --font-hand: var(--font-shantell-sans); + --radius-wobble: 255px 15px 225px 15px / 15px 225px 15px 255px; +} + +/* ===== DOODLE BASE ===== */ + +@layer base { + * { + -webkit-tap-highlight-color: transparent; + } + + html { + background-color: var(--color-white); + color: var(--color-black); + scroll-behavior: smooth; + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + font-feature-settings: + "kern" 1, + "liga" 1; + text-rendering: optimizeLegibility; + } + + body { + font-family: var(--font-sans), system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: var(--color-white); + color: var(--color-black); + } + + h1, + h2, + h3, + h4, + label, + button { + font-family: var(--font-hand), system-ui, sans-serif; + } + + ::selection { + background-color: var(--color-note); + color: var(--color-black); + } + + *:focus-visible { + outline: 2px dashed var(--color-pen); + outline-offset: 3px; + } + + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + ::-webkit-scrollbar-track { + background: var(--color-white); + } + + ::-webkit-scrollbar-thumb { + background: var(--color-black); + border: 2px solid var(--color-white); + border-radius: 6px; + } + + input, + textarea, + select { + border: 2px solid var(--color-black); + border-radius: var(--radius-wobble); + background: transparent; + color: var(--color-black); + font-family: var(--font-sans), system-ui, sans-serif; + padding: 0.75rem 1rem; + transition: + border-color 0.15s ease, + box-shadow 0.15s ease; + } + + input:focus, + textarea:focus, + select:focus { + border-color: var(--color-pen); + box-shadow: 3px 3px 0 var(--color-pen); + outline: none; + } + + input::placeholder, + textarea::placeholder { + color: var(--color-gray-500); + font-size: 0.875rem; + } + + /* Keep date/month picker icon visible on macOS WebKit browsers */ + input[type="date"], + input[type="month"] { + color-scheme: light; + padding-right: 2rem; + } + + input[type="date"]::-webkit-calendar-picker-indicator, + input[type="month"]::-webkit-calendar-picker-indicator { + opacity: 1; + cursor: pointer; + } + + /* macOS/Safari: keep select controls visually consistent with custom theme */ + select { + -webkit-appearance: none; + appearance: none; + background-image: + linear-gradient(45deg, transparent 50%, var(--color-black) 50%), + linear-gradient(135deg, var(--color-black) 50%, transparent 50%); + background-position: + calc(100% - 16px) calc(50% - 2px), + calc(100% - 10px) calc(50% - 2px); + background-size: + 6px 6px, + 6px 6px; + background-repeat: no-repeat; + padding-right: 2.25rem; + } + + /* macOS/Safari: custom checkbox styling (Safari ignores border-radius without appearance reset) */ + input[type="checkbox"] { + -webkit-appearance: none; + appearance: none; + width: 1.125rem; + height: 1.125rem; + background: transparent; + cursor: pointer; + position: relative; + flex-shrink: 0; + padding: 0; + vertical-align: middle; + } + + input[type="checkbox"]:checked { + background-color: var(--color-note); + } + + input[type="checkbox"]:checked::after { + content: ""; + position: absolute; + left: 4px; + top: 0; + width: 6px; + height: 11px; + border: 2px solid var(--color-black); + border-top: none; + border-left: none; + transform: rotate(40deg); + } + + button { + cursor: pointer; + border: 2px solid var(--color-black); + border-radius: var(--radius-wobble); + background: transparent; + color: var(--color-black); + font-size: 0.875rem; + font-weight: 600; + padding: 0.625rem 1.25rem; + transition: + background-color 0.15s ease, + color 0.15s ease, + transform 0.1s ease, + box-shadow 0.1s ease; + } + + button:hover { + background-color: var(--color-note); + } + + .border, + .border-2 { + border-radius: var(--radius-wobble); + } + + /* + * CVs stay plain: recruiters and ATS software read them, and the preview must + * match the PDF. This restores the ink and paper colors to pure black and white. + */ + .cv-sheet { + --color-black: #000000; + --color-white: #ffffff; + font-family: Arial, Helvetica, sans-serif; + } + + /* important: Tailwind's rounded-* utilities sit in a later layer */ + .cv-sheet * { + border-radius: 0 !important; + } + + .cv-sheet h1, + .cv-sheet h2, + .cv-sheet h3, + .cv-sheet h4 { + font-family: inherit; + } + + @media print { + html, + body { + background: #ffffff; + } + } + + @media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + transition: none !important; + animation: none !important; + } + } +} + +/* ===== DOODLE COMPONENTS ===== */ + +@layer components { + .label-mono { + font-family: var(--font-hand), system-ui, sans-serif; + font-size: 0.8125rem; + color: var(--color-gray-600); + } + + .sketch-card { + background-color: var(--color-white); + border: 2px solid var(--color-black); + border-radius: var(--radius-wobble); + box-shadow: 4px 4px 0 var(--color-black); + } + + /* Buttons press down over their shadow, like a sticker pushed onto paper */ + .sketch-btn, + :is(a, button).border-black:not(.cv-sheet *) { + box-shadow: 2px 2px 0 var(--color-black); + transition: + background-color 0.15s ease, + color 0.15s ease, + transform 0.1s ease, + box-shadow 0.1s ease; + } + + .sketch-btn:hover:not(:disabled), + :is(a, button).border-black:not(.cv-sheet *):hover:not(:disabled) { + transform: translate(1px, 1px); + box-shadow: 1px 1px 0 var(--color-black); + } + + .sketch-btn:active:not(:disabled), + :is(a, button).border-black:not(.cv-sheet *):active:not(:disabled) { + transform: translate(2px, 2px); + box-shadow: none; + } + + .sketch-note { + position: relative; + background-color: var(--color-note); + box-shadow: 2px 3px 0 rgb(45 45 45 / 0.15); + transform: rotate(-1.5deg); + } + + /* Strip of tape holding the note to the page */ + .sketch-note::before { + content: ""; + position: absolute; + top: -0.625rem; + left: 50%; + width: 5rem; + height: 1.25rem; + background-color: rgb(45 93 161 / 0.2); + transform: translateX(-50%) rotate(2deg); + } + + .notebook-paper { + background-color: var(--color-white); + background-image: + linear-gradient( + 90deg, + transparent 3rem, + rgb(255 77 77 / 0.35) 3rem, + rgb(255 77 77 / 0.35) calc(3rem + 1px), + transparent calc(3rem + 1px) + ), + repeating-linear-gradient( + transparent, + transparent 1.9375rem, + var(--color-notebook-line) 1.9375rem, + var(--color-notebook-line) 2rem + ); + } + + /* scrollbar-none hides scrollbars while preserving scroll on macOS overlay-scrollbar mode */ + .scrollbar-none { + scrollbar-width: none; + } + + .scrollbar-none::-webkit-scrollbar { + display: none; + } +} + +/* Later in the utilities layer than Tailwind's own .border, so the stroke wins */ +@layer utilities { + .border { + border-width: 2px; + } + + .cv-sheet .border { + border-width: 1px; + } } diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx index 4811fee..8a6e312 100644 --- a/src/components/LanguageSwitcher.tsx +++ b/src/components/LanguageSwitcher.tsx @@ -4,25 +4,25 @@ import { useRouter, usePathname } from "@/i18n/routing"; import { useLocale } from "next-intl"; export function LanguageSwitcher() { - const locale = useLocale(); - const router = useRouter(); - const pathname = usePathname(); + const locale = useLocale(); + const router = useRouter(); + const pathname = usePathname(); - function onSelectChange(e: React.ChangeEvent) { - const nextLocale = e.target.value as "en" | "vi"; - router.replace(pathname, { locale: nextLocale }); - } + function onSelectChange(e: React.ChangeEvent) { + const nextLocale = e.target.value as "en" | "vi"; + router.replace(pathname, { locale: nextLocale }); + } - return ( - - ); + return ( + + ); } diff --git a/src/components/applications/mock-interview-client.tsx b/src/components/applications/mock-interview-client.tsx index e4dfb1f..26c121d 100644 --- a/src/components/applications/mock-interview-client.tsx +++ b/src/components/applications/mock-interview-client.tsx @@ -210,7 +210,7 @@ export default function MockInterviewClient({

{currentA && (
-

+

Your Answer

@@ -242,7 +242,7 @@ export default function MockInterviewClient({ {!currentA && !isCompleted && (

-

+

Your Answer

diff --git a/src/components/builder/ai-suggestions.tsx b/src/components/builder/ai-suggestions.tsx index 2d49dde..9b51895 100644 --- a/src/components/builder/ai-suggestions.tsx +++ b/src/components/builder/ai-suggestions.tsx @@ -17,100 +17,208 @@ function analyzeResume(data: ResumeData): Suggestion[] { // Personal info if (!p.fullName.trim()) { - suggestions.push({ section: "Personal", severity: "error", message: "Add your full name — it's the first thing recruiters see." }); + suggestions.push({ + section: "Personal", + severity: "error", + message: "Add your full name — it's the first thing recruiters see.", + }); } if (!p.email.trim()) { - suggestions.push({ section: "Personal", severity: "error", message: "Add an email address so employers can contact you." }); + suggestions.push({ + section: "Personal", + severity: "error", + message: "Add an email address so employers can contact you.", + }); } if (!(p.phone ?? "").trim()) { - suggestions.push({ section: "Personal", severity: "warning", message: "Consider adding a phone number for quicker recruiter contact." }); + suggestions.push({ + section: "Personal", + severity: "warning", + message: "Consider adding a phone number for quicker recruiter contact.", + }); } if (!(p.location ?? "").trim()) { - suggestions.push({ section: "Personal", severity: "tip", message: "Adding a location helps with geography-based job matching." }); + suggestions.push({ + section: "Personal", + severity: "tip", + message: "Adding a location helps with geography-based job matching.", + }); } const summary = (p.summary ?? "").trim(); if (!summary) { - suggestions.push({ section: "Personal", severity: "warning", message: "Write a professional summary — a 2-3 sentence elevator pitch increases engagement." }); + suggestions.push({ + section: "Personal", + severity: "warning", + message: + "Write a professional summary — a 2-3 sentence elevator pitch increases engagement.", + }); } else if (summary.length < 50) { - suggestions.push({ section: "Personal", severity: "tip", message: "Your summary is rather short. Aim for 50-200 characters with key strengths." }); + suggestions.push({ + section: "Personal", + severity: "tip", + message: + "Your summary is rather short. Aim for 50-200 characters with key strengths.", + }); } else if (summary.length > 300) { - suggestions.push({ section: "Personal", severity: "tip", message: "Your summary is quite long. Keep it concise — 2-3 impactful sentences work best." }); + suggestions.push({ + section: "Personal", + severity: "tip", + message: + "Your summary is quite long. Keep it concise — 2-3 impactful sentences work best.", + }); } - if (!(p.linkedin ?? "").trim() && !(p.github ?? "").trim() && !(p.website ?? "").trim()) { - suggestions.push({ section: "Personal", severity: "tip", message: "Add at least one professional link (LinkedIn, GitHub, or portfolio)." }); + if ( + !(p.linkedin ?? "").trim() && + !(p.github ?? "").trim() && + !(p.website ?? "").trim() + ) { + suggestions.push({ + section: "Personal", + severity: "tip", + message: + "Add at least one professional link (LinkedIn, GitHub, or portfolio).", + }); } // Experience if (data.experience.length === 0) { - suggestions.push({ section: "Experience", severity: "warning", message: "Add work experience — it's the most important section for most roles." }); + suggestions.push({ + section: "Experience", + severity: "warning", + message: + "Add work experience — it's the most important section for most roles.", + }); } for (const exp of data.experience) { if (!exp.position.trim() || !exp.company.trim()) { - suggestions.push({ section: "Experience", severity: "error", message: `Fill in position and company for all experience entries.` }); + suggestions.push({ + section: "Experience", + severity: "error", + message: `Fill in position and company for all experience entries.`, + }); break; } } for (const exp of data.experience) { if (!exp.description.trim()) { - suggestions.push({ section: "Experience", severity: "warning", message: `Add a description for "${exp.position || "untitled role"}" — explain what you did.` }); + suggestions.push({ + section: "Experience", + severity: "warning", + message: `Add a description for "${exp.position || "untitled role"}" — explain what you did.`, + }); } } - const noHighlights = data.experience.filter((e) => e.highlights.filter((h) => h.trim()).length === 0); + const noHighlights = data.experience.filter( + (e) => e.highlights.filter((h) => h.trim()).length === 0, + ); if (noHighlights.length > 0 && data.experience.length > 0) { - suggestions.push({ section: "Experience", severity: "tip", message: "Add bullet-point highlights with quantifiable achievements (e.g., 'Increased sales by 30%')." }); + suggestions.push({ + section: "Experience", + severity: "tip", + message: + "Add bullet-point highlights with quantifiable achievements (e.g., 'Increased sales by 30%').", + }); } - const hasNumbers = data.experience.some( - (e) => /\d+%|\$\d|[0-9]+[xX]/.test(e.description + e.highlights.join(" ")) + const hasNumbers = data.experience.some((e) => + /\d+%|\$\d|[0-9]+[xX]/.test(e.description + e.highlights.join(" ")), ); if (data.experience.length > 0 && !hasNumbers) { - suggestions.push({ section: "Experience", severity: "tip", message: "Use numbers and metrics in your experience (e.g., 'managed team of 8', 'reduced costs by 20%')." }); + suggestions.push({ + section: "Experience", + severity: "tip", + message: + "Use numbers and metrics in your experience (e.g., 'managed team of 8', 'reduced costs by 20%').", + }); } const hasActionVerbs = data.experience.some((e) => /^(Led|Built|Developed|Managed|Designed|Implemented|Created|Launched|Improved|Reduced|Increased|Achieved)/m.test( - e.highlights.join("\n") - ) + e.highlights.join("\n"), + ), ); if (data.experience.length > 0 && !hasActionVerbs) { - suggestions.push({ section: "Experience", severity: "tip", message: "Start highlight bullets with strong action verbs: Led, Built, Improved, Reduced, etc." }); + suggestions.push({ + section: "Experience", + severity: "tip", + message: + "Start highlight bullets with strong action verbs: Led, Built, Improved, Reduced, etc.", + }); } // Education if (data.education.length === 0) { - suggestions.push({ section: "Education", severity: "warning", message: "Add your education background — most employers expect at least one entry." }); + suggestions.push({ + section: "Education", + severity: "warning", + message: + "Add your education background — most employers expect at least one entry.", + }); } for (const edu of data.education) { if (!edu.institution.trim() || !edu.degree.trim()) { - suggestions.push({ section: "Education", severity: "error", message: "Fill in institution and degree for all education entries." }); + suggestions.push({ + section: "Education", + severity: "error", + message: "Fill in institution and degree for all education entries.", + }); break; } } // Skills if (data.skills.length === 0) { - suggestions.push({ section: "Skills", severity: "warning", message: "Add skill categories — ATS systems scan for specific keywords." }); + suggestions.push({ + section: "Skills", + severity: "warning", + message: "Add skill categories — ATS systems scan for specific keywords.", + }); } const emptyCategories = data.skills.filter((s) => s.items.length === 0); if (emptyCategories.length > 0) { - suggestions.push({ section: "Skills", severity: "warning", message: "Some skill categories are empty — add items or remove the category." }); + suggestions.push({ + section: "Skills", + severity: "warning", + message: + "Some skill categories are empty — add items or remove the category.", + }); } // Projects if (data.projects.length === 0 && data.experience.length < 2) { - suggestions.push({ section: "Projects", severity: "tip", message: "With limited experience, adding projects helps demonstrate your abilities." }); + suggestions.push({ + section: "Projects", + severity: "tip", + message: + "With limited experience, adding projects helps demonstrate your abilities.", + }); } // General length - const totalHighlights = data.experience.reduce((n, e) => n + e.highlights.filter((h) => h.trim()).length, 0); + const totalHighlights = data.experience.reduce( + (n, e) => n + e.highlights.filter((h) => h.trim()).length, + 0, + ); if (totalHighlights > 20) { - suggestions.push({ section: "General", severity: "tip", message: "You have many highlights. Keep your resume to 1-2 pages for best results." }); + suggestions.push({ + section: "General", + severity: "tip", + message: + "You have many highlights. Keep your resume to 1-2 pages for best results.", + }); } return suggestions; } -const severityStyles: Record = { +const severityStyles: Record< + Severity, + { dot: string; text: string; bg: string } +> = { error: { dot: "bg-red-500", text: "text-red-700", bg: "bg-red-50" }, - warning: { dot: "bg-yellow-500", text: "text-yellow-700", bg: "bg-yellow-50" }, + warning: { + dot: "bg-yellow-500", + text: "text-yellow-700", + bg: "bg-yellow-50", + }, tip: { dot: "bg-blue-500", text: "text-blue-700", bg: "bg-blue-50" }, }; @@ -125,7 +233,7 @@ export function AISuggestionsPanel() { return (
- ANALYSIS // SUGGESTIONS + Suggestions

Smart Suggestions

@@ -136,19 +244,23 @@ export function AISuggestionsPanel() { {suggestions.length === 0 ? (
-

✓ Looking Great!

+

+ ✓ Looking Great! +

No issues found. Your resume is well-structured.

) : ( <> -
+
{errors.length > 0 && ( {errors.length} critical )} {warnings.length > 0 && ( - {warnings.length} warnings + + {warnings.length} warnings + )} {tips.length > 0 && ( {tips.length} tips @@ -161,10 +273,16 @@ export function AISuggestionsPanel() { return (
- +
- {s.section} - {s.message} + + {s.section} + + + {s.message} +
diff --git a/src/components/builder/builder-client.tsx b/src/components/builder/builder-client.tsx index c0f39af..1e836d7 100644 --- a/src/components/builder/builder-client.tsx +++ b/src/components/builder/builder-client.tsx @@ -230,7 +230,7 @@ export function BuilderClient({ resume }: BuilderClientProps) { : "opacity-0 translate-y-2" }`} > -
+
All changes saved
diff --git a/src/components/builder/builder-form.tsx b/src/components/builder/builder-form.tsx index 3f82b49..3d0aecc 100644 --- a/src/components/builder/builder-form.tsx +++ b/src/components/builder/builder-form.tsx @@ -182,11 +182,11 @@ export function BuilderForm() { data.personalInfo.fullName.trim() && data.personalInfo.email.trim(), ); return hasContact ? ( - + OK ) : ( - + Required ); @@ -263,7 +263,7 @@ export function BuilderForm() {
-

NAVIGATION

+

Navigation

Section {Math.max(activeIndex + 1, 1)} of{" "} {visibleSections.length} @@ -274,10 +274,10 @@ export function BuilderForm() {

-
+
Reordering: {activeSectionLabel}
@@ -404,7 +404,7 @@ export function BuilderForm() {
-

SECTION NAVIGATION

+

Section navigation

Section {Math.max(activeIndex + 1, 1)} of{" "} {visibleSections.length} @@ -418,7 +418,7 @@ export function BuilderForm() { type="button" onClick={() => goToAdjacentSection("prev")} disabled={!hasPrev} - className="border border-gray-400 px-3 py-2 text-[10px] font-bold uppercase tracking-wider hover:bg-black hover:text-white transition-colors duration-150 disabled:opacity-30" + className="border border-gray-400 px-3 py-2 text-[10px] font-bold hover:bg-note transition-colors duration-150 disabled:opacity-30" title="Previous section" > Prev @@ -427,7 +427,7 @@ export function BuilderForm() { type="button" onClick={() => goToAdjacentSection("next")} disabled={!hasNext} - className="border border-gray-400 px-3 py-2 text-[10px] font-bold uppercase tracking-wider hover:bg-black hover:text-white transition-colors duration-150 disabled:opacity-30" + className="border border-gray-400 px-3 py-2 text-[10px] font-bold hover:bg-note transition-colors duration-150 disabled:opacity-30" title="Next section" > Next @@ -449,7 +449,7 @@ export function BuilderForm() { key={section.id} data-section-id={section.id} onClick={() => setActiveSection(section.id)} - className={`snap-start px-3 py-2 text-[11px] sm:text-xs font-bold uppercase tracking-wider whitespace-nowrap transition-all duration-300 border flex items-center gap-1.5 ${ + className={`snap-start px-3 py-2 text-[11px] sm:text-xs font-bold whitespace-nowrap transition-all duration-300 border flex items-center gap-1.5 ${ isJustMoved ? "bg-yellow-400 text-black border-yellow-500 ring-2 ring-yellow-300 scale-105" : activeSection === section.id @@ -476,10 +476,10 @@ export function BuilderForm() { @@ -255,45 +255,45 @@ export function BuilderHeader({

- SIGN IN TO EXPORT + Sign in to export
) : ( <>
- +
{writingStatus && (

)} {exportError && ( -

+

{exportError}

)} @@ -331,12 +331,12 @@ export function BuilderHeader({ {/* Font selector */}
- FONT: + Font: setPdfLocale(e.target.value)} - className="border border-black bg-transparent px-3 py-2 text-xs font-bold tracking-wider transition-all duration-150" + className="border border-black bg-transparent px-3 py-2 text-xs font-bold transition-all duration-150" > {PDF_LOCALES.map((loc) => (