Conversation
📝 WalkthroughWalkthroughIntroduces a new "LingoLaunch" community project—a complete Next.js-based multilingual page builder. Includes authentication, dashboard for page management, editor for content creation with sections, preview mode, locale switching with automatic translation sync, theme support, localStorage-backed persistence, and localization files for English, Spanish, German, French, and Hindi. Changes
Sequence DiagramssequenceDiagram
participant User
participant LanguageSwitcher
participant Storage
participant API as /api/lingo-sync
participant CMD as lingo run (CLI)
User->>LanguageSwitcher: Select Language
LanguageSwitcher->>Storage: Fetch all content strings
LanguageSwitcher->>Storage: Get new locale
Storage-->>LanguageSwitcher: Return all content
LanguageSwitcher->>API: POST texts array
API->>API: Generate lingo-dynamic-source.tsx
API->>CMD: Execute npx lingo run
CMD-->>API: Translation output
API-->>LanguageSwitcher: Success response
LanguageSwitcher->>Storage: Update locale in localStorage
LanguageSwitcher-->>User: Render in new language
sequenceDiagram
participant User
participant Editor as EditorPage
participant Storage
participant Lingo as Storage API
User->>Editor: Load page by ID
Editor->>Storage: getPage(pageId)
Storage-->>Editor: Return SitePage with sections
User->>Editor: Add new section
Editor->>Lingo: addSection(pageId, type)
Lingo-->>Storage: Create section with default content
Storage-->>Editor: Return updated page
User->>Editor: Edit section content
Editor->>Lingo: updateSection(pageId, sectionId, content)
Lingo-->>Storage: Update section content
User->>Editor: Reorder sections
Editor->>Lingo: moveSectionUp/Down(pageId, sectionId)
Lingo-->>Storage: Persist new order
Storage-->>Editor: Refresh page data
Editor-->>User: Display updated page
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (12)
community/lingo-launch/app/lingo-dynamic-source.tsx-14-14 (1)
14-14:⚠️ Potential issue | 🟡 MinorDemo content contains informal, unprofessional text.
Line 14:
"Here are the prices for using this application its 1000 rs one time payment"— this reads like placeholder/test content with grammatical issues. If this ships as the default template, replace with polished copy or clearly mark it as sample data.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/lingo-dynamic-source.tsx` at line 14, The demo copy inside the JSX call to t(...) is informal and grammatically incorrect; replace the string passed to t (the translation key or default text used in lingo-dynamic-source.tsx) with polished, professional copy or mark it explicitly as sample/demo text (e.g., "Sample pricing: one-time payment of ₹1,000") and, if using i18n keys, add a proper translation key and entries instead of inline placeholder text to ensure production users don't see the test sentence.community/lingo-launch/next.config.ts-9-9 (1)
9-9:⚠️ Potential issue | 🟡 MinorHindi (
hi) locale is missing fromtargetLocales.The
i18n.jsondeclareshias a target locale, locale files exist for it, but it is not listed intargetLocales. Add"hi"to align the configuration:Suggested change
- targetLocales: ["es", "de", "fr"], + targetLocales: ["es", "de", "fr", "hi"],🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/next.config.ts` at line 9, The i18n targetLocales array is missing the Hindi locale; update the targetLocales configuration in next.config.ts to include "hi" so it matches i18n.json and the existing locale files (edit the targetLocales array where it currently contains ["es", "de", "fr"] to also include "hi").community/lingo-launch/app/lib/storage.ts-42-44 (1)
42-44:⚠️ Potential issue | 🟡 Minor
saveUsersandsavePageslack theisBrowser()guard present in their corresponding read functions.
getUsers()(line 37) andgetAllPages()(line 79) checkisBrowser()before accessinglocalStorage, butsaveUsersandsavePagesdo not. If these are ever invoked in an SSR context (e.g., during Next.js pre-rendering), they'll throw aReferenceErroronlocalStorage.🛡️ Add guards
function saveUsers(users: User[]) { + if (!isBrowser()) return; localStorage.setItem('lingolaunch_users', JSON.stringify(users)); }function savePages(pages: SitePage[]) { + if (!isBrowser()) return; localStorage.setItem('lingolaunch_pages', JSON.stringify(pages)); }Also applies to: 101-103
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/lib/storage.ts` around lines 42 - 44, The saveUsers and savePages functions access localStorage without checking for a browser environment; wrap their bodies with the same isBrowser() guard used by getUsers and getAllPages (e.g., return early if !isBrowser()) so they never call localStorage in SSR, and ensure any callers relying on their return behavior still work (no-op or return false/undefined as appropriate); update saveUsers and savePages (the functions referenced alongside getUsers and getAllPages) to match the defensive pattern used by the read functions.community/lingo-launch/locales/fr.json-1-5 (1)
1-5:⚠️ Potential issue | 🟡 MinorMinor inconsistency: "lingo" is lowercase here and in
es.json, but capitalized as "Lingo" inde.json.Line 3 uses "lingo" (lowercase), while
de.jsonuses "Lingo" (capitalized). Consider aligning the brand name casing across all locale files.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/locales/fr.json` around lines 1 - 5, The brand name casing is inconsistent in the French locale: change the value for "hero.subtitle" (and any other occurrences of "lingo") to use the same capitalization as other locales (e.g., "Lingo") so the brand reads consistently across translations; locate the "hero.subtitle" key in fr.json and update the string to replace "lingo" with "Lingo".community/lingo-launch/app/editor/[pageId]/page.tsx-91-94 (1)
91-94:⚠️ Potential issue | 🟡 MinorNo confirmation before removing a section — users can accidentally delete content.
handleRemoveimmediately deletes the section without prompting. A simplewindow.confirm()would prevent accidental data loss.🛡️ Suggested fix
const handleRemove = () => { + if (!window.confirm('Are you sure you want to remove this section?')) return; removeSection(pageId, section.id); onUpdate(); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/editor/`[pageId]/page.tsx around lines 91 - 94, The handleRemove function currently calls removeSection(pageId, section.id) and onUpdate() immediately, risking accidental deletion; update handleRemove to prompt the user (e.g., via window.confirm with a clear message) and only call removeSection(pageId, section.id) and onUpdate() if the user confirms, otherwise do nothing; keep the confirmation logic inside handleRemove so callers don't need to change.community/lingo-launch/app/register/page.tsx-62-110 (1)
62-110:⚠️ Potential issue | 🟡 MinorSame missing
htmlFor/idpairing on all form labels as inLoginPage.The name, email, and password
<label>elements all lackhtmlFor, and the inputs they describe have noid. Add matchinghtmlFor/idpairs ("name","email","password") to all three field pairs, following the same fix pattern noted inLoginPage.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/register/page.tsx` around lines 62 - 110, Add matching htmlFor/id attributes for the three form field pairs in register/page.tsx: update the Name label and its input to use htmlFor="name" and id="name", the Email label and input to use htmlFor="email" and id="email", and the Password label and input to use htmlFor="password" and id="password" (the inputs bound via setName, setEmail, setPassword and using the User, Mail, Lock icons should receive the ids).community/lingo-launch/README.md-27-47 (1)
27-47:⚠️ Potential issue | 🟡 MinorREADME describes a different implementation than what was actually built — several references are stale.
The following items in the documented project structure and tech stack do not match the actual code in this PR:
README says Actual code "Node.js File System API" (tech stack, line 27) localStorageonly — no FS APIapi/save-and-compile/(structure, line 40)app/api/lingo-sync/route.tspublic/locales/(structure, line 43)locales/[locale].json(top-level, not underpublic/)lingo.config.ts(structure, line 45)i18n.jsonpublic/locales/{pageId}/en.json(line 78)locales/[locale].jsonAdditionally, the "Install Lingo CLI" step (line 145) runs
pnpm dlx lingo compile, which invokes the compiler — it doesn't install the CLI.Please update the README to reflect the actual implementation so it doesn't mislead contributors.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/README.md` around lines 27 - 47, README is stale and references files/commands that don't exist; update the docs to match the actual implementation: replace "Node.js File System API" with "localStorage" (or note both if applicable), change api/save-and-compile/ to app/api/lingo-sync/route.ts, update public/locales/ and public/locales/{pageId}/en.json to locales/[locale].json at repo root, swap lingo.config.ts references to i18n.json, and fix the "Install Lingo CLI" step to clarify that `pnpm dlx lingo compile` runs the compiler (not installs the CLI) or provide the correct install command; ensure all file/path examples and the tech stack section reference these exact symbols (localStorage, app/api/lingo-sync/route.ts, locales/[locale].json, i18n.json, `pnpm dlx lingo compile`) so the README accurately reflects the code.community/lingo-launch/app/login/page.tsx-55-68 (1)
55-68:⚠️ Potential issue | 🟡 MinorLabels are not programmatically associated with their inputs — accessibility failure.
All
<label>elements on this form (and on the register form) lackhtmlFor, and their corresponding<input>elements lackid. Because the label is a sibling of the input rather than a parent, the browser cannot associate them, meaning screen readers will not announce the label when the input is focused.♿ Proposed fix
-<label className="block text-sm font-medium text-card-foreground mb-1.5"> +<label htmlFor="email" className="block text-sm font-medium text-card-foreground mb-1.5"> Email </label> <div className="relative"> <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <input + id="email" type="email" required ...Apply the same pattern (pair
htmlFor="password"withid="password") to the password field.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/login/page.tsx` around lines 55 - 68, The label for the email input is not programmatically associated with the input which breaks accessibility; add a unique id to the email input (e.g., id="email") and set the label's htmlFor to that id, and do the same for the password field (e.g., id="password" with htmlFor="password") so screen readers can link labels to inputs; update the JSX around the email input (value={email}, onChange={e => setEmail(e.target.value)}, and the corresponding password input/handler) to include the id attributes and matching htmlFor on their <label> elements.community/lingo-launch/app/register/page.tsx-17-35 (1)
17-35:⚠️ Potential issue | 🟡 Minor
setLoadingnever reset tofalseon the success path.On a successful registration (
useris truthy),router.push("/dashboard")is called butsetLoading(false)is not. If navigation is delayed (e.g. slow client-side transition, user navigating back), the submit button stays permanently disabled. The same gap exists inLoginPage.🔧 Proposed fix
const user = register(name, email, password); if (user) { + setLoading(false); router.push("/dashboard"); } else {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/register/page.tsx` around lines 17 - 35, The submit handler handleSubmit sets loading=true but never resets it on success, so the button can remain disabled; update handleSubmit (and the analogous LoginPage handler) to ensure setLoading(false) is called on the success path — e.g., call setLoading(false) just before calling router.push("/dashboard") or wrap the register + navigation flow in a try/finally that calls setLoading(false) in finally; reference the handleSubmit function, the register(...) call, and router.push(...) when making the change.community/lingo-launch/app/globals.css-2-4 (1)
2-4:⚠️ Potential issue | 🟡 MinorStylelint false positives for Tailwind v4 at-rules — configure the ignore list.
@variantand@themeare valid Tailwind CSS v4 at-rules, but the project'sstylelintconfig usesscss/at-rule-no-unknown, which has no built-in awareness of them and flags them as errors. This will fail any lint CI step. Suppress the false positives by adding the Tailwind v4 at-rules to the ignore list in your Stylelint config:// .stylelintrc or equivalent { "rules": { + "scss/at-rule-no-unknown": [true, { + "ignoreAtRules": ["theme", "variant", "utility", "source"] + }] } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/globals.css` around lines 2 - 4, Stylelint is flagging Tailwind v4 at-rules (`@variant` and `@theme`) as unknown via the scss/at-rule-no-unknown rule; update the Stylelint config to add these Tailwind at-rules to the ignore list so they are not treated as errors. Modify your stylelint config (where scss/at-rule-no-unknown is configured) to include an ignoreAtRules array containing "variant" and "theme" (i.e., add "variant" and "theme" to scss/at-rule-no-unknown's ignoreAtRules) so the CSS in globals.css using `@variant` and `@theme` passes linting.community/lingo-launch/README.md-33-100 (1)
33-100:⚠️ Potential issue | 🟡 MinorAdd language identifiers to fenced code blocks.
Seven code blocks (lines 33, 57, 63, 77, 83, 89, 100) are missing language specifiers, triggering
MD040lint warnings frommarkdownlint-cli2. Usetextor the appropriate language tag where a specific syntax isn't needed.📝 Example fix for the directory-tree block and URL blocks
-``` +```text lingo-launch/ ...-``` +```text /editor/{pageId}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/README.md` around lines 33 - 100, Several fenced code blocks in README.md (the directory tree block showing "lingo-launch/" and the example path/code blocks like "/editor/{pageId}", "/editor/landing", the generated file paths "public/locales/{pageId}/en.json", the commands "lingo compile", and the preview path) lack language specifiers and cause MD040 lint warnings; fix by adding a language tag (use text for plain examples) to each fenced block so they become e.g. ```text ... ```—update the blocks around the directory-tree and all example path/command blocks mentioned in the diff.community/lingo-launch/app/page.tsx-176-204 (1)
176-204:⚠️ Potential issue | 🟡 MinorAdd Hindi to the supported languages showcase on the marketing page.
Hindi is configured as a supported target language in
i18n.jsonwith a correspondinglocales/hi.jsonfile, but the language list on lines 186-191 includes only EN, ES, DE, FR. Add Hindi to the showcase or clarify why it's excluded.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/page.tsx` around lines 176 - 204, The language showcase array in the JSX section (the inline array mapped over to render language cards) is missing Hindi; update that array used in the map callback (the object list with elements like { code: "EN", name: "English" }) to include Hindi (e.g., { code: "HI", name: "हिन्दी" } or "Hindi") so the Hindi locale configured in i18n.json/locales/hi.json is displayed, or alternatively add a comment in that same array explaining why Hindi is intentionally excluded.
🧹 Nitpick comments (12)
community/lingo-launch/next.config.ts (1)
5-14: Consider naming the exported function for better debuggability.An anonymous default export makes stack traces harder to read.
Proposed fix
-export default async function (): Promise<NextConfig> { +export default async function createNextConfig(): Promise<NextConfig> {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/next.config.ts` around lines 5 - 14, The exported async function is anonymous which hurts stack traces; give it a descriptive name (for example getNextConfig or createNextConfig) while keeping it as the default export so stack traces show that function name; locate the anonymous default export function in next.config.ts (the exported async function returning withLingo(nextConfig, {...})) and change its declaration to a named function (e.g., export default async function getNextConfig(): Promise<NextConfig>) so the symbol appears in error traces and logs.community/lingo-launch/app/lingo-dynamic-source.tsx (1)
1-53: Auto-generated file is committed to version control.This file is generated by the
/api/lingo-syncroute and marked "Do not edit manually." Consider addingapp/lingo-dynamic-source.tsxto.gitignoreto avoid merge conflicts and noisy diffs. Regeneration can happen on demand via the API route.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/lingo-dynamic-source.tsx` around lines 1 - 53, The auto-generated component LingoDynamicSource is being committed; stop committing it by adding "app/lingo-dynamic-source.tsx" to .gitignore and remove the tracked file from git (git rm --cached) so future regenerations from the /api/lingo-sync route won't cause noisy diffs or merge conflicts; ensure your CI/build still runs the generator or documents how to regenerate the file on demand.community/lingo-launch/theme/ThemeProvider.tsx (2)
16-16: Unsafe cast of localStorage value.
localStorage.getItem('theme') as Themeblindly trusts the stored value. If localStorage is corrupted or tampered with, this could set an invalid theme string.Proposed fix
- const savedTheme = localStorage.getItem('theme') as Theme || 'light' + const stored = localStorage.getItem('theme'); + const savedTheme: Theme = stored === 'dark' ? 'dark' : 'light';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/theme/ThemeProvider.tsx` at line 16, The code unsafely casts localStorage.getItem('theme') to Theme; update ThemeProvider to validate the stored value before using it by adding a type guard for the Theme type (e.g., isTheme(value): value is Theme) and replace the direct cast for savedTheme with a safe lookup that returns the stored value only if isTheme(localValue) is true, otherwise fall back to a default like 'light' so corrupted/tampered values cannot propagate.
12-27: Theme flash (FOUC) on page load for dark-mode users.The initial state is
"light", and localStorage is read only after mount in an effect. This means dark-mode users will see a flash of the light theme on every navigation. Additionally, Effect 2 (lines 20-27) runs before the state update from Effect 1 propagates, briefly writing"light"back to localStorage.A common fix is to initialize state from localStorage synchronously (guarded for SSR), or inject a blocking
<script>in the<head>that sets thedarkclass before paint:Proposed fix — lazy initializer
- const [theme, setTheme] = useState<Theme>("light") - - useEffect(() => { - const savedTheme = localStorage.getItem('theme') as Theme || 'light' - setTheme(savedTheme) - }, []) + const [theme, setTheme] = useState<Theme>(() => { + if (typeof window !== 'undefined') { + return (localStorage.getItem('theme') as Theme) || 'light'; + } + return 'light'; + })Note: With React SSR,
useStateinitializers still run on the server, so thetypeof windowguard is needed. For a fully flicker-free experience, a blocking script in<head>is the gold standard.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/theme/ThemeProvider.tsx` around lines 12 - 27, ThemeProvider currently seeds theme state as "light" and reads localStorage in a mount effect, causing a flash and an immediate overwrite; change the useState initializer to a lazy initializer that synchronously reads localStorage when available (guarded with typeof window !== 'undefined') so theme is correct on first render, remove the first useEffect that reads savedTheme, and keep the existing effect that applies the document.documentElement.classList change and writes localStorage (the effect that depends on theme) so subsequent updates still sync; refer to ThemeProvider, the theme/setTheme state, and the useEffect that adds/removes 'dark' for locating the code to modify.community/lingo-launch/app/components/SectionRenderer.tsx (1)
3-4: Unused imports:useLingoanduseLingoLocaleare imported but never used.Line 4 imports
useLingoanduseLingoLocalefromlingo.dev/react-client, but neither is referenced anywhere in this file.🧹 Remove unused imports
import { useLingoContext } from '@lingo.dev/compiler/react'; -import { useLingo, useLingoLocale } from 'lingo.dev/react-client';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/components/SectionRenderer.tsx` around lines 3 - 4, The file imports unused hooks useLingo and useLingoLocale from 'lingo.dev/react-client'; remove those imports to avoid dead code and lint failures. Open SectionRenderer.tsx, locate the import line that contains useLingo and useLingoLocale and delete those two symbols so only useLingoContext (or any remaining used imports) are imported.community/lingo-launch/app/components/LanguageSwitcher.tsx (1)
71-71:as anycast onsetLocalesuppresses type checking.If
setLocaleexpects a specific union type (e.g.,'en' | 'es' | 'de' | 'fr' | 'hi'), thelanguage.codestrings in thelanguagesarray should be typed accordingly instead of casting toany.🔧 Type-safe fix
-const languages = [ +const languages: { code: 'en' | 'es' | 'de' | 'fr' | 'hi'; label: string }[] = [ { code: 'en', label: 'English' }, { code: 'es', label: 'Español' }, { code: 'de', label: 'Deutsch' }, { code: 'fr', label: 'Français' }, { code: 'hi', label: 'हिंदी' }, ];Then:
- setLocale(language.code as any); + setLocale(language.code);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/components/LanguageSwitcher.tsx` at line 71, The code uses an unsafe "as any" cast when calling setLocale(setLocale) with language.code; instead make this type-safe by giving the languages array (or the Language type) a precise union type for code (e.g., type Locale = 'en' | 'es' | 'de' | 'fr' | 'hi') and update LanguageSwitcher to use that type so language.code already matches the expected parameter type of setLocale (or, if setLocale's type is declared elsewhere, narrow language.code with a proper cast to that Locale type rather than any). Update the Language type or languages constant and/or the setLocale prop type so setLocale(language.code) needs no "as any".community/lingo-launch/app/lib/storage.ts (1)
138-145:updatePagespreadsPartial<SitePage>without filtering out immutable fields.
{ ...pages[index], ...updates, updatedAt: ... }allows callers to accidentally overwriteid,userId, andcreatedAt. Consider destructuring out the fields that should never change:🛡️ Proposed fix
export function updatePage(pageId: string, updates: Partial<SitePage>): SitePage | null { const pages = getAllPages(); const index = pages.findIndex((p) => p.id === pageId); if (index === -1) return null; - pages[index] = { ...pages[index], ...updates, updatedAt: new Date().toISOString() }; + const { id: _id, userId: _uid, createdAt: _ca, ...safeUpdates } = updates; + pages[index] = { ...pages[index], ...safeUpdates, updatedAt: new Date().toISOString() }; savePages(pages); return pages[index]; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/lib/storage.ts` around lines 138 - 145, The updatePage function currently spreads the incoming Partial<SitePage> directly into pages[index], allowing immutable fields to be overwritten; update the function (updatePage and its use of pages[index]) to explicitly exclude id, userId, and createdAt from incoming updates by building a sanitized update object containing only allowed mutable fields (or by destructuring updates to remove those keys) and then merge that sanitized object with the existing page and set updatedAt before calling savePages.community/lingo-launch/app/editor/[pageId]/page.tsx (1)
216-222:setTimeoutinhandleSavePageInfois not cleaned up on unmount.If the user navigates away within 2 seconds of saving,
setSaved(false)fires on an unmounted component. While React 19 suppresses the warning, it's cleaner to store the timer ID and clear it.🧹 Suggested improvement
+ const savedTimerRef = useRef<ReturnType<typeof setTimeout>>(); + const handleSavePageInfo = () => { if (!page) return; updatePage(page.id, { title: title.trim(), description: description.trim() }); setSaved(true); - setTimeout(() => setSaved(false), 2000); + clearTimeout(savedTimerRef.current); + savedTimerRef.current = setTimeout(() => setSaved(false), 2000); loadPage(); }; + + useEffect(() => { + return () => clearTimeout(savedTimerRef.current); + }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/editor/`[pageId]/page.tsx around lines 216 - 222, The setTimeout created in handleSavePageInfo can call setSaved(false) after the component unmounts; change it to store the timer ID (e.g., in a useRef like saveTimerRef) and clear any existing timer before creating a new one, then clear the timer in a useEffect cleanup on unmount; update handleSavePageInfo (which calls updatePage, setSaved, loadPage) to use the ref-based timer management so setSaved(false) is never invoked after unmount.community/lingo-launch/app/components/Providers.tsx (1)
15-17: HardcodedvalidLocaleslist andas anycast.
validLocalesduplicates thetargetsarray fromi18n.json— these will silently diverge if a new target locale is added to the config but not here. Consider importing or deriving the list from a single source of truth. Theas anycast onsetLocalecan be replaced withas stringonceuseLingoContexttypings are confirmed:-setLocale(savedLocale as any); +setLocale(savedLocale as string);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/components/Providers.tsx` around lines 15 - 17, Replace the hardcoded validLocales array and the unsafe "as any" cast by deriving the locale list from the single source of truth (i18n.json targets) and tightening typings: import or require the targets array from i18n.json (or export it from your i18n helper) and use that instead of validLocales in the conditional, and change setLocale(savedLocale as any) to setLocale(savedLocale as string) after verifying useLingoContext's setLocale accepts string; update imports and types where needed so valid locale checking and setLocale use the same typed source.community/lingo-launch/app/components/Navbar.tsx (1)
33-96: Consider addingaria-labelto the<nav>element for accessibility.Screen readers benefit from a label to distinguish this navigation landmark from others on the page.
♻️ Suggested fix
- <nav className="sticky top-0 z-50 bg-card/80 backdrop-blur-md border-b border-border"> + <nav aria-label="Main navigation" className="sticky top-0 z-50 bg-card/80 backdrop-blur-md border-b border-border">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/components/Navbar.tsx` around lines 33 - 96, The nav landmark in the Navbar component (Navbar.tsx) is missing an aria-label for screen readers; update the <nav> element in the Navbar component to include a clear, descriptive ARIA label (e.g., aria-label="Main navigation" or "Primary navigation") so assistive tech can distinguish this navigation region.community/lingo-launch/app/page.tsx (1)
17-57: Hardcoded hex colors bypass the design token system.Lines 30-32 use hardcoded hex values (
#0ea5e9,#8b5cf6) for the gradient, while the rest of the page consistently uses design tokens likechart-2,chart-5,primary, etc. Consider using theme-aware tokens here for consistency and to ensure the gradient adapts to theme changes (light/dark).♻️ Suggested fix
- <span className="bg-gradient-to-r from-[`#0ea5e9`] to-[`#8b5cf6`] bg-clip-text text-transparent"> + <span className="bg-gradient-to-r from-chart-2 to-chart-5 bg-clip-text text-transparent">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/page.tsx` around lines 17 - 57, The gradient in the hero H1 is using hardcoded hex colors in the span (the bg-gradient-to-r from-[`#0ea5e9`] to-[`#8b5cf6`] class) which bypasses theme tokens; replace those hex values with theme-aware design tokens or CSS vars (e.g., use your existing token classes like from-chart-2 to-chart-5 or from-[var(--token-chart-2)] to-[var(--token-chart-5)] / utility classes that map to tokens) so the gradient uses the same token names as the rest of the page and will adapt to light/dark themes; update the span that holds "Launch Everywhere" accordingly.community/lingo-launch/app/layout.tsx (1)
27-39: Set<html lang>dynamically based on selected locale.The
<html lang="en">is hardcoded, but LingoLaunch supports multiple locales (en, es, de, fr, hi) via client-side context. Thelangattribute impacts accessibility, search engines, and browser spell-check. Currently, locale state lives inlocalStorage(seeLocaleSyncin Providers.tsx), making server-side lang attribution difficult in Next.js App Router without middleware or a[locale]routing segment. Track as a follow-up improvement for the community project.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@community/lingo-launch/app/layout.tsx` around lines 27 - 39, The root layout currently hardcodes <html lang="en">; update it so the lang attribute is set dynamically from the app's locale instead of "en". Locate the RootLayout in layout.tsx and use a server-determined locale prop (or a routing segment/middleware) to pass the current locale into the RootLayout and set html lang accordingly; if you opt to keep client-side locale via Providers.LocaleSync (in Providers.tsx) then add a short-term approach to mirror that locale to the server (e.g., add a [locale] route segment or middleware to read and inject locale) and ensure the <html> element reads that locale prop rather than the hardcoded "en". Reference symbols: layout.tsx RootLayout/return block, Providers component, and LocaleSync in Providers.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@community/lingo-launch/app/api/lingo-sync/route.ts`:
- Around line 9-55: The POST handler (exported POST) currently writes files
(filePath / lingo-dynamic-source.tsx) and executes a shell command via
execAsync('npx lingo run') with no auth; add an authentication/authorization
gate at the top of POST that validates a secret/API key from headers or session
against a server-side env var (e.g., process.env.LINGO_SYNC_KEY) and immediately
return a 401/403 NextResponse when missing/invalid; additionally validate and
sanitize the incoming texts array (ensure strings, length limits, escape
content) before building fileContent, restrict filePath to a known safe location
and prevent path traversal, and only run execAsync when auth succeeds—log and
return errors safely using NextResponse with appropriate status codes.
- Line 46: The execAsync call that runs 'npx lingo run' can hang indefinitely;
update the call site in the route handler where execAsync is used to pass a
timeout (or an AbortSignal) to the spawn/exec options so the child is killed
after a reasonable period (e.g., 30s), and add explicit error handling for the
timeout case to log/return a clear error response; locate the execAsync
invocation (the const { stdout, stderr } = await execAsync('npx lingo run', {
cwd: process.cwd() }); line) and modify its options to include timeout/abort and
wrap the await in a try/catch that handles and logs timeout errors and ensures
the child process is cleaned up.
- Line 40: The sync call fs.writeFileSync(filePath, fileContent, 'utf-8') blocks
the event loop; replace it with the async promise-based API
(fs.promises.writeFile or imported fs.promises) and await it inside the request
handler (make the handler async if not already), and wrap the await in try/catch
to surface and log errors before returning the response. Target the
fs.writeFileSync(...) invocation in route.ts (inside the API request handler
function) and change to an awaited fs.promises.writeFile(...) call with proper
error handling.
- Line 31: The generated TypeScript interpolation in texts.map that emits
{t("${text.replace(/"/g, '\\"')}")} is vulnerable to code injection because only
double quotes are escaped; update the sanitizer used before interpolation (the
texts.map arrow callback) to first escape backslashes, then escape double
quotes, backticks, sequence `${` (or replace/escape both '$' and '{'), and
convert newlines to safe escape sequences so user input cannot break out of the
t() call or inject template expressions; ensure this robust escaping is applied
wherever the same pattern is used and keep the sanitized value passed into t()
(reference the texts.map(...) expression and the t(...) call).
In `@community/lingo-launch/app/components/Providers.tsx`:
- Around line 27-31: LocaleSync currently returns null while mounted is false,
blocking ThemeProvider and all children and causing a blank-page flash; change
LocaleSync so it always returns the children (do not gate render on the mounted
flag) and instead perform locale restoration asynchronously (e.g., in useEffect)
to call your locale-setting logic after mount; specifically, remove the early
"if (!mounted) return null" behavior in the LocaleSync component and move any
localStorage read / setLocale calls into an effect that runs on mount so
ThemeProvider and children render immediately while the saved locale is applied
once available.
In `@community/lingo-launch/app/components/SectionRenderer.tsx`:
- Around line 9-10: The code currently destructures `{ t: translate }` from
useLingoContext (in SectionRenderer) which is undefined at runtime; replace that
with the proper hook `useTranslation()` to obtain the `t` function (e.g., const
{ t: translate } = useTranslation()), remove the `// `@ts-ignore`` line, and
update all section render branches so translation is applied consistently —
change hero, features, and text sections to call `translate(...)` for any
user-facing strings the same way cta and testimonial sections do, and ensure the
existing translate calls (used in SectionRenderer render functions around the
cta/testimonial branches) keep the correct `translate` symbol.
In `@community/lingo-launch/app/dashboard/page.tsx`:
- Around line 53-56: handleDelete currently performs a destructive delete
immediately; update it to prompt the user for confirmation before proceeding
(e.g., use window.confirm or open a confirmation modal) and only call
deletePage(pageId) and setPages(...) when the user confirms; keep the function
signature handleDelete(pageId: string) and ensure any modal result or
window.confirm boolean gates both deletePage and the state update to prevent
accidental permanent deletions.
- Around line 86-146: The modal opened when showCreate is true lacks Escape-key
and backdrop-click dismissal; add an onKeyDown on the overlay element (the fixed
inset-0 div) that listens for e.key === 'Escape' and calls setShowCreate(false),
and add an onClick on the same overlay that closes the modal unless the click is
inside the inner modal—achieve this by adding onClick={() =>
setShowCreate(false)} to the overlay and stopping propagation on the inner modal
div (the element with classes "bg-card rounded-2xl ...") via onClick={(e) =>
e.stopPropagation()}; ensure focus is managed so Escape works (keep autoFocus on
the Page Title input) and existing handlers like handleCreate remain unchanged.
In `@community/lingo-launch/app/lib/storage.ts`:
- Around line 1-6: The User interface and the login/register flows in storage.ts
currently accept and persist plaintext passwords; update the implementation to
avoid normalizing insecure patterns by either hashing passwords client-side
using crypto.subtle.digest (e.g., hash the password in register and compare
hashed values in login) before saving to localStorage, or if you intentionally
keep plaintext for demo simplicity, add a prominent top-of-file comment above
the User interface and the login/register functions stating this is for
demonstration only and not secure for production. Ensure you reference and
update the User interface, and the register and login functions to consistently
use the chosen approach.
In `@community/lingo-launch/app/login/page.tsx`:
- Around line 21-27: The current storage flow saves plaintext passwords and
compares them directly; update app/lib/storage.ts so register and login
asynchronously hash passwords (e.g., SHA-256 via the Web Crypto API) and
store/compare the hex/base64 hash instead of the raw password, making both
register and login return Promises; then update the login flow in page.tsx to
await the async login call (await login(email, password)), handle the resolved
user or error, and keep using router.push("/dashboard") on success and
setError/setLoading on failure.
In `@community/lingo-launch/eslint.config.mjs`:
- Around line 1-3: Update the devDependencies entry for "eslint" in package.json
from the broad "^9" range to "^9.22.0" so that imports used in the flat config
(defineConfig and globalIgnores from "eslint/config") are available; modify the
"eslint" version string only (leave other deps like eslint-config-next as-is)
and run npm/yarn install to lock the new minimum version.
---
Nitpick comments:
In `@community/lingo-launch/app/components/LanguageSwitcher.tsx`:
- Line 71: The code uses an unsafe "as any" cast when calling
setLocale(setLocale) with language.code; instead make this type-safe by giving
the languages array (or the Language type) a precise union type for code (e.g.,
type Locale = 'en' | 'es' | 'de' | 'fr' | 'hi') and update LanguageSwitcher to
use that type so language.code already matches the expected parameter type of
setLocale (or, if setLocale's type is declared elsewhere, narrow language.code
with a proper cast to that Locale type rather than any). Update the Language
type or languages constant and/or the setLocale prop type so
setLocale(language.code) needs no "as any".
In `@community/lingo-launch/app/components/Navbar.tsx`:
- Around line 33-96: The nav landmark in the Navbar component (Navbar.tsx) is
missing an aria-label for screen readers; update the <nav> element in the Navbar
component to include a clear, descriptive ARIA label (e.g., aria-label="Main
navigation" or "Primary navigation") so assistive tech can distinguish this
navigation region.
In `@community/lingo-launch/app/components/Providers.tsx`:
- Around line 15-17: Replace the hardcoded validLocales array and the unsafe "as
any" cast by deriving the locale list from the single source of truth (i18n.json
targets) and tightening typings: import or require the targets array from
i18n.json (or export it from your i18n helper) and use that instead of
validLocales in the conditional, and change setLocale(savedLocale as any) to
setLocale(savedLocale as string) after verifying useLingoContext's setLocale
accepts string; update imports and types where needed so valid locale checking
and setLocale use the same typed source.
In `@community/lingo-launch/app/components/SectionRenderer.tsx`:
- Around line 3-4: The file imports unused hooks useLingo and useLingoLocale
from 'lingo.dev/react-client'; remove those imports to avoid dead code and lint
failures. Open SectionRenderer.tsx, locate the import line that contains
useLingo and useLingoLocale and delete those two symbols so only useLingoContext
(or any remaining used imports) are imported.
In `@community/lingo-launch/app/editor/`[pageId]/page.tsx:
- Around line 216-222: The setTimeout created in handleSavePageInfo can call
setSaved(false) after the component unmounts; change it to store the timer ID
(e.g., in a useRef like saveTimerRef) and clear any existing timer before
creating a new one, then clear the timer in a useEffect cleanup on unmount;
update handleSavePageInfo (which calls updatePage, setSaved, loadPage) to use
the ref-based timer management so setSaved(false) is never invoked after
unmount.
In `@community/lingo-launch/app/layout.tsx`:
- Around line 27-39: The root layout currently hardcodes <html lang="en">;
update it so the lang attribute is set dynamically from the app's locale instead
of "en". Locate the RootLayout in layout.tsx and use a server-determined locale
prop (or a routing segment/middleware) to pass the current locale into the
RootLayout and set html lang accordingly; if you opt to keep client-side locale
via Providers.LocaleSync (in Providers.tsx) then add a short-term approach to
mirror that locale to the server (e.g., add a [locale] route segment or
middleware to read and inject locale) and ensure the <html> element reads that
locale prop rather than the hardcoded "en". Reference symbols: layout.tsx
RootLayout/return block, Providers component, and LocaleSync in Providers.tsx.
In `@community/lingo-launch/app/lib/storage.ts`:
- Around line 138-145: The updatePage function currently spreads the incoming
Partial<SitePage> directly into pages[index], allowing immutable fields to be
overwritten; update the function (updatePage and its use of pages[index]) to
explicitly exclude id, userId, and createdAt from incoming updates by building a
sanitized update object containing only allowed mutable fields (or by
destructuring updates to remove those keys) and then merge that sanitized object
with the existing page and set updatedAt before calling savePages.
In `@community/lingo-launch/app/lingo-dynamic-source.tsx`:
- Around line 1-53: The auto-generated component LingoDynamicSource is being
committed; stop committing it by adding "app/lingo-dynamic-source.tsx" to
.gitignore and remove the tracked file from git (git rm --cached) so future
regenerations from the /api/lingo-sync route won't cause noisy diffs or merge
conflicts; ensure your CI/build still runs the generator or documents how to
regenerate the file on demand.
In `@community/lingo-launch/app/page.tsx`:
- Around line 17-57: The gradient in the hero H1 is using hardcoded hex colors
in the span (the bg-gradient-to-r from-[`#0ea5e9`] to-[`#8b5cf6`] class) which
bypasses theme tokens; replace those hex values with theme-aware design tokens
or CSS vars (e.g., use your existing token classes like from-chart-2 to-chart-5
or from-[var(--token-chart-2)] to-[var(--token-chart-5)] / utility classes that
map to tokens) so the gradient uses the same token names as the rest of the page
and will adapt to light/dark themes; update the span that holds "Launch
Everywhere" accordingly.
In `@community/lingo-launch/next.config.ts`:
- Around line 5-14: The exported async function is anonymous which hurts stack
traces; give it a descriptive name (for example getNextConfig or
createNextConfig) while keeping it as the default export so stack traces show
that function name; locate the anonymous default export function in
next.config.ts (the exported async function returning withLingo(nextConfig,
{...})) and change its declaration to a named function (e.g., export default
async function getNextConfig(): Promise<NextConfig>) so the symbol appears in
error traces and logs.
In `@community/lingo-launch/theme/ThemeProvider.tsx`:
- Line 16: The code unsafely casts localStorage.getItem('theme') to Theme;
update ThemeProvider to validate the stored value before using it by adding a
type guard for the Theme type (e.g., isTheme(value): value is Theme) and replace
the direct cast for savedTheme with a safe lookup that returns the stored value
only if isTheme(localValue) is true, otherwise fall back to a default like
'light' so corrupted/tampered values cannot propagate.
- Around line 12-27: ThemeProvider currently seeds theme state as "light" and
reads localStorage in a mount effect, causing a flash and an immediate
overwrite; change the useState initializer to a lazy initializer that
synchronously reads localStorage when available (guarded with typeof window !==
'undefined') so theme is correct on first render, remove the first useEffect
that reads savedTheme, and keep the existing effect that applies the
document.documentElement.classList change and writes localStorage (the effect
that depends on theme) so subsequent updates still sync; refer to ThemeProvider,
the theme/setTheme state, and the useEffect that adds/removes 'dark' for
locating the code to modify.
| export async function POST(req: Request) { | ||
| try { | ||
| const { texts } = await req.json(); | ||
|
|
||
| if (!Array.isArray(texts) || texts.length === 0) { | ||
| return NextResponse.json({ message: 'No texts provided' }, { status: 400 }); | ||
| } | ||
|
|
||
| // Generate the dummy source file content | ||
| // We import useLingo but don't use the hook or component logic, just static t() calls | ||
| // so the compiler picks them up. | ||
| // Actually, lingo might need a valid react component structure. | ||
| const fileContent = ` | ||
| // This file is auto-generated by LingoLaunch to enable dynamic content translation. | ||
| // Do not edit manually. | ||
| import { useLingo } from '@lingo.dev/compiler/react'; | ||
|
|
||
| export default function LingoDynamicSource() { | ||
| const { t } = useLingo(); | ||
|
|
||
| return ( | ||
| <> | ||
| ${texts.map(text => `{/* @ts-ignore */}\n {t("${text.replace(/"/g, '\\"')}")}`).join('\n ')} | ||
| </> | ||
| ); | ||
| } | ||
| `; | ||
|
|
||
| const filePath = path.join(process.cwd(), 'app', 'lingo-dynamic-source.tsx'); | ||
|
|
||
| // Write the file | ||
| fs.writeFileSync(filePath, fileContent, 'utf-8'); | ||
|
|
||
| // Run lingo run | ||
| // Using npx lingo run. Assuming it's available in the environment. | ||
| // We might need to handle the output/error. | ||
| // CWD should be project root. | ||
| const { stdout, stderr } = await execAsync('npx lingo run', { cwd: process.cwd() }); | ||
|
|
||
| console.log('Lingo Run Output:', stdout); | ||
| if (stderr) console.error('Lingo Run Error:', stderr); | ||
|
|
||
| return NextResponse.json({ success: true, message: 'Translations updated' }); | ||
| } catch (error) { | ||
| console.error('Error in lingo-sync:', error); | ||
| return NextResponse.json({ error: 'Failed to sync translations' }, { status: 500 }); | ||
| } |
There was a problem hiding this comment.
Critical: Unauthenticated endpoint writes arbitrary files and executes shell commands.
This POST handler has no authentication or authorization. Any client can trigger file writes to disk and shell command execution (npx lingo run). In a deployed environment, this is a remote code execution vector. At minimum, gate this behind an auth check or an API key.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@community/lingo-launch/app/api/lingo-sync/route.ts` around lines 9 - 55, The
POST handler (exported POST) currently writes files (filePath /
lingo-dynamic-source.tsx) and executes a shell command via execAsync('npx lingo
run') with no auth; add an authentication/authorization gate at the top of POST
that validates a secret/API key from headers or session against a server-side
env var (e.g., process.env.LINGO_SYNC_KEY) and immediately return a 401/403
NextResponse when missing/invalid; additionally validate and sanitize the
incoming texts array (ensure strings, length limits, escape content) before
building fileContent, restrict filePath to a known safe location and prevent
path traversal, and only run execAsync when auth succeeds—log and return errors
safely using NextResponse with appropriate status codes.
|
|
||
| return ( | ||
| <> | ||
| ${texts.map(text => `{/* @ts-ignore */}\n {t("${text.replace(/"/g, '\\"')}")}`).join('\n ')} |
There was a problem hiding this comment.
Critical: Code injection via insufficient input sanitization.
Only double quotes are escaped (text.replace(/"/g, '\\"')), but the user-supplied text is interpolated into a generated TypeScript source file. An attacker can inject arbitrary code through:
- Backslash sequences (e.g.,
\"to un-escape the quote) - Template literal expressions if the outer template uses backticks
- Newlines to break out of the
t()call
At a minimum, sanitize backslashes before quotes, and also handle ${}, backticks, and newlines:
Proposed safer escaping
- ${texts.map(text => `{/* `@ts-ignore` */}\n {t("${text.replace(/"/g, '\\"')}")}`).join('\n ')}
+ ${texts.map(text => {
+ const safe = text
+ .replace(/\\/g, '\\\\')
+ .replace(/"/g, '\\"')
+ .replace(/\n/g, '\\n')
+ .replace(/\r/g, '\\r')
+ .replace(/`/g, '\\`')
+ .replace(/\$\{/g, '\\${');
+ return `{/* `@ts-ignore` */}\n {t("${safe}")}`;
+ }).join('\n ')}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ${texts.map(text => `{/* @ts-ignore */}\n {t("${text.replace(/"/g, '\\"')}")}`).join('\n ')} | |
| ${texts.map(text => { | |
| const safe = text | |
| .replace(/\\/g, '\\\\') | |
| .replace(/"/g, '\\"') | |
| .replace(/\n/g, '\\n') | |
| .replace(/\r/g, '\\r') | |
| .replace(/`/g, '\\`') | |
| .replace(/\$\{/g, '\\${'); | |
| return `{/* `@ts-ignore` */}\n {t("${safe}")}`; | |
| }).join('\n ')} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@community/lingo-launch/app/api/lingo-sync/route.ts` at line 31, The generated
TypeScript interpolation in texts.map that emits {t("${text.replace(/"/g,
'\\"')}")} is vulnerable to code injection because only double quotes are
escaped; update the sanitizer used before interpolation (the texts.map arrow
callback) to first escape backslashes, then escape double quotes, backticks,
sequence `${` (or replace/escape both '$' and '{'), and convert newlines to safe
escape sequences so user input cannot break out of the t() call or inject
template expressions; ensure this robust escaping is applied wherever the same
pattern is used and keep the sanitized value passed into t() (reference the
texts.map(...) expression and the t(...) call).
| const filePath = path.join(process.cwd(), 'app', 'lingo-dynamic-source.tsx'); | ||
|
|
||
| // Write the file | ||
| fs.writeFileSync(filePath, fileContent, 'utf-8'); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Synchronous file write blocks the event loop.
fs.writeFileSync on an API request path blocks all other requests. Use fs.promises.writeFile instead.
Proposed fix
- fs.writeFileSync(filePath, fileContent, 'utf-8');
+ await fs.promises.writeFile(filePath, fileContent, 'utf-8');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fs.writeFileSync(filePath, fileContent, 'utf-8'); | |
| await fs.promises.writeFile(filePath, fileContent, 'utf-8'); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@community/lingo-launch/app/api/lingo-sync/route.ts` at line 40, The sync call
fs.writeFileSync(filePath, fileContent, 'utf-8') blocks the event loop; replace
it with the async promise-based API (fs.promises.writeFile or imported
fs.promises) and await it inside the request handler (make the handler async if
not already), and wrap the await in try/catch to surface and log errors before
returning the response. Target the fs.writeFileSync(...) invocation in route.ts
(inside the API request handler function) and change to an awaited
fs.promises.writeFile(...) call with proper error handling.
| // Using npx lingo run. Assuming it's available in the environment. | ||
| // We might need to handle the output/error. | ||
| // CWD should be project root. | ||
| const { stdout, stderr } = await execAsync('npx lingo run', { cwd: process.cwd() }); |
There was a problem hiding this comment.
exec has no timeout — can hang indefinitely.
If npx lingo run stalls, this request will never resolve and the connection stays open. Add a timeout.
Proposed fix
- const { stdout, stderr } = await execAsync('npx lingo run', { cwd: process.cwd() });
+ const { stdout, stderr } = await execAsync('npx lingo run', {
+ cwd: process.cwd(),
+ timeout: 60_000, // 60 seconds
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { stdout, stderr } = await execAsync('npx lingo run', { cwd: process.cwd() }); | |
| const { stdout, stderr } = await execAsync('npx lingo run', { | |
| cwd: process.cwd(), | |
| timeout: 60_000, // 60 seconds | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@community/lingo-launch/app/api/lingo-sync/route.ts` at line 46, The execAsync
call that runs 'npx lingo run' can hang indefinitely; update the call site in
the route handler where execAsync is used to pass a timeout (or an AbortSignal)
to the spawn/exec options so the child is killed after a reasonable period
(e.g., 30s), and add explicit error handling for the timeout case to log/return
a clear error response; locate the execAsync invocation (the const { stdout,
stderr } = await execAsync('npx lingo run', { cwd: process.cwd() }); line) and
modify its options to include timeout/abort and wrap the await in a try/catch
that handles and logs timeout errors and ensures the child process is cleaned
up.
| if (!mounted) { | ||
| return null; // or a loading spinner to prevent hydration mismatch/flash | ||
| } | ||
|
|
||
| return <>{children}</>; |
There was a problem hiding this comment.
LocaleSync blocks all page rendering until after hydration — causes blank-page flash.
When !mounted, LocaleSync returns null, which suppresses ThemeProvider and all page children on every initial render. Because restoring the locale from localStorage is a cosmetic side-effect (the page is usable in the default locale immediately), it should not gate the render. Render children unconditionally and apply the saved locale asynchronously:
🔧 Proposed fix
function LocaleSync({ children }: { children: React.ReactNode }) {
const { locale, setLocale } = useLingoContext();
- const [mounted, setMounted] = useState(false);
useEffect(() => {
- setMounted(true);
const savedLocale = localStorage.getItem('lingolaunch_locale');
const validLocales = ['en', 'es', 'de', 'fr', 'hi'];
if (savedLocale && validLocales.includes(savedLocale) && savedLocale !== locale) {
setLocale(savedLocale as any);
}
}, []);
useEffect(() => {
- if (mounted && locale) {
+ if (locale) {
localStorage.setItem('lingolaunch_locale', locale);
}
- }, [locale, mounted]);
+ }, [locale]);
- if (!mounted) {
- return null;
- }
-
return <>{children}</>;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!mounted) { | |
| return null; // or a loading spinner to prevent hydration mismatch/flash | |
| } | |
| return <>{children}</>; | |
| function LocaleSync({ children }: { children: React.ReactNode }) { | |
| const { locale, setLocale } = useLingoContext(); | |
| useEffect(() => { | |
| const savedLocale = localStorage.getItem('lingolaunch_locale'); | |
| const validLocales = ['en', 'es', 'de', 'fr', 'hi']; | |
| if (savedLocale && validLocales.includes(savedLocale) && savedLocale !== locale) { | |
| setLocale(savedLocale as any); | |
| } | |
| }, []); | |
| useEffect(() => { | |
| if (locale) { | |
| localStorage.setItem('lingolaunch_locale', locale); | |
| } | |
| }, [locale]); | |
| return <>{children}</>; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@community/lingo-launch/app/components/Providers.tsx` around lines 27 - 31,
LocaleSync currently returns null while mounted is false, blocking ThemeProvider
and all children and causing a blank-page flash; change LocaleSync so it always
returns the children (do not gate render on the mounted flag) and instead
perform locale restoration asynchronously (e.g., in useEffect) to call your
locale-setting logic after mount; specifically, remove the early "if (!mounted)
return null" behavior in the LocaleSync component and move any localStorage read
/ setLocale calls into an effect that runs on mount so ThemeProvider and
children render immediately while the saved locale is applied once available.
| const handleDelete = (pageId: string) => { | ||
| deletePage(pageId); | ||
| setPages((prev) => prev.filter((p) => p.id !== pageId)); | ||
| }; |
There was a problem hiding this comment.
Missing confirmation before destructive delete action.
handleDelete immediately removes the page with no confirmation dialog or undo mechanism. A single misclick on the trash icon permanently deletes user content. Add a window.confirm() or a confirmation modal before proceeding.
🛡️ Minimal fix using window.confirm
const handleDelete = (pageId: string) => {
+ if (!confirm("Are you sure you want to delete this page? This cannot be undone.")) return;
deletePage(pageId);
setPages((prev) => prev.filter((p) => p.id !== pageId));
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleDelete = (pageId: string) => { | |
| deletePage(pageId); | |
| setPages((prev) => prev.filter((p) => p.id !== pageId)); | |
| }; | |
| const handleDelete = (pageId: string) => { | |
| if (!confirm("Are you sure you want to delete this page? This cannot be undone.")) return; | |
| deletePage(pageId); | |
| setPages((prev) => prev.filter((p) => p.id !== pageId)); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@community/lingo-launch/app/dashboard/page.tsx` around lines 53 - 56,
handleDelete currently performs a destructive delete immediately; update it to
prompt the user for confirmation before proceeding (e.g., use window.confirm or
open a confirmation modal) and only call deletePage(pageId) and setPages(...)
when the user confirms; keep the function signature handleDelete(pageId: string)
and ensure any modal result or window.confirm boolean gates both deletePage and
the state update to prevent accidental permanent deletions.
| {showCreate && ( | ||
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm px-4"> | ||
| <div className="bg-card rounded-2xl border border-border shadow-xl w-full max-w-md p-6"> | ||
| <div className="flex items-center justify-between mb-6"> | ||
| <h2 className="text-xl font-bold text-card-foreground"> | ||
| Create New Page | ||
| </h2> | ||
| <button | ||
| onClick={() => setShowCreate(false)} | ||
| className="p-1 rounded-md hover:bg-secondary transition-colors" | ||
| > | ||
| <X className="h-5 w-5 text-muted-foreground" /> | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="space-y-4"> | ||
| <div> | ||
| <label className="block text-sm font-medium text-card-foreground mb-1.5"> | ||
| Page Title | ||
| </label> | ||
| <input | ||
| type="text" | ||
| value={newTitle} | ||
| onChange={(e) => setNewTitle(e.target.value)} | ||
| placeholder="My Landing Page" | ||
| className="w-full px-4 py-2.5 bg-background border border-input rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring" | ||
| autoFocus | ||
| onKeyDown={(e) => e.key === "Enter" && handleCreate()} | ||
| /> | ||
| </div> | ||
| <div> | ||
| <label className="block text-sm font-medium text-card-foreground mb-1.5"> | ||
| Description | ||
| </label> | ||
| <textarea | ||
| value={newDesc} | ||
| onChange={(e) => setNewDesc(e.target.value)} | ||
| placeholder="A brief description of your page" | ||
| rows={3} | ||
| className="w-full px-4 py-2.5 bg-background border border-input rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring resize-none" | ||
| /> | ||
| </div> | ||
| <div className="flex gap-3 pt-2"> | ||
| <button | ||
| onClick={() => setShowCreate(false)} | ||
| className="flex-1 px-4 py-2.5 border border-border rounded-lg text-foreground font-medium hover:bg-secondary transition-colors" | ||
| > | ||
| Cancel | ||
| </button> | ||
| <button | ||
| onClick={handleCreate} | ||
| disabled={!newTitle.trim()} | ||
| className="flex-1 bg-primary text-primary-foreground px-4 py-2.5 rounded-lg font-semibold hover:opacity-90 transition-opacity disabled:opacity-50" | ||
| > | ||
| Create Page | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Modal is missing Escape-key dismissal and backdrop click-to-close.
The create-page modal doesn't close when pressing Escape or clicking the backdrop overlay. Both are standard expectations for modal UX and accessibility (WCAG dialog patterns).
♻️ Suggested improvements
Add an onKeyDown handler on the overlay div and an onClick on the backdrop:
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm px-4">
- <div className="bg-card rounded-2xl border border-border shadow-xl w-full max-w-md p-6">
+ <div
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm px-4"
+ onClick={() => setShowCreate(false)}
+ onKeyDown={(e) => e.key === "Escape" && setShowCreate(false)}
+ >
+ <div
+ className="bg-card rounded-2xl border border-border shadow-xl w-full max-w-md p-6"
+ onClick={(e) => e.stopPropagation()}
+ >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@community/lingo-launch/app/dashboard/page.tsx` around lines 86 - 146, The
modal opened when showCreate is true lacks Escape-key and backdrop-click
dismissal; add an onKeyDown on the overlay element (the fixed inset-0 div) that
listens for e.key === 'Escape' and calls setShowCreate(false), and add an
onClick on the same overlay that closes the modal unless the click is inside the
inner modal—achieve this by adding onClick={() => setShowCreate(false)} to the
overlay and stopping propagation on the inner modal div (the element with
classes "bg-card rounded-2xl ...") via onClick={(e) => e.stopPropagation()};
ensure focus is managed so Escape works (keep autoFocus on the Page Title input)
and existing handlers like handleCreate remain unchanged.
| export interface User { | ||
| id: string; | ||
| name: string; | ||
| email: string; | ||
| password: string; | ||
| } |
There was a problem hiding this comment.
Plaintext password storage in localStorage.
The User interface stores the password as a plain string, and login/register work with raw plaintext. Even for a demo/community project, this normalizes insecure patterns. Consider at minimum hashing with crypto.subtle.digest before storing, or adding a prominent comment that this is intentional for demo purposes only and not suitable for production.
Also applies to: 53-68
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@community/lingo-launch/app/lib/storage.ts` around lines 1 - 6, The User
interface and the login/register flows in storage.ts currently accept and
persist plaintext passwords; update the implementation to avoid normalizing
insecure patterns by either hashing passwords client-side using
crypto.subtle.digest (e.g., hash the password in register and compare hashed
values in login) before saving to localStorage, or if you intentionally keep
plaintext for demo simplicity, add a prominent top-of-file comment above the
User interface and the login/register functions stating this is for
demonstration only and not secure for production. Ensure you reference and
update the User interface, and the register and login functions to consistently
use the chosen approach.
| const user = login(email, password); | ||
| if (user) { | ||
| router.push("/dashboard"); | ||
| } else { | ||
| setError("Invalid email or password. Please try again."); | ||
| setLoading(false); | ||
| } |
There was a problem hiding this comment.
Plaintext passwords stored in and read from localStorage — security risk.
The login function (and register) in app/lib/storage.ts stores the entire User object, including the plain-text password, in localStorage, and authentication is a direct string equality check (u.password === password). Any script executing in the same origin — including via XSS — can enumerate all registered users and their passwords.
At a minimum, derive a hash (e.g. SHA-256 via the Web Crypto API) before persisting passwords and compare hashes on login. Even for a demo, never store or compare raw passwords, as developers routinely reuse credentials:
// In storage.ts register():
-const user: User = { id: generateId(), name, email, password };
+const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(password));
+const passwordHash = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2,"0")).join("");
+const user: User = { id: generateId(), name, email, password: passwordHash };
// In storage.ts login():
-const user = getUsers().find((u) => u.email === email && u.password === password);
+const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(password));
+const passwordHash = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2,"0")).join("");
+const user = getUsers().find((u) => u.email === email && u.password === passwordHash);Note: this also makes login and register async, which needs to be reflected in the page components.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@community/lingo-launch/app/login/page.tsx` around lines 21 - 27, The current
storage flow saves plaintext passwords and compares them directly; update
app/lib/storage.ts so register and login asynchronously hash passwords (e.g.,
SHA-256 via the Web Crypto API) and store/compare the hex/base64 hash instead of
the raw password, making both register and login return Promises; then update
the login flow in page.tsx to await the async login call (await login(email,
password)), handle the resolved user or error, and keep using
router.push("/dashboard") on success and setError/setLoading on failure.
| import { defineConfig, globalIgnores } from "eslint/config"; | ||
| import nextVitals from "eslint-config-next/core-web-vitals"; | ||
| import nextTs from "eslint-config-next/typescript"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat community/lingo-launch/package.jsonRepository: lingodotdev/lingo.dev
Length of output: 696
Update eslint version constraint to ^9.22.0 in package.json.
The eslint version is declared as "^9" in devDependencies, which permits any ESLint 9.x version from 9.0.0 onwards. However, defineConfig and globalIgnores from "eslint/config" were first introduced in ESLint v9.22.0. The current constraint risks installing an incompatible version (e.g., 9.0.0–9.21.x) and breaking the configuration at runtime.
Update the constraint to "^9.22.0" to ensure the minimum required version is installed.
The eslint-config-next version (16.1.6) is appropriate and supports the flat-config subpath exports (/core-web-vitals, /typescript) used in lines 2–3.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@community/lingo-launch/eslint.config.mjs` around lines 1 - 3, Update the
devDependencies entry for "eslint" in package.json from the broad "^9" range to
"^9.22.0" so that imports used in the flat config (defineConfig and
globalIgnores from "eslint/config") are available; modify the "eslint" version
string only (leave other deps like eslint-config-next as-is) and run npm/yarn
install to lock the new minimum version.
Summary
Introduces LingoLaunch — a multilingual landing page builder with authentication, dashboard management, and dynamic language switching.
Changes
Testing
No automated tests added in this PR.
Visuals
Demo video attached below.