Conversation
|
An error occured. This error may be due to rate limits. If this error persists, please email us. |
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThis update delivers a major overhaul of the Zero Mail app's UI, structure, and features. It introduces a new blog system, rebrands and simplifies core pages (about, contributors, privacy, terms, pricing), launches a new signup flow, refactors navigation, and implements a dynamic, interactive mesh background on the homepage. Numerous localization files are reformatted, and new utility modules, UI components, and dependencies are added. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Browser
participant ZeroMailApp
participant UnicornStudio
participant BlogAPI
User->>Browser: Navigates to homepage
Browser->>ZeroMailApp: Loads HomeContent
ZeroMailApp->>UnicornStudio: Dynamically load and init mesh background
User->>ZeroMailApp: Clicks "Create a Free Account"
ZeroMailApp->>ZeroMailApp: Render SignupClient
ZeroMailApp->>ZeroMailApp: Fetch providers, render signup form
User->>Browser: Navigates to /blog
Browser->>ZeroMailApp: Loads BlogPage
ZeroMailApp->>BlogAPI: fetchAllBlogPosts()
BlogAPI-->>ZeroMailApp: Blog post list
ZeroMailApp->>Browser: Render blog grid
User->>Browser: Clicks blog post
Browser->>ZeroMailApp: Loads BlogSlugPage
ZeroMailApp->>BlogAPI: fetchBlogPost(slug)
BlogAPI-->>ZeroMailApp: Blog post content
ZeroMailApp->>Browser: Render blog post with author info
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
cubic analysis
35 issues found across 63 files • Review in cubic
React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.
| "archived": "Archiviert", | ||
| "failedToArchive": "Archivierung fehlgeschlagen", | ||
| "openInNewTab": "In neuem Tab öffnen" | ||
| "failedToArchive": "Archivierung fehlgeschlagen" |
There was a problem hiding this comment.
The translation key 'openInNewTab' was removed from the 'mail' object. If this key is still referenced in the UI or code, this will cause missing translations or runtime errors. Please ensure this removal is intentional and update the PR description accordingly, or restore the key if it is still needed.
Prompt for AI agents
Address the following comment on apps/mail/messages/de.json at line 393:
<comment>The translation key 'openInNewTab' was removed from the 'mail' object. If this key is still referenced in the UI or code, this will cause missing translations or runtime errors. Please ensure this removal is intentional and update the PR description accordingly, or restore the key if it is still needed.</comment>
<file context>
@@ -370,8 +390,7 @@
"failedToMute": "Stummschaltung fehlgeschlagen",
"failedToUnmute": "Aufhebung der Stummschaltung fehlgeschlagen",
"archived": "Archiviert",
- "failedToArchive": "Archivierung fehlgeschlagen",
- "openInNewTab": "In neuem Tab öffnen"
+ "failedToArchive": "Archivierung fehlgeschlagen"
},
"units": {
</file context>
| "failedToArchive": "Archivierung fehlgeschlagen" | |
| "failedToArchive": "Archivierung fehlgeschlagen", | |
| "openInNewTab": "In neuem Tab öffnen" |
| role: "CTO & Co-Founder", | ||
| location: { | ||
| city: "Vancouver", | ||
| country: "BC" |
There was a problem hiding this comment.
"BC" is a province abbreviation, not a country; this makes the location data inaccurate.
Prompt for AI agents
Address the following comment on apps/mail/data/employees.ts at line 46:
<comment>"BC" is a province abbreviation, not a country; this makes the location data inaccurate.</comment>
<file context>
@@ -0,0 +1,184 @@
+export interface Employee {
+ id: string;
+ fullName: string;
+ role: string;
+ location: {
+ city: string;
+ country: string;
+ };
+ timezone: string;
</file context>
| country: "BC" | |
| country: "Canada" |
| role: "CEO & Co-Founder", | ||
| location: { | ||
| city: "San Francisco", | ||
| country: "CA" |
There was a problem hiding this comment.
"CA" is a state abbreviation, not a country; this will surface incorrect location data wherever the record is used.
Prompt for AI agents
Address the following comment on apps/mail/data/employees.ts at line 27:
<comment>"CA" is a state abbreviation, not a country; this will surface incorrect location data wherever the record is used.</comment>
<file context>
@@ -0,0 +1,184 @@
+export interface Employee {
+ id: string;
+ fullName: string;
+ role: string;
+ location: {
+ city: string;
+ country: string;
+ };
+ timezone: string;
</file context>
| country: "CA" | |
| country: "USA" |
| "countPlural=0": "ответы", | ||
| "countPlural=one": "# ответ", | ||
| "countPlural=other": "# ответов" | ||
| "countPlural=other": "# ответы" |
There was a problem hiding this comment.
The plural form for "countPlural=other" in "replies" should be "# ответов" instead of "# ответы" to match correct Russian grammar for pluralization.
Prompt for AI agents
Address the following comment on apps/mail/messages/ru.json at line 340:
<comment>The plural form for "countPlural=other" in "replies" should be "# ответов" instead of "# ответы" to match correct Russian grammar for pluralization.</comment>
<file context>
@@ -312,12 +327,17 @@
"mail": {
"replies": [
{
- "declarations": ["input count", "local countPlural = count: plural"],
- "selectors": ["countPlural"],
+ "declarations": [
+ "input count",
+ "local countPlural = count: plural"
+ ],
</file context>
| "countPlural=other": "# ответы" | |
| "countPlural=other": "# ответов" |
| "countPlural=0": "Добавьте заметки", | ||
| "countPlural=one": "# заметка", | ||
| "countPlural=other": "# заметок" | ||
| "countPlural=other": "# заметки" |
There was a problem hiding this comment.
The plural form for "countPlural=other" in "noteCount" should be "# заметок" instead of "# заметки" to match correct Russian grammar for pluralization.
Prompt for AI agents
Address the following comment on apps/mail/messages/ru.json at line 272:
<comment>The plural form for "countPlural=other" in "noteCount" should be "# заметок" instead of "# заметки" to match correct Russian grammar for pluralization.</comment>
<file context>
@@ -249,12 +259,17 @@
"search": "Найти заметки...",
"noteCount": [
{
- "declarations": ["input count", "local countPlural = count: plural"],
- "selectors": ["countPlural"],
+ "declarations": [
+ "input count",
+ "local countPlural = count: plural"
+ ],
</file context>
| "countPlural=other": "# заметки" | |
| "countPlural=other": "# заметок" |
| flex items-center justify-center text-xs font-medium text-gray-600 dark:text-gray-300 | ||
| shadow-lg | ||
| `} | ||
| style={{ zIndex: 0 }} |
There was a problem hiding this comment.
Rule violated: Detect React performance bottlenecks and rule breaking
Defining object literals directly in JSX props (such as the 'style' prop) causes a new object to be created on every render, leading to unnecessary re-renders. Move the style object outside the JSX or memoize it to avoid performance issues.
Prompt for AI agents
Address the following comment on apps/mail/components/ui/overlapping-avatars.tsx at line 65:
<comment>Defining object literals directly in JSX props (such as the 'style' prop) causes a new object to be created on every render, leading to unnecessary re-renders. Move the style object outside the JSX or memoize it to avoid performance issues.</comment>
<file context>
@@ -0,0 +1,74 @@
+import React from 'react';
+import { getAllEmployees, Employee } from '@/data/employees';
+import { EmployeeHoverCard } from './employee-hover-card';
+
+interface OverlappingAvatarsProps {
+ maxDisplay?: number;
+ size?: 'sm' | 'md' | 'lg';
+ className?: string;
+}
</file context>
| {/* Investors Grid */} | ||
| <div className="grid grid-cols-2 lg:grid-cols-4 gap-8 justify-items-center"> | ||
| {investors.map((investor, index) => { | ||
| const totalInvestors = investors.length; |
There was a problem hiding this comment.
Rule violated: Prevent Side Effects or Logic Inside Render Functions or Loops
Non-trivial logic (array length calculation, modulo, and conditional logic) is performed inside the React render loop. Move this logic outside the render function or loop to prevent performance issues and repeated computation.
Prompt for AI agents
Address the following comment on apps/mail/app/(full-width)/about.tsx at line 99:
<comment>Non-trivial logic (array length calculation, modulo, and conditional logic) is performed inside the React render loop. Move this logic outside the render function or loop to prevent performance issues and repeated computation.</comment>
<file context>
@@ -1,159 +1,170 @@
-import { Card, CardHeader, CardTitle } from '@/components/ui/card';
-import { Github, Mail, ArrowLeft } from 'lucide-react';
-import { Navigation } from '@/components/navigation';
+import { useTheme } from 'next-themes';
+import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
-import Footer from '@/components/home/footer';
import React from 'react';
</file context>
| size = 'md', | ||
| className = '' | ||
| }) => { | ||
| const allEmployees = getAllEmployees(); |
There was a problem hiding this comment.
Rule violated: Prevent Side Effects or Logic Inside Render Functions or Loops
Calling getAllEmployees() directly inside the render function can cause repeated side effects or performance issues if it is not a pure, trivial function. Move this logic to a useEffect or memoization hook to prevent unnecessary executions.
Prompt for AI agents
Address the following comment on apps/mail/components/ui/overlapping-avatars.tsx at line 16:
<comment>Calling getAllEmployees() directly inside the render function can cause repeated side effects or performance issues if it is not a pure, trivial function. Move this logic to a useEffect or memoization hook to prevent unnecessary executions.</comment>
<file context>
@@ -0,0 +1,74 @@
+import React from 'react';
+import { getAllEmployees, Employee } from '@/data/employees';
+import { EmployeeHoverCard } from './employee-hover-card';
+
+interface OverlappingAvatarsProps {
+ maxDisplay?: number;
+ size?: 'sm' | 'md' | 'lg';
+ className?: string;
+}
</file context>
| ) : ( | ||
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5"> | ||
| {blogPosts.map((post, index) => { | ||
| const author = post.authorId ? getEmployeeById(post.authorId) : null; |
There was a problem hiding this comment.
Rule violated: Prevent Side Effects or Logic Inside Render Functions or Loops
Calling getEmployeeById inside the render loop can cause repeated side effects or performance issues. Move all data lookups or non-trivial logic outside the render function or loop and precompute them before rendering.
Prompt for AI agents
Address the following comment on apps/mail/app/(routes)/blog/page.tsx at line 68:
<comment>Calling getEmployeeById inside the render loop can cause repeated side effects or performance issues. Move all data lookups or non-trivial logic outside the render function or loop and precompute them before rendering.</comment>
<file context>
@@ -0,0 +1,154 @@
+
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Calendar, Clock, ArrowRight, User } from 'lucide-react';
+import { Link } from 'react-router';
+
+
+import { useTheme } from 'next-themes';
+import { useEffect, useState } from 'react';
</file context>
| <div | ||
| className="text-[#a1a1aa] leading-relaxed text-lg [&>h1]:text-4xl [&>h1]:font-bold [&>h1]:text-white [&>h1]:mb-6 [&>h1]:mt-12 [&>h1]:leading-tight [&>h2]:text-2xl [&>h2]:font-bold [&>h2]:text-white [&>h2]:mb-5 [&>h2]:mt-10 [&>h2]:leading-tight [&>h3]:text-xl [&>h3]:font-semibold [&>h3]:text-white [&>h3]:mb-4 [&>h3]:mt-8 [&>h3]:leading-tight [&>p]:mb-6 [&>p]:leading-relaxed [&>p]:text-[#a1a1aa] [&>p]:text-lg [&>ul]:mb-6 [&>ul]:ml-6 [&>li]:mb-3 [&>li]:list-disc [&>li]:leading-relaxed [&>li]:text-[#a1a1aa] [&>li]:text-lg [&>strong]:text-white [&>strong]:font-semibold [&>em]:text-white/80 [&>a]:text-blue-400 [&>a]:hover:text-blue-300 [&>a]:underline [&>a]:underline-offset-2 [&>hr]:border-white/10 [&>hr]:my-12" | ||
| dangerouslySetInnerHTML={{ | ||
| __html: post.content |
There was a problem hiding this comment.
Rule violated: Prevent Side Effects or Logic Inside Render Functions or Loops
Extensive Markdown-to-HTML parsing is executed directly inside the component’s render path. This non-trivial processing violates the guideline “Prevent Side Effects or Logic Inside Render Functions or Loops” (section: heavy logic in render) because it runs on every re-render, degrading performance. Move this transformation to useMemo or compute it server-side.
Prompt for AI agents
Address the following comment on apps/mail/app/(routes)/blog/[slug]/page.tsx at line 119:
<comment>Extensive Markdown-to-HTML parsing is executed directly inside the component’s render path. This non-trivial processing violates the guideline “Prevent Side Effects or Logic Inside Render Functions or Loops” (section: heavy logic in render) because it runs on every re-render, degrading performance. Move this transformation to useMemo or compute it server-side.</comment>
<file context>
@@ -0,0 +1,153 @@
+import { useLoaderData } from 'react-router';
+import { useEffect } from 'react';
+import { useTheme } from 'next-themes';
+import { getEmployeeById } from '@/data/employees';
+import { EmployeeHoverCard } from '@/components/ui/employee-hover-card';
+import { fetchBlogPost, type BlogPost } from '@/lib/mdx-utils';
+import type { Route } from './+types/page';
+
+export async function clientLoader({ params }: Route.ClientLoaderArgs) {
</file context>
|
An error occured. This error may be due to rate limits. If this error persists, please email us. |
There was a problem hiding this comment.
Actionable comments posted: 63
🔭 Outside diff range comments (16)
apps/mail/messages/fa.json (4)
168-179: Missing numeric placeholder – users won’t see the count 🚀The new
attachmentCountpattern lists the plural categories but never injects the actual number (#). Persian users will just read “پیوست” regardless of whether there are 2 or 200 attachments. We’re aiming for Mars-grade UX here—let’s actually show the number.- "countPlural=one": "پیوست", - "countPlural=other": "پیوست" + "countPlural=one": "# پیوست", + "countPlural=other": "# پیوست"
184-197: Same blind spot forfileCount– number vanishes into the voidMirror image of the previous issue: the
fileCountmessage drops#, so the figure never leaves the launchpad. Patch it the same way:- "countPlural=one": "فایل", - "countPlural=other": "فایل" + "countPlural=one": "# فایل", + "countPlural=other": "# فایل"
330-341: Zero category missing#again
replieszero state is “پاسخها” with no quantity. Consistency and clarity matter—even at light-speed. Suggest adding a numeral or adopting the pattern fromnoteCount.- "countPlural=0": "پاسخها", + "count=0": "۰ پاسخ",
262-275: Correct zero-case selector and translation in Persian plural
- Location:
apps/mail/messages/fa.json(lines 262–275)- Change the invalid
countPlural=0selector (never matches a plural category) to an explicit zero-case on the raw variable.- Replace the imperative “افزودن یادداشتها” with the numeric count “۰ یادداشت” for consistency.
"match": { - "countPlural=0": "افزودن یادداشتها", + "count=0": "۰ یادداشت", "countPlural=one": "# یادداشت", "countPlural=other": "# یادداشت" }apps/mail/messages/cs.json (2)
166-179: Missing{count}will hide the number – not very rocket-grade telemetry.The
attachmentCountstrings no longer interpolate{count}, so UI will output plain “příloha/příloh” with zero context. Users deserve the actual number.- "countPlural=0": "příloh", - "countPlural=one": "příloha", - "countPlural=other": "příloh" + "countPlural=0": "{count} příloh", + "countPlural=one": "{count} příloha", + "countPlural=other": "{count} příloh"
182-195: Same telemetry gap forfileCount.Keep the data visible; add
{count}as above.- "countPlural=0": "souborů", - "countPlural=one": "soubor", - "countPlural=other": "souborů" + "countPlural=0": "{count} souborů", + "countPlural=one": "{count} soubor", + "countPlural=other": "{count} souborů"apps/mail/messages/zh_TW.json (1)
392-395: Action Required: Align “openInNewTab” Across All LocalesOur scan shows that the
openInNewTabkey is still present in several translation files but missing from most others. You’ll need to decide whether to fully remove this UI string or restore it everywhere.Files containing
"openInNewTab"(line 394):
- apps/mail/messages/tr.json
- apps/mail/messages/pl.json
- apps/mail/messages/cs.json
- apps/mail/messages/zh_TW.json
Next steps (choose one):
- Remove the
"openInNewTab"entry from all above files if the feature has been deprecated.- Reintroduce
"openInNewTab"with appropriate translations in all other locale files to keep your key sets uniform.apps/mail/messages/tr.json (1)
338-341: Minor style point – singular already includes “#”.
"countPlural=one": "# yanıt"prints “1 yanıt” fine, but Turkish usually omits the number for singular (“yanıt”). Up to you, but worth a thought.apps/mail/messages/ja.json (1)
168-190: Plural scaffolding fine, but wording feels off-target.
noteCountfalls back to English (“Add notes”, “# note”). Japanese users shouldn't see English here.- "countPlural=0": "Add notes" - "countPlural=one": "# note" - "countPlural=other": "# notes" + "countPlural=0": "メモを追加" + "countPlural=one": "# 件のメモ" + "countPlural=other": "# 件のメモ"Also applies to: 262-274
apps/mail/messages/ru.json (1)
168-179: Russian declension off-trajectory for plural “attachmentCount”Numbers >1 should use the genitive plural “вложений”.
Leaving “вложения” after 2,5,7 etc. sounds odd to natives.- "countPlural=other": "вложений" + "countPlural=other": "вложений"apps/mail/components/navigation.tsx (3)
2-13: Remove dead imports - we don't carry dead weight to MarsThese imports are no longer used after the refactoring. Clean them up.
import NavigationMenu from '@/components/ui/navigation-menu'; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; -import { GitHub, Twitter, Discord, LinkedIn, Star } from './icons/icons'; -import { AnimatedNumber } from '@/components/ui/animated-number'; -import { signIn, useSession } from '@/lib/auth-client'; -import { Separator } from '@/components/ui/separator'; -import { useQuery } from '@tanstack/react-query'; -import { Link, useNavigate } from 'react-router'; -import { Button } from '@/components/ui/button'; import { useState, useEffect } from 'react'; -import { Menu } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { toast } from 'sonner';
15-75: Remove unused constants - efficiency mattersThese resource and link definitions are orphaned code. If they're not used, they're just burning electricity.
Delete all unused constants from lines 15-75 since they're not referenced in the component.
77-102: State management cleanup needed - simplify like SpaceX enginesMultiple unused state variables and API calls. Keep only what's necessary.
export function Navigation() { - const [open, setOpen] = useState(false); - const [stars, setStars] = useState(0); // Default fallback value const [isScrolled, setIsScrolled] = useState(false); - const { data: session } = useSession(); - const navigate = useNavigate(); - - const { data: githubData } = useQuery({ - queryKey: ['githubStars'], - queryFn: async () => { - const response = await fetch('https://api.github.com/repos/Mail-0/Zero', { - headers: { - Accept: 'application/vnd.github.v3+json', - }, - }); - if (!response.ok) { - throw new Error('Failed to fetch GitHub stars'); - } - return response.json() as Promise<GitHubApiResponse>; - }, - }); - - useEffect(() => { - if (githubData) { - setStars(githubData.stargazers_count || 0); - } - }, [githubData]);apps/mail/tailwind.config.ts (2)
139-146: Houston, we have a problem - duplicate animation definitions detectedYou've defined
fadeInanimation twice - once as a keyframe (lines 139-146) and again in the animation list (line 248). This creates a naming collision that could cause unpredictable behavior.Remove the duplicate or rename one of them:
gauge_fadeIn: 'gauge_fadeIn 1s ease forwards', gauge_fill: 'gauge_fill 1s ease forwards', scaleIn: 'scaleIn 200ms ease', scaleOut: 'scaleOut 200ms ease', - fadeIn: 'fadeIn 200ms ease', + fadeInQuick: 'fadeIn 200ms ease', fadeOut: 'fadeOut 200ms ease',Also applies to: 248-248
1-279: Legacy Tailwind config detected - time to upgrade the propulsion systemAccording to Tailwind v4 best practices, configuration should be done using the
@themedirective in CSS files instead oftailwind.config.js. This is like using chemical propulsion when we have electric.Consider migrating this configuration to CSS using:
/* In your CSS file */ @import "tailwindcss"; @theme { /* Your theme configuration here */ }apps/mail/app/(full-width)/contributors.tsx (1)
68-92: Sequential API calls are like single-stage rockets. Parallelize for speed.You're fetching contributor pages sequentially when they could be parallel.
const { data: allPages } = useQuery({ queryFn: async () => { const pages = await Promise.all([ fetch(`https://api.github.com/repos/${REPOSITORY}/contributors?per_page=100&page=1`).then(res => res.json()), fetch(`https://api.github.com/repos/${REPOSITORY}/contributors?per_page=100&page=2`).then(res => res.json()), fetch(`https://api.github.com/repos/${REPOSITORY}/contributors?per_page=100&page=3`).then(res => res.json()) ]); return pages.flat(); }, queryKey: ['all-contributors', REPOSITORY], });
| const shouldShowSimpleForm = !hasMissingRequiredProviders; | ||
|
|
||
| if (shouldShowSimpleForm) { | ||
| return ( | ||
| <> | ||
| {/* Left Column - Login Form */} | ||
| <div className="flex flex-col h-full"> | ||
| {/* Logo Section */} | ||
| <div className="flex items-center gap-[18px] p-4 pl-6"> | ||
| <a href="/home" className="flex items-center justify-center w-6 h-6 cursor-pointer"> | ||
| <div className="w-[48px] h-[48px] cursor-pointer"> | ||
| <img | ||
| src="/white-icon.svg" | ||
| alt="Zero" | ||
| width={38} | ||
| height={38} | ||
| className="w-full h-full cursor-pointer" | ||
| /> | ||
| </div> | ||
| </a> | ||
| </div> | ||
| <div className="flex flex-1 items-center justify-center px-6 md:px-10"> | ||
| <div className="w-full max-w-xs"> | ||
| {error && ( | ||
| <Alert variant="default" className="mb-6 border-orange-500/40 bg-orange-500/10"> | ||
| <AlertTitle className="text-orange-400">Error</AlertTitle> | ||
| <AlertDescription>Failed to log you in. Please try again.</AlertDescription> | ||
| </Alert> | ||
| )} | ||
| <ErrorMessage /> | ||
| <LoginForm providers={providers} /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Right Column - Image */} | ||
| <div className="bg-muted relative hidden lg:block h-full"> | ||
| <img | ||
| src="/couple.jpeg" | ||
| alt="Login" | ||
| className="absolute inset-0 h-full w-full object-cover brightness-[0.5]" | ||
| /> | ||
| {/* Overlay gradient for better text readability if needed */} | ||
| <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" /> | ||
| </div> | ||
| </> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Component complexity approaching escape velocity
This component has grown quite large with multiple conditional rendering paths. Consider extracting the simplified login form into a separate component for better maintainability - just like how we modularize spacecraft systems.
Would you like me to help refactor this into smaller, more focused components?
🤖 Prompt for AI Agents
In apps/mail/app/(auth)/login/login-client.tsx around lines 128 to 175, the
login form rendering block is large and complex, making the component hard to
maintain. To fix this, extract the entire JSX returned inside the
shouldShowSimpleForm condition into a new separate functional component, for
example, SimpleLoginForm. Then replace the original JSX with a call to this new
component. This modularization will improve readability and maintainability by
isolating the login form logic and UI.
| <button | ||
| onClick={() => toggleProvider(provider.id)} | ||
| className="flex w-full items-center justify-between bg-black/5 px-4 py-3 text-left transition-colors hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10" | ||
| className="flex w-full items-center justify-between bg-white/5 px-4 py-3 text-left transition-colors hover:bg-white/10" | ||
| > |
There was a problem hiding this comment.
Critical issue: Button without type attribute is like a rocket without a guidance system
The button is missing an explicit type prop. In a form context, this defaults to "submit" which could cause unintended form submissions.
<button
+ type="button"
onClick={() => toggleProvider(provider.id)}
className="flex w-full items-center justify-between bg-white/5 px-4 py-3 text-left transition-colors hover:bg-white/10"
>📝 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.
| <button | |
| onClick={() => toggleProvider(provider.id)} | |
| className="flex w-full items-center justify-between bg-black/5 px-4 py-3 text-left transition-colors hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10" | |
| className="flex w-full items-center justify-between bg-white/5 px-4 py-3 text-left transition-colors hover:bg-white/10" | |
| > | |
| <button | |
| type="button" | |
| onClick={() => toggleProvider(provider.id)} | |
| className="flex w-full items-center justify-between bg-white/5 px-4 py-3 text-left transition-colors hover:bg-white/10" | |
| > |
🧰 Tools
🪛 Biome (2.1.2)
[error] 217-220: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
🤖 Prompt for AI Agents
In apps/mail/app/(auth)/login/login-client.tsx around lines 217 to 220, the
button element lacks an explicit type attribute, causing it to default to
"submit" in a form context and potentially trigger unintended form submissions.
Add a type="button" attribute to the button element to ensure it does not submit
the form when clicked.
| useEffect(() => { | ||
| if (error && isErrorToast(error)) { | ||
| toast.error(m[`errorMessages.${error}`]()); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Houston, we have a major problem with this useEffect!
This useEffect is missing its dependency array, which means it runs on every render. This could create an infinite loop of toast notifications - like having a rocket engine that keeps firing after reaching orbit!
useEffect(() => {
if (error && isErrorToast(error)) {
toast.error(m[`errorMessages.${error}`]());
}
- });
+ }, [error]);📝 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.
| useEffect(() => { | |
| if (error && isErrorToast(error)) { | |
| toast.error(m[`errorMessages.${error}`]()); | |
| } | |
| }); | |
| useEffect(() => { | |
| if (error && isErrorToast(error)) { | |
| toast.error(m[`errorMessages.${error}`]()); | |
| } | |
| }, [error]); |
🤖 Prompt for AI Agents
In apps/mail/app/(auth)/signup/error-message.tsx around lines 23 to 27, the
useEffect hook is missing a dependency array, causing it to run on every render
and potentially trigger an infinite loop of toast notifications. Fix this by
adding a dependency array that includes all variables used inside the effect,
specifically 'error' and 'm', to ensure the effect only runs when these values
change.
| export async function clientLoader() { | ||
| const isProd = !import.meta.env.DEV; | ||
|
|
||
| const response = await fetch(import.meta.env.VITE_PUBLIC_BACKEND_URL + '/api/public/providers'); | ||
| const data = (await response.json()) as { allProviders: any[] }; | ||
|
|
||
| return { | ||
| allProviders: data.allProviders, | ||
| isProd, | ||
| }; | ||
| } |
There was a problem hiding this comment.
This clientLoader needs more robust error handling, my friend!
Like launching rockets, we need backup systems when things go wrong. The fetch request could fail spectacularly and we're not catching it. Also, using any[] for providers is like designing a rocket without specifications - technically works but not optimal for long-term success.
export async function clientLoader() {
const isProd = !import.meta.env.DEV;
- const response = await fetch(import.meta.env.VITE_PUBLIC_BACKEND_URL + '/api/public/providers');
- const data = (await response.json()) as { allProviders: any[] };
+ try {
+ const response = await fetch(import.meta.env.VITE_PUBLIC_BACKEND_URL + '/api/public/providers');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = (await response.json()) as { allProviders: AuthProvider[] };
+
+ return {
+ allProviders: data.allProviders,
+ isProd,
+ };
+ } catch (error) {
+ console.error('Failed to fetch providers:', error);
+ return {
+ allProviders: [],
+ isProd,
+ };
+ }
-
- return {
- allProviders: data.allProviders,
- isProd,
- };
}📝 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.
| export async function clientLoader() { | |
| const isProd = !import.meta.env.DEV; | |
| const response = await fetch(import.meta.env.VITE_PUBLIC_BACKEND_URL + '/api/public/providers'); | |
| const data = (await response.json()) as { allProviders: any[] }; | |
| return { | |
| allProviders: data.allProviders, | |
| isProd, | |
| }; | |
| } | |
| export async function clientLoader() { | |
| const isProd = !import.meta.env.DEV; | |
| try { | |
| const response = await fetch( | |
| import.meta.env.VITE_PUBLIC_BACKEND_URL + '/api/public/providers', | |
| ); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = (await response.json()) as { allProviders: AuthProvider[] }; | |
| return { | |
| allProviders: data.allProviders, | |
| isProd, | |
| }; | |
| } catch (error) { | |
| console.error('Failed to fetch providers:', error); | |
| return { | |
| allProviders: [], | |
| isProd, | |
| }; | |
| } | |
| } |
🤖 Prompt for AI Agents
In apps/mail/app/(auth)/signup/page.tsx lines 5 to 15, the clientLoader function
lacks error handling for the fetch request and uses a generic any[] type for
providers. To fix this, wrap the fetch call in a try-catch block to handle
potential errors gracefully, returning a fallback or error state if the fetch
fails. Also, define a proper TypeScript interface for the provider objects
instead of using any[], and use that interface to type the allProviders array
for better type safety and maintainability.
| // Route to title mapping | ||
| const routeTitles: Record<string, string> = { | ||
| '/about': 'About - Zero', | ||
| '/contributors': 'Contributors - Zero', | ||
| '/pricing': 'Pricing - Zero', | ||
| '/privacy': 'Privacy - Zero', | ||
| '/terms': 'Terms - Zero', | ||
| '/team': 'Team - Zero', | ||
| }; |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
We're missing the main blog route in our title mapping constellation!
The /blog route itself isn't included in the routeTitles mapping, which means it'll fall back to just "Zero". That's like having a sign for every Mars colony except Mars itself.
const routeTitles: Record<string, string> = {
'/about': 'About - Zero',
'/contributors': 'Contributors - Zero',
'/pricing': 'Pricing - Zero',
'/privacy': 'Privacy - Zero',
'/terms': 'Terms - Zero',
'/team': 'Team - Zero',
+ '/blog': 'Blog - Zero',
};📝 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.
| // Route to title mapping | |
| const routeTitles: Record<string, string> = { | |
| '/about': 'About - Zero', | |
| '/contributors': 'Contributors - Zero', | |
| '/pricing': 'Pricing - Zero', | |
| '/privacy': 'Privacy - Zero', | |
| '/terms': 'Terms - Zero', | |
| '/team': 'Team - Zero', | |
| }; | |
| // Route to title mapping | |
| const routeTitles: Record<string, string> = { | |
| '/about': 'About - Zero', | |
| '/contributors': 'Contributors - Zero', | |
| '/pricing': 'Pricing - Zero', | |
| '/privacy': 'Privacy - Zero', | |
| '/terms': 'Terms - Zero', | |
| '/team': 'Team - Zero', | |
| '/blog': 'Blog - Zero', | |
| }; |
🤖 Prompt for AI Agents
In apps/mail/app/(full-width)/layout.tsx between lines 6 and 14, the routeTitles
object lacks an entry for the main '/blog' route, causing it to default to just
"Zero" instead of a descriptive title. Add a key-value pair for '/blog' with an
appropriate title like 'Blog - Zero' to the routeTitles mapping to ensure the
blog route has a proper title.
| "replies": [ | ||
| { | ||
| "declarations": ["input count", "local countPlural = count: plural"], | ||
| "selectors": ["countPlural"], | ||
| "declarations": [ | ||
| "input count", | ||
| "local countPlural = count: plural" | ||
| ], | ||
| "selectors": [ | ||
| "countPlural" | ||
| ], | ||
| "match": { | ||
| "countPlural=0": "arquivos", | ||
| "countPlural=one": "# resposta", | ||
| "countPlural=other": "# respostas" | ||
| "countPlural=one": "arquivo", | ||
| "countPlural=other": "arquivos" | ||
| } |
There was a problem hiding this comment.
Wrong payload – “replies” translated as “arquivos”.
We’re asking for “respostas”, got “files” instead. Portuguese speakers will be puzzled.
- "countPlural=0": "arquivos",
- "countPlural=one": "arquivo",
- "countPlural=other": "arquivos"
+ "countPlural=0": "respostas",
+ "countPlural=one": "resposta",
+ "countPlural=other": "respostas"Let’s land the right word before users think their mails turned into attachments.
📝 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.
| "replies": [ | |
| { | |
| "declarations": ["input count", "local countPlural = count: plural"], | |
| "selectors": ["countPlural"], | |
| "declarations": [ | |
| "input count", | |
| "local countPlural = count: plural" | |
| ], | |
| "selectors": [ | |
| "countPlural" | |
| ], | |
| "match": { | |
| "countPlural=0": "arquivos", | |
| "countPlural=one": "# resposta", | |
| "countPlural=other": "# respostas" | |
| "countPlural=one": "arquivo", | |
| "countPlural=other": "arquivos" | |
| } | |
| "replies": [ | |
| { | |
| "declarations": [ | |
| "input count", | |
| "local countPlural = count: plural" | |
| ], | |
| "selectors": [ | |
| "countPlural" | |
| ], | |
| "match": { | |
| "countPlural=0": "respostas", | |
| "countPlural=one": "resposta", | |
| "countPlural=other": "respostas" | |
| } |
🤖 Prompt for AI Agents
In apps/mail/messages/pt.json around lines 328 to 341, the translation for
"replies" is incorrectly given as "arquivos" (files). Replace all instances of
"arquivos" in the match object with the correct Portuguese word for replies,
"respostas", to ensure the translation matches the intended meaning.
| "declarations": [ | ||
| "input count", | ||
| "local countPlural = count: plural" | ||
| ], | ||
| "selectors": [ | ||
| "countPlural" | ||
| ], | ||
| "match": { | ||
| "countPlural=0": "Добавьте заметки", | ||
| "countPlural=one": "# заметка", | ||
| "countPlural=other": "# заметок" | ||
| "countPlural=other": "# заметки" | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
“noteCount” lost proper genitive form
Same linguistic physics as above – “# заметки” is grammatically wrong after most numerals. Use “заметок”.
- "countPlural=other": "# заметки"
+ "countPlural=other": "# заметок"📝 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.
| "declarations": [ | |
| "input count", | |
| "local countPlural = count: plural" | |
| ], | |
| "selectors": [ | |
| "countPlural" | |
| ], | |
| "match": { | |
| "countPlural=0": "Добавьте заметки", | |
| "countPlural=one": "# заметка", | |
| "countPlural=other": "# заметок" | |
| "countPlural=other": "# заметки" | |
| } | |
| "declarations": [ | |
| "input count", | |
| "local countPlural = count: plural" | |
| ], | |
| "selectors": [ | |
| "countPlural" | |
| ], | |
| "match": { | |
| "countPlural=0": "Добавьте заметки", | |
| "countPlural=one": "# заметка", | |
| "countPlural=other": "# заметок" | |
| } |
🤖 Prompt for AI Agents
In apps/mail/messages/ru.json around lines 262 to 273, the plural form for
"noteCount" uses "заметки," which is grammatically incorrect in this context.
Update the "countPlural=other" selector to use the genitive plural form
"заметок" instead of "заметки" to ensure proper Russian grammar after numerals.
| "declarations": [ | ||
| "input count", | ||
| "local countPlural = count: plural" | ||
| ], | ||
| "selectors": [ | ||
| "countPlural" | ||
| ], | ||
| "match": { | ||
| "countPlural=0": "ответы", | ||
| "countPlural=one": "# ответ", | ||
| "countPlural=other": "# ответов" | ||
| "countPlural=other": "# ответы" | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Plural gravity mis-aligned for “replies”
The other-case should read “ответов”, not “ответы”.
- "countPlural=other": "# ответы"
+ "countPlural=other": "# ответов"📝 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.
| "declarations": [ | |
| "input count", | |
| "local countPlural = count: plural" | |
| ], | |
| "selectors": [ | |
| "countPlural" | |
| ], | |
| "match": { | |
| "countPlural=0": "ответы", | |
| "countPlural=one": "# ответ", | |
| "countPlural=other": "# ответов" | |
| "countPlural=other": "# ответы" | |
| } | |
| "declarations": [ | |
| "input count", | |
| "local countPlural = count: plural" | |
| ], | |
| "selectors": [ | |
| "countPlural" | |
| ], | |
| "match": { | |
| "countPlural=0": "ответы", | |
| "countPlural=one": "# ответ", | |
| "countPlural=other": "# ответов" | |
| } |
🤖 Prompt for AI Agents
In apps/mail/messages/ru.json around lines 330 to 341, the plural form for the
"other" case of "replies" is incorrect; it currently reads "ответы" but should
be "ответов". Update the "countPlural=other" value in the "match" object to
"ответов" to correctly reflect the plural grammar.
| "snoozed": "已推迟" | ||
| }, | ||
| "settings": { |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Translation update looks good, but check UI fit.
"snoozed": "已推迟" is shorter – nice. Just make sure the sidebar width still accommodates other long-form locales.
🤖 Prompt for AI Agents
In apps/mail/messages/zh_CN.json around lines 428 to 430, the translation for
"snoozed" was shortened to "已推迟". Verify in the UI that this shorter text does
not cause layout issues, especially in the sidebar, and confirm that the sidebar
width still properly accommodates longer translations from other locales without
truncation or overflow.
| @@ -0,0 +1 @@ | |||
| {"history":[{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"gradient","usesPingPong":false,"speed":0.25,"trackMouse":0,"mouseMomentum":0,"texture":false,"animating":false,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision highp float;in vec2 vTextureCoord;uniform vec2 uMousePos;vec3 getColor(int index) { switch(index) { case 0: return vec3(0, 0, 0); case 1: return vec3(0, 0, 0); case 2: return vec3(0, 0, 0); case 3: return vec3(0, 0, 0); case 4: return vec3(0, 0, 0); case 5: return vec3(0, 0, 0); case 6: return vec3(0, 0, 0); case 7: return vec3(0, 0, 0); case 8: return vec3(0, 0, 0); case 9: return vec3(0, 0, 0); case 10: return vec3(0, 0, 0); case 11: return vec3(0, 0, 0); case 12: return vec3(0, 0, 0); case 13: return vec3(0, 0, 0); case 14: return vec3(0, 0, 0); case 15: return vec3(0, 0, 0); default: return vec3(0.0); } }const float PI = 3.14159265;vec2 rotate(vec2 coord, float angle) { float s = sin(angle); float c = cos(angle); return vec2( coord.x * c - coord.y * s, coord.x * s + coord.y * c ); }out vec4 fragColor;vec3 getColor(vec2 uv) {return vec3(0, 0, 0); }void main() {vec2 uv = vTextureCoord; vec2 pos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos-0.5), 0.0000); uv -= pos; uv /= (0.5000*2.); uv = rotate(uv, (0.0000 - 0.5) * 2. * PI); vec4 color = vec4(getColor(uv), 1.); fragColor = color; }"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = aTextureCoord; }"],"data":{"downSample":0.5,"depth":false,"uniforms":{},"isBackground":true},"id":"effect"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"beam","usesPingPong":false,"thickness":1,"speed":0.25,"trackMouse":0,"mouseMomentum":0,"texture":false,"animating":false,"isMask":0,"states":{"appear":[{"local":{"pendingChanges":{},"changeDebouncer":null,"dragSession":null},"type":"appear","id":"b9b3db10-dab0-492e-8ec5-00ee7bd43239","prop":"thickness","transition":{"duration":1000,"delay":0,"ease":"easeInOutQuart"},"complete":false,"progress":0,"value":0,"endValue":1,"initialized":false,"breakpoints":[],"loop":"none","loopDelay":0,"uniformData":{"type":"1f","name":"uThickness"}}],"scroll":[],"hover":[]},"compiledFragmentShaders":["#version 300 es\nprecision highp float; precision highp int;in vec2 vTextureCoord;uniform sampler2D uTexture;uniform float uThickness; uniform float uTime;uniform vec2 uMousePos; vec3 blend (int blendMode, vec3 src, vec3 dst) { return src + dst; }uint fibonacciHash(uint x) { const uint FIB_HASH = 2654435769u; uint hash = x * FIB_HASH; hash ^= hash >> 16; hash *= 0x85ebca6bu; hash ^= hash >> 13; hash *= 0xc2b2ae35u; hash ^= hash >> 16; return hash; }float randFibo(vec2 xy) { uint x_bits = floatBitsToUint(xy.x); uint y_bits = floatBitsToUint(xy.y); uint y_hash = fibonacciHash(y_bits); uint x_xor_y = x_bits ^ y_hash; uint final_hash = fibonacciHash(x_xor_y); return float(final_hash) / float(0xffffffffu); }vec3 Tonemap_tanh(vec3 x) { x = clamp(x, -40.0, 40.0); return (exp(x) - exp(-x)) / (exp(x) + exp(-x)); }out vec4 fragColor;const float PI = 3.14159265359; const float TWO_PI = 2.0 * PI;float luma(vec3 color) { return dot(color, vec3(0.299, 0.587, 0.114)); }vec3 drawLine(vec2 uv, vec2 center, float scale, float angle) { float radAngle = -angle * TWO_PI; float phase = fract(uTime * 0.01 + 0.5000) * (3. * max(1., scale)) - (1.5 * max(1., scale));vec2 direction = vec2(cos(radAngle), sin(radAngle));vec2 centerToPoint = uv - center;float projection = dot(centerToPoint, direction);float distToLine = length(centerToPoint - projection * direction);float lineRadius = uThickness * 0.25; float brightness = lineRadius / (1. - smoothstep(0.4, 0., distToLine + 0.02));float glowRadius = scale; float glow = smoothstep(glowRadius, 0.0, abs(projection - phase));return brightness * (1.-distToLine)*(1.-distToLine) * vec3(1, 1, 1) * glow; }vec3 getBeam(vec2 uv) { vec2 pos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos-0.5), 0.0000); return drawLine(uv, pos, 1.0000, 0.9990); }void main() { vec2 uv = vTextureCoord; vec4 bg = texture(uTexture, uv);vec3 beam = getBeam(uv); float dither = (randFibo(gl_FragCoord.xy) - 0.5) / 255.0;vec3 blended = blend(1, Tonemap_tanh(beam), bg.rgb); vec3 result = mix(bg.rgb, blended, 1.0000); result += dither;vec4 color = vec4(result, max(bg.a, luma(beam))); fragColor = color;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"depth":false,"uniforms":{},"isBackground":false},"id":"effect1"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"flowField","usesPingPong":false,"speed":0.15,"trackMouse":0,"mouseMomentum":0,"texture":false,"animating":true,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision mediump float;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform float uTime; uniform vec2 uMousePos; uniform vec2 uResolution; float ease (int easingFunc, float t) { return t; }vec3 hash33(vec3 p3) { p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787)); p3 += dot(p3, p3.yxz + 19.19); return -1.0 + 2.0 * fract(vec3( (p3.x + p3.y) * p3.z, (p3.x + p3.z) * p3.y, (p3.y + p3.z) * p3.x )); }float perlin_noise(vec3 p) { vec3 pi = floor(p); vec3 pf = p - pi;vec3 w = pf * pf * (3.0 - 2.0 * pf);float n000 = dot(pf - vec3(0.0, 0.0, 0.0), hash33(pi + vec3(0.0, 0.0, 0.0))); float n100 = dot(pf - vec3(1.0, 0.0, 0.0), hash33(pi + vec3(1.0, 0.0, 0.0))); float n010 = dot(pf - vec3(0.0, 1.0, 0.0), hash33(pi + vec3(0.0, 1.0, 0.0))); float n110 = dot(pf - vec3(1.0, 1.0, 0.0), hash33(pi + vec3(1.0, 1.0, 0.0))); float n001 = dot(pf - vec3(0.0, 0.0, 1.0), hash33(pi + vec3(0.0, 0.0, 1.0))); float n101 = dot(pf - vec3(1.0, 0.0, 1.0), hash33(pi + vec3(1.0, 0.0, 1.0))); float n011 = dot(pf - vec3(0.0, 1.0, 1.0), hash33(pi + vec3(0.0, 1.0, 1.0))); float n111 = dot(pf - vec3(1.0, 1.0, 1.0), hash33(pi + vec3(1.0, 1.0, 1.0)));float nx00 = mix(n000, n100, w.x); float nx01 = mix(n001, n101, w.x); float nx10 = mix(n010, n110, w.x); float nx11 = mix(n011, n111, w.x);float nxy0 = mix(nx00, nx10, w.y); float nxy1 = mix(nx01, nx11, w.y);float nxyz = mix(nxy0, nxy1, w.z);return nxyz; }const float MAX_ITERATIONS = 16.; vec2 flow (in vec2 st) { float aspectRatio = uResolution.x/uResolution.y;vec2 mPos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos-0.5), 0.0000); vec2 pos = mix(vec2(0.5, 0.5), mPos, floor(1.0000)); float dist = ease(0, max(0.,1.-distance(st * vec2(aspectRatio, 1), mPos * vec2(aspectRatio, 1)) * 4. * (1. - 1.0000)));float sprd = (0.5000 + 0.01) / ((aspectRatio + 1.) / 2.); float amt = 0.3500 * 0.01 * dist; if(amt <= 0.) { return st; }for (float i = 0.; i < MAX_ITERATIONS; i++) { vec2 scaled = (st-0.5) * vec2(aspectRatio, 1) + (1. - pos); float perlin = perlin_noise(vec3((scaled-0.5) * (5. * sprd), 0.0000*5. + uTime/60.))-0.5; float ang = (perlin * (360. * (0.5000 * 6.))) * 3.1415926 / 180.; st += vec2(cos(ang), sin(ang)) * amt; st = clamp(st, 0., 1.); }return st; }out vec4 fragColor;void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, mix(uv, flow(uv), 1.0000)); fragColor = color;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"depth":false,"uniforms":{},"isBackground":false},"id":"effect2"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"mouse","usesPingPong":true,"mouseMomentum":0,"texture":false,"animating":false,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision highp float;in vec2 vTextureCoord; in vec3 vVertexPosition;uniform sampler2D uTexture; uniform sampler2D uPingPongTexture; uniform vec2 uResolution;const float PI = 3.1415926; const float ITERATIONS = 24.0;out vec4 fragColor;vec3 rgb2hsv(vec3 c) { vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));float d = q.x - min(q.w, q.y); float e = 1.0e-10; return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); }vec3 chromatic_aberration(vec3 color, vec2 uv, vec2 offset) { vec4 left = texture(uTexture, uv - offset); vec4 right = texture(uTexture, uv + offset);color.r = left.r; color.b = right.b;return color; }vec2 pixelate(vec2 uv) { float aspectRatio = uResolution.x / uResolution.y; float scale = 0.1500 / 2.0; vec2 modulate = mod(vec2(uv.x * aspectRatio, uv.y) - 0.5, (scale + 0.01) / 12.0); return vec2( uv.x - modulate.x / aspectRatio + (0.08333 * scale) / 2.0, uv.y - modulate.y + (0.08333 * scale) / 2.0 ); }vec2 angleToDir(float angle) { float rad = angle * 2.0 * PI; return vec2(cos(rad), sin(rad)); }vec4 pixelTrail(vec2 uv, vec2 mouseDir, float strength) { vec4 color = vec4(0); vec2 distorted = mouseDir * 0.4; vec2 pixelated = uv - distorted; color = texture(uTexture, pixelated); color.rgb = chromatic_aberration(color.rgb, pixelated, distorted * 0.6100 * 0.12); return color; }vec4 getTrailColor(vec2 uv, vec2 mouseDir, float strength) { vec4 color = vec4(0); float aspectRatio = uResolution.x / uResolution.y;return pixelTrail(uv, mouseDir, strength); }void main() { vec2 uv = vTextureCoord; vec2 pingpongUv = uv;pingpongUv = pixelate(pingpongUv);vec3 mouseRgb = texture(uPingPongTexture, pingpongUv).rgb; vec3 mouseTrail = rgb2hsv(mouseRgb);float angle = mouseTrail.x; float strength = mouseTrail.z * (0.4500 * 2.0); vec2 direction = angleToDir(angle); vec2 mouseDir = direction * strength;vec4 color = getTrailColor(uv, mouseDir, strength);fragColor = color; }","#version 300 es\nprecision highp float;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uPingPongTexture; uniform vec2 uPreviousMousePos;uniform vec2 uMousePos; uniform vec2 uResolution;const float PI = 3.1415926; const float TWOPI = 6.2831852;out vec4 fragColor;vec3 hsv2rgb(vec3 c) { vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); }void main() { float aspectRatio = uResolution.x / uResolution.y; vec2 uv = vTextureCoord; vec2 correctedUv = uv * vec2(aspectRatio, 1.0);vec2 dir = (uMousePos - uPreviousMousePos) * vec2(aspectRatio, 1.0); float dist = length(dir); dir = normalize(dir);float rad = 0.3400 * 0.4 * mix(aspectRatio, 1.0, 0.5); float angle = atan(dir.y, dir.x); if (angle < 0.0) angle += TWOPI;float distLine = distance(uPreviousMousePos, uMousePos);float t = clamp(dot(correctedUv - uPreviousMousePos * vec2(aspectRatio, 1.0), dir) / dist, 0.0, 1.0); vec2 closestPoint = mix(uPreviousMousePos, uMousePos, t) * vec2(aspectRatio, 1.0); float distanceToLine = distance(correctedUv, closestPoint);float s = smoothstep(rad, rad * 0.0000, distanceToLine);s = s * s;vec3 color = vec3(angle / TWOPI, 1.0, 1.0); vec3 mouseColor = hsv2rgb(color);vec2 sampleUv = mix(uv, uv / (1.0 + 0.0000 * 0.03) + 0.0000 * 0.015, 0.0000); vec3 lastFrameColor = texture(uPingPongTexture, sampleUv).rgb; lastFrameColor = pow(lastFrameColor, vec3(2.2)); mouseColor = pow(mouseColor, vec3(2.2)); float intensity = min(0.7, dist * 10.0) * s * 0.4; vec3 draw = mix(lastFrameColor, mouseColor, intensity); draw *= pow(0.2100, 0.2); draw = pow(draw, vec3(1.0/2.2)); fragColor = vec4(draw, 1.0); }"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }","#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = aTextureCoord; }"],"data":{"depth":false,"uniforms":{},"isBackground":false},"id":"effect3"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"diffuse","usesPingPong":false,"speed":0.25,"trackMouse":0,"mouseMomentum":0,"texture":false,"animating":false,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision highp float; precision highp int;in vec2 vTextureCoord;uniform sampler2D uTexture;uniform float uTime; uniform float xy;uniform vec2 uMousePos; uniform vec2 uResolution;float ease (int easingFunc, float t) { return t * t; }uint fibonacciHash(uint x) { const uint FIB_HASH = 2654435769u; uint hash = x * FIB_HASH; hash ^= hash >> 16; hash *= 0x85ebca6bu; hash ^= hash >> 13; hash *= 0xc2b2ae35u; hash ^= hash >> 16; return hash; }float randFibo(vec2 xy) { uint x_bits = floatBitsToUint(xy.x); uint y_bits = floatBitsToUint(xy.y); uint y_hash = fibonacciHash(y_bits); uint x_xor_y = x_bits ^ y_hash; uint final_hash = fibonacciHash(x_xor_y); return float(final_hash) / float(0xffffffffu); }const float MAX_ITERATIONS = 24.; const float PI = 3.14159265; const float TWOPI = 6.2831853;out vec4 fragColor;void main() { vec2 uv = vTextureCoord; vec2 pos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos-0.5), 0.0000); float aspectRatio = uResolution.x/uResolution.y; float delta = fract(floor(uTime)/20.); float angle, rotation, amp; float inner = distance(uv * vec2(aspectRatio, 1), pos * vec2(aspectRatio, 1)); float outer = max(0., 1.-distance(uv * vec2(aspectRatio, 1), pos * vec2(aspectRatio, 1))); float amount = 0.0300 * 2.;vec2 mPos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos-0.5), 0.0000); pos = vec2(0.5, 0.5); float dist = ease(1, max(0.,1.-distance(uv * vec2(aspectRatio, 1), mPos * vec2(aspectRatio, 1)) * 4. * (1. - 1.0000)));amount *= dist;vec4 col; if(amount <= 0.001) { col = texture(uTexture, uv); } else { vec4 result = vec4(0); float threshold = max(1. - 0.6400, 2./MAX_ITERATIONS); const float invMaxIterations = 1.0 / float(MAX_ITERATIONS);vec2 dir = vec2(0.6100 / aspectRatio, 1.-0.6100) * amount * 0.4; float iterations = 0.0; for(float i = 1.; i <= MAX_ITERATIONS; i++) { float th = i * invMaxIterations; if(th > threshold) break;float random1 = randFibo(uv + th + delta); float random2 = randFibo(uv + th * 2. + delta); float random3 = randFibo(uv + th * 3. + delta); vec2 ranPoint = vec2(random1 * 2. - 1., random2 * 2. - 1.) * mix(1., random3, 0.8); result += texture(uTexture, uv + ranPoint * dir); iterations += 1.0; }result /= max(1.0, iterations); col = result; } fragColor = col;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"depth":false,"uniforms":{},"isBackground":false},"id":"effect4"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"blur","usesPingPong":false,"trackMouse":0,"mouseMomentum":0,"texture":false,"animating":false,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform vec2 uMousePos; uniform vec2 uResolution; float ease (int easingFunc, float t) { return t; }out vec4 fragColor;const int kernelSize = 36;vec4 BoxBlur(sampler2D tex, vec2 uv, vec2 direction) { vec4 color = vec4(0.0);vec2 pos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos-0.5), 0.0000); float inner = distance(uv, pos); float outer = max(0., 1.-distance(uv, pos)); float amount = 1.0000 * ease(0, mix(inner, outer, 0.5000)); for (int i = 0; i < kernelSize; i++) { float x = float(i - kernelSize / 2) * amount/144.; color += texture(tex, uv + vec2(x) * direction * vec2(0.5000, 1. - 0.5000)); } return color/float(kernelSize); }vec4 blur(vec2 uv, vec2 direction) { return BoxBlur(uTexture, uv, direction); }void main() { vec2 uv = vTextureCoord; vec4 color = vec4(0); int dir = 0 % 2; vec2 direction = dir == 1 ? vec2(0, uResolution.x/uResolution.y) : vec2(1, 0);color = blur(uv, direction); fragColor = color;}","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform vec2 uMousePos; uniform vec2 uResolution; float ease (int easingFunc, float t) { return t; }out vec4 fragColor;const int kernelSize = 36;vec4 BoxBlur(sampler2D tex, vec2 uv, vec2 direction) { vec4 color = vec4(0.0);vec2 pos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos-0.5), 0.0000); float inner = distance(uv, pos); float outer = max(0., 1.-distance(uv, pos)); float amount = 1.0000 * ease(0, mix(inner, outer, 0.5000)); for (int i = 0; i < kernelSize; i++) { float x = float(i - kernelSize / 2) * amount/144.; color += texture(tex, uv + vec2(x) * direction * vec2(0.5000, 1. - 0.5000)); } return color/float(kernelSize); }vec4 blur(vec2 uv, vec2 direction) { return BoxBlur(uTexture, uv, direction); }void main() { vec2 uv = vTextureCoord; vec4 color = vec4(0); int dir = 1 % 2; vec2 direction = dir == 1 ? vec2(0, uResolution.x/uResolution.y) : vec2(1, 0);color = blur(uv, direction); fragColor = color;}","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform vec2 uMousePos; uniform vec2 uResolution; float ease (int easingFunc, float t) { return t; }out vec4 fragColor;const int kernelSize = 36;vec4 BoxBlur(sampler2D tex, vec2 uv, vec2 direction) { vec4 color = vec4(0.0);vec2 pos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos-0.5), 0.0000); float inner = distance(uv, pos); float outer = max(0., 1.-distance(uv, pos)); float amount = 1.0000 * ease(0, mix(inner, outer, 0.5000)); for (int i = 0; i < kernelSize; i++) { float x = float(i - kernelSize / 2) * amount/144.; color += texture(tex, uv + vec2(x) * direction * vec2(0.5000, 1. - 0.5000)); } return color/float(kernelSize); }vec4 blur(vec2 uv, vec2 direction) { return BoxBlur(uTexture, uv, direction); }void main() { vec2 uv = vTextureCoord; vec4 color = vec4(0); int dir = 2 % 2; vec2 direction = dir == 1 ? vec2(0, uResolution.x/uResolution.y) : vec2(1, 0);color = blur(uv, direction); fragColor = color;}","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform vec2 uMousePos; uniform vec2 uResolution; float ease (int easingFunc, float t) { return t; } uint fibonacciHash(uint x) { const uint FIB_HASH = 2654435769u; uint hash = x * FIB_HASH; hash ^= hash >> 16; hash *= 0x85ebca6bu; hash ^= hash >> 13; hash *= 0xc2b2ae35u; hash ^= hash >> 16; return hash; }float randFibo(vec2 xy) { uint x_bits = floatBitsToUint(xy.x); uint y_bits = floatBitsToUint(xy.y); uint y_hash = fibonacciHash(y_bits); uint x_xor_y = x_bits ^ y_hash; uint final_hash = fibonacciHash(x_xor_y); return float(final_hash) / float(0xffffffffu); }out vec4 fragColor;const int kernelSize = 36;vec4 BoxBlur(sampler2D tex, vec2 uv, vec2 direction) { vec4 color = vec4(0.0);vec2 pos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos-0.5), 0.0000); float inner = distance(uv, pos); float outer = max(0., 1.-distance(uv, pos)); float amount = 1.0000 * ease(0, mix(inner, outer, 0.5000)); for (int i = 0; i < kernelSize; i++) { float x = float(i - kernelSize / 2) * amount/144.; color += texture(tex, uv + vec2(x) * direction * vec2(0.5000, 1. - 0.5000)); } return color/float(kernelSize); }vec4 blur(vec2 uv, vec2 direction) { return BoxBlur(uTexture, uv, direction); }void main() { vec2 uv = vTextureCoord; vec4 color = vec4(0); int dir = 3 % 2; vec2 direction = dir == 1 ? vec2(0, uResolution.x/uResolution.y) : vec2(1, 0);color = blur(uv, direction);float dither = (randFibo(gl_FragCoord.xy) - 0.5) / 255.0; color.rgb += dither; fragColor = color;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"downSample":0.25,"depth":false,"uniforms":{},"isBackground":false,"passes":[{"prop":"vertical","value":1,"downSample":0.25},{"prop":"vertical","value":2,"downSample":0.5},{"prop":"vertical","value":3,"downSample":0.5}]},"id":"effect5"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"blinds","usesPingPong":false,"speed":0,"trackMouse":0,"mouseMomentum":0,"texture":false,"animating":true,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision highp float; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform float uTime;uniform vec2 uMousePos; uniform vec2 uResolution;float ease (int easingFunc, float t) { return t * t; }const float STEPS = 10.0; const float PI = 3.14159265359;mat2 rot(float a) { return mat2(cos(a), -sin(a), sin(a), cos(a)); }vec3 chromatic_abberation(vec2 st, vec2 aberrated) { vec4 red = vec4(0); vec4 blue = vec4(0); vec4 green = vec4(0);float invSteps = 1.0 / STEPS; float invStepsHalf = invSteps * 0.5;for(float i = 1.0; i <= STEPS; i++) { vec2 offset = aberrated * (i * invSteps); red += texture(uTexture, st - offset) * invSteps; blue += texture(uTexture, st + offset) * invSteps; green += texture(uTexture, st - offset * 0.5) * invStepsHalf; green += texture(uTexture, st + offset * 0.5) * invStepsHalf; }return vec3(red.r, green.g, blue.b); }vec2 scaleAspect(vec2 st, float aspectRatio) { return st * vec2(aspectRatio, 1.0); }vec2 unscaleAspect(vec2 st) { float aspectRatio = uResolution.x / uResolution.y; return st * vec2(1.0/aspectRatio, 1.0); }vec2 rotate(vec2 st, float angle) { float s = sin(angle); float c = cos(angle); mat2 rot = mat2(c, -s, s, c); return rot * st; }struct StructFunc { vec2 st; vec3 distort; };StructFunc style0(vec2 st, vec2 pos, float divisions, float dist, float amount, vec3 first, vec3 second, vec3 third) { float segment = fract((st.y + 1. - pos.y - 1. + uTime * 0.01) * divisions); segment = mix(segment, smoothstep(0.0, 0.5, segment) - smoothstep(0.5, 1.0, segment), 0.0000); vec3 distort = mix(mix(first, second, segment * 2.), mix(second, third, (segment - 0.5) / (1. - 0.5)), step(0.5, segment)); st.y -= pow(distort.r, dist) / 10. * amount; st.y += pow(distort.b, dist) / 10. * amount;st = rot(0.1053 * 2. * PI) * (st - pos) + pos; st = unscaleAspect(st);return StructFunc(st, distort); }StructFunc getStyle(vec2 st, vec2 pos, float divisions, float dist, float amount, vec3 first, vec3 second, vec3 third) { return style0(st, pos, divisions, dist, amount, first, second, third); }vec4 blinds(vec2 st, float mDist) { float aspectRatio = uResolution.x / uResolution.y; vec2 pos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos - 0.5), 0.0000) * floor(1.0000); pos = scaleAspect(pos, aspectRatio); st = scaleAspect(st, aspectRatio);st = rotate(st - pos, -1. * 0.1053 * 2.0 * PI) + pos;vec3 first = vec3(1, 0, 0); vec3 second = vec3(0, 1, 0); vec3 third = vec3(0, 0, 1); float divisions = 2. + 0.1400 * 30.; float dist = 0.8400 * 4. + 1.; float amount = 1.8000 * mDist;StructFunc result = getStyle(st, pos, divisions, dist, amount, first, second, third); vec4 color = texture(uTexture, result.st);vec2 offset = vec2(pow(result.distort.r, dist), pow(result.distort.b, dist)) * vec2(0.1) * amount; color.rgb = chromatic_abberation(result.st, offset * 1.0000);return color; }out vec4 fragColor;void main() { vec2 uv = vTextureCoord; float aspectRatio = uResolution.x / uResolution.y;vec2 mPos = vec2(0.5, 0.5) + mix(vec2(0), (uMousePos - 0.5), 0.0000); vec2 pos = mix(vec2(0.5, 0.5), mPos, floor(1.0000)); float mDist = ease(1, max(0., 1. - distance(uv * vec2(aspectRatio, 1), mPos * vec2(aspectRatio, 1)) * 4. * (1. - 1.0000)));vec4 col = blinds(uv, mDist); fragColor = col;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"depth":false,"uniforms":{},"isBackground":false},"id":"effect6"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"retro_screen","usesPingPong":false,"speed":0.25,"trackMouse":0,"mouseMomentum":0,"texture":false,"animating":false,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision mediump float;in vec2 vTextureCoord; uniform sampler2D uTexture; uniform float uTime;uniform vec2 uResolution;out vec4 fragColor;vec3 styleOne(vec2 curvedUV) { float size = max(3.0 / 1080.0, 0.028 * (1.0 - 0.7700)); float aspectRatio = uResolution.x / uResolution.y; float aspectCorrection = mix(aspectRatio, 1./aspectRatio, 0.5); vec2 cellSize = vec2(size / aspectRatio, size) * aspectCorrection;vec2 staggeredUV = curvedUV; if (mod(floor(curvedUV.x / cellSize.x), 2.0) > 0.5) { staggeredUV.y += 0.5 * cellSize.y; }vec2 cellCoords = floor(staggeredUV / cellSize) * cellSize;vec2 unstaggerOffset = vec2(0.0); if (mod(floor(curvedUV.x / cellSize.x), 2.0) > 0.5) { unstaggerOffset.y = -0.5 * cellSize.y; }vec2 sampleCoord = cellCoords + 0.5 * cellSize + unstaggerOffset; vec4 texColor = texture(uTexture, sampleCoord);vec2 staggeredCellPos = mod(staggeredUV, cellSize) / cellSize;float segmentWidth = 0.5; vec3 finalColor = vec3(0.0);float distCoord = staggeredCellPos.x;float distRed = abs(distCoord - segmentWidth * 0.5); float distGreen = abs(distCoord - segmentWidth * 1.); float distBlue = abs(distCoord - segmentWidth * 1.5);distRed = min(distRed, 1.0 - distRed); distGreen = min(distGreen, 1.0 - distGreen); distBlue = min(distBlue, 1.0 - distBlue);float softness = 0.75 * segmentWidth; float redFactor = smoothstep(softness, 0.0, distRed * 1.05); float greenFactor = smoothstep(softness, 0.0, distGreen * 1.1); float blueFactor = smoothstep(softness, 0.0, distBlue * 0.9);vec3 blurColor = vec3(0.0); float blurFactor = 1.0 / 9.0; for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { vec2 offset = vec2(float(dx), dy) * cellSize * 0.8100; blurColor += texture(uTexture, sampleCoord + offset).rgb * blurFactor; } } finalColor.r = redFactor * blurColor.r * (3. * 0.3800); finalColor.g = greenFactor * blurColor.g * (3. * 0.3800); finalColor.b = blueFactor * blurColor.b * (3. * 0.3800);float edgeWidth = 0.05; vec2 edgeDistance = abs(staggeredCellPos - 0.5); float edgeFactor = smoothstep(0.45 - edgeWidth, 0.5, max(edgeDistance.x, edgeDistance.y)); edgeFactor = ((1.0 - edgeFactor) + 0.2); finalColor = finalColor * edgeFactor; finalColor = floor(finalColor * 16.0000) / 16.0000;float flicker = 1.0+0.03*cos(sampleCoord.x/6e1 + uTime*2e1); finalColor *= mix(1., flicker, 0.6500);return finalColor; }void main() { vec3 finalColor;vec4 color = texture(uTexture, vTextureCoord);if(color.a == 0.) { fragColor = vec4(0); return; }finalColor = styleOne(vTextureCoord);vec4 bg = texture(uTexture, vTextureCoord);vec4 col = mix(bg, vec4(finalColor, bg.a), 1.0000); fragColor = col;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"depth":false,"uniforms":{},"isBackground":false},"id":"effect7"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"noiseField","usesPingPong":false,"speed":0.1,"texture":false,"animating":true,"mouseMomentum":0,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision mediump float;in vec3 vVertexPosition; in vec2 vTextureCoord; uniform sampler2D uTexture;out vec4 fragColor;void main() { vec2 uv = vTextureCoord; uv = vec2( 1. - uv.x, 1. - uv.y ); vec4 color = texture(uTexture, uv); fragColor = color;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord; uniform mat4 uMVMatrix; uniform mat4 uPMatrix; out vec3 vVertexPosition; out vec2 vTextureCoord;uniform float uTime; uniform vec2 uResolution;vec3 hash33(vec3 p3) { p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787)); p3 += dot(p3, p3.yxz + 19.19); return -1.0 + 2.0 * fract(vec3( (p3.x + p3.y) * p3.z, (p3.x + p3.z) * p3.y, (p3.y + p3.z) * p3.x )); }float perlin_noise(vec3 p) { vec3 pi = floor(p); vec3 pf = p - pi;vec3 w = pf * pf * (3.0 - 2.0 * pf);float n000 = dot(pf - vec3(0.0, 0.0, 0.0), hash33(pi + vec3(0.0, 0.0, 0.0))); float n100 = dot(pf - vec3(1.0, 0.0, 0.0), hash33(pi + vec3(1.0, 0.0, 0.0))); float n010 = dot(pf - vec3(0.0, 1.0, 0.0), hash33(pi + vec3(0.0, 1.0, 0.0))); float n110 = dot(pf - vec3(1.0, 1.0, 0.0), hash33(pi + vec3(1.0, 1.0, 0.0))); float n001 = dot(pf - vec3(0.0, 0.0, 1.0), hash33(pi + vec3(0.0, 0.0, 1.0))); float n101 = dot(pf - vec3(1.0, 0.0, 1.0), hash33(pi + vec3(1.0, 0.0, 1.0))); float n011 = dot(pf - vec3(0.0, 1.0, 1.0), hash33(pi + vec3(0.0, 1.0, 1.0))); float n111 = dot(pf - vec3(1.0, 1.0, 1.0), hash33(pi + vec3(1.0, 1.0, 1.0)));float nx00 = mix(n000, n100, w.x); float nx01 = mix(n001, n101, w.x); float nx10 = mix(n010, n110, w.x); float nx11 = mix(n011, n111, w.x);float nxy0 = mix(nx00, nx10, w.y); float nxy1 = mix(nx01, nx11, w.y);float nxyz = mix(nxy0, nxy1, w.z);return nxyz; }mat4 rotation(float angle) { return mat4( vec4( cos(angle), -sin(angle), 0.0, 0.0 ), vec4( sin(angle), cos(angle), 0.0, 0.0 ), vec4( 0.0, 0.0, 1.0, 0.0 ), vec4( 0.0, 0.0, 0.0, 1.0 ) ); } void main() { vec3 vertexPosition = aVertexPosition; vec3 waveCoord = aVertexPosition; float cumval = 0.; float spr = (0.0700 + 1.) / ((uResolution.x/uResolution.y + 1.) * 0.5) * 10.; float time = 0.0000 * 10. + uTime * 0.05;float value = perlin_noise(vec3(((waveCoord.xy * 0.0700 * 10.) + (vec2(0.5, 0.5) - 0.5) * 20. * 0.0700) * (vec2(0.2, 1.)), time)) * 0.2300;waveCoord.z = 0.; waveCoord.y += mix(value, smoothstep(-1., 0., value) - 1., 0.5000); waveCoord.x += value * 0.02; if(vertexPosition.x == 1.) { waveCoord.x = 1.; } if(vertexPosition.x == -1.) { waveCoord.x = -1.; } if(vertexPosition.y == 1.) { waveCoord.y = 1.; } if(vertexPosition.y == -1.) { waveCoord.y = -1.; }gl_Position = uPMatrix * uMVMatrix * rotation(radians(180.0)) * vec4(waveCoord, 1.);vTextureCoord = aTextureCoord - vec2(0., mix(value, smoothstep(-1., 0., value) - 1., 0.5000)); vVertexPosition = vertexPosition; }"],"data":{"depth":false,"uniforms":{},"isBackground":false,"heightSegments":300,"widthSegments":300},"id":"effect8"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"godrays","usesPingPong":false,"trackMouse":0,"mouseMomentum":0,"texture":false,"animating":false,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture;const float MAX_ITERATIONS = 64.; const float PI2 = 6.28318530718; const float EPSILON = 0.0001; const float stepFactor = 0.0098;vec4 getBrightAreas(vec2 uv) { vec4 color = texture(uTexture, uv); float lum = dot(color.rgb, vec3(0.299, 0.587, 0.114)); color = color * smoothstep(0.6600 - 0.1, 0.6600, lum); return color; }vec4 getColor(vec2 uv) { return getBrightAreas(uv); }out vec4 fragColor;void main() { vec2 uv = vTextureCoord; vec4 color = getColor(uv); if(0 == 2) { fragColor = color;} else { fragColor = color; } }","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture;uniform vec2 uMousePos;uint fibonacciHash(uint x) { const uint FIB_HASH = 2654435769u; uint hash = x * FIB_HASH; hash ^= hash >> 16; hash *= 0x85ebca6bu; hash ^= hash >> 13; hash *= 0xc2b2ae35u; hash ^= hash >> 16; return hash; }float randFibo(vec2 xy) { uint x_bits = floatBitsToUint(xy.x); uint y_bits = floatBitsToUint(xy.y); uint y_hash = fibonacciHash(y_bits); uint x_xor_y = x_bits ^ y_hash; uint final_hash = fibonacciHash(x_xor_y); return float(final_hash) / float(0xffffffffu); }const float MAX_ITERATIONS = 64.; const float PI2 = 6.28318530718; const float EPSILON = 0.0001; const float stepFactor = 0.0098;vec4 godRays(vec2 st, float decay) { vec3 color = vec3(0); float offset = (0.25 + min(1., 0.5200)) * stepFactor; vec2 pos = vec2(0.5, 0.5) - mix(vec2(0), (vec2(1. - uMousePos.x, 1. - uMousePos.y) - 0.5), 0.0000); float weight = 1.0; float bnoz = randFibo(st) * 0.5200; float distanceInfo = 0.0; vec2 marchPos = st;for (float i = 0.0; i < MAX_ITERATIONS; i += 4.0) { for (float j = 0.0; j < 4.0; j++) { float bno = randFibo(st + vec2(i/MAX_ITERATIONS + j/4.0)) * 0.5200; vec2 offbno = vec2(cos(bno) - 0.5, sin(bno) - 0.5); float x = min(0.999, (i + j) * offset) + bnoz * 0.02; float y = min(0.999, (i + j)); marchPos = st * (1.0 - x) + vec2(x * 0.5) + (pos - 0.5) * x + offbno * 0.02 * 1.0000 * x; color += texture(uTexture, marchPos).rgb * weight; distanceInfo += y * weight; weight *= decay; if(weight < 0.01) break; } } return vec4(color / MAX_ITERATIONS, distance(st, marchPos)); }vec4 getGodRays(vec2 uv) { if(0.3400 == 0.) { return vec4(0); } vec4 rays = godRays(uv, 0.972); rays.rgb *= vec3(1, 0.8196078431372549, 0.596078431372549); vec4 color; color.rgb = rays.rgb; color.a = rays.a; return color; }vec4 getColor(vec2 uv) { return getGodRays(uv); }out vec4 fragColor;void main() { vec2 uv = vTextureCoord; vec4 color = getColor(uv); if(1 == 2) { fragColor = color;} else { fragColor = color; } }","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform sampler2D uBgTexture; uniform sampler2D uBlueNoise;uniform vec2 uResolution;const float MAX_ITERATIONS = 64.; const float PI2 = 6.28318530718; const float EPSILON = 0.0001; const float stepFactor = 0.0098;float luma(vec4 color) { return dot(color.rgb, vec3(0.299, 0.587, 0.114)); }float getBlueNoiseOffset(vec2 st) { ivec2 texSize = ivec2(512, 512); vec4 blueNoise = texelFetch(uBlueNoise, ivec2(fract(st * (uResolution)/vec2(texSize) * vec2(texSize.x/texSize.y, 1.0)) * vec2(texSize)) % texSize, 0); return mod((blueNoise.r - 0.5) * PI2, PI2); }vec4 composite(vec2 uv) { vec4 godrays = texture(uTexture, uv); float distanceInfo = godrays.a; float luminance = luma(godrays); float blueNoise = getBlueNoiseOffset(uv) - 0.5; vec2 circNoise = vec2(cos(blueNoise), sin(blueNoise)); float brightnessScale = (1. - (luminance + 0.25)); vec2 offset = circNoise * 0.05 * pow(brightnessScale, 3.) * distanceInfo * 2.; vec4 color = texture(uTexture, uv + offset); vec4 bg = texture(uBgTexture, uv);color.rgb = bg.rgb + (color.rgb * 2.9 * 0.3400 + blueNoise * 0.001); color.a = bg.a + color.r; return color; }vec4 getColor(vec2 uv) { return composite(uv); }out vec4 fragColor;void main() { vec2 uv = vTextureCoord; vec4 color = getColor(uv); if(2 == 2) { fragColor = color;} else { fragColor = color; } }"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"depth":false,"uniforms":{},"isBackground":false,"passes":[{"prop":"pass","value":1,"downSample":0.5},{"prop":"pass","value":2,"includeBg":true}],"texture":{"src":"https://assets.unicorn.studio/media/blue_noise_med.png","sampler":"uBlueNoise"}},"id":"effect9"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"duotone","usesPingPong":false,"texture":false,"animating":false,"mouseMomentum":0,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision mediump float; in vec3 vVertexPosition; in vec2 vTextureCoord; uniform sampler2D uTexture;out vec4 fragColor; void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv); float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); vec3 duotoneColor = mix(vec3(0, 0, 0), vec3(1, 1, 1), gray); color = vec4(mix(color.rgb, duotoneColor, 1.0000), color.a); fragColor = color;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"depth":false,"uniforms":{},"isBackground":false},"id":"effect10"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"bloom","usesPingPong":false,"texture":false,"animating":false,"mouseMomentum":0,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture;out vec4 fragColor;float luma(vec4 color) { return dot(color.rgb, vec3(0.299, 0.587, 0.114)); }vec4 thresholdPass(vec4 color) { color.rgb = pow(color.rgb, vec3(1.0/2.2)); color.rgb = 1.2 * (color.rgb - 0.5) + 0.5; vec4 bloom = color * smoothstep(0.5000 - 0.1, 0.5000, luma(color)); return vec4(bloom.rgb, color.a); }vec4 getColor(vec4 color) { return thresholdPass(color); }void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv); fragColor = getColor(color); }","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture;uniform vec2 uResolution;out vec4 fragColor;float getExponentialWeight(int index) { switch(index) { case 0: return 1.0000000000; case 1: return 0.7165313106; case 2: return 0.5134171190; case 3: return 0.3678794412; case 4: return 0.2636050919; case 5: return 0.1888756057; case 6: return 0.1353352832; case 7: return 0.0969670595; case 8: return 0.0694877157; default: return 0.0; } }vec4 blur(vec2 uv, bool vertical, float radius, bool diamond) { vec4 color = vec4(0.0); float total_weight = 0.0; float aspectRatio = uResolution.x/uResolution.y;vec2 dir; if (diamond) { dir = vertical ? vec2(1, 1) : vec2(1, -1); } else { dir = vertical ? vec2(0, 1) : vec2(1, 0); } dir *= vec2(0.5000, 1. - 0.5000); dir.x /= aspectRatio; vec4 center = texture(uTexture, uv); float center_weight = getExponentialWeight(0); color += center * center_weight; total_weight += center_weight;radius *= 0.7200; for (int i = 1; i <= 8; i++) { float weight = getExponentialWeight(i); float offset = mix(0.015, 0.025, radius) * float(i)/8.; vec4 sample1 = texture(uTexture, uv + offset * dir); vec4 sample2 = texture(uTexture, uv - offset * dir); color += (sample1 + sample2) * weight; total_weight += 2.0 * weight; }return color / total_weight; }vec4 blurPass(vec2 uv, bool vertical, float radius, float intensity, bool diamond) { return blur(uv, vertical, radius, diamond); }vec4 getColor(vec4 color) { return blurPass(vTextureCoord, false, 40., 1.25, true); }void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv); fragColor = getColor(color); }","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform sampler2D uBgTexture;uniform vec2 uResolution;out vec4 fragColor;float luma(vec4 color) { return dot(color.rgb, vec3(0.299, 0.587, 0.114)); }float getExponentialWeight(int index) { switch(index) { case 0: return 1.0000000000; case 1: return 0.7165313106; case 2: return 0.5134171190; case 3: return 0.3678794412; case 4: return 0.2636050919; case 5: return 0.1888756057; case 6: return 0.1353352832; case 7: return 0.0969670595; case 8: return 0.0694877157; default: return 0.0; } }vec4 blur(vec2 uv, bool vertical, float radius, bool diamond) { vec4 color = vec4(0.0); float total_weight = 0.0; float aspectRatio = uResolution.x/uResolution.y;vec2 dir; if (diamond) { dir = vertical ? vec2(1, 1) : vec2(1, -1); } else { dir = vertical ? vec2(0, 1) : vec2(1, 0); } dir *= vec2(0.5000, 1. - 0.5000); dir.x /= aspectRatio; vec4 center = texture(uTexture, uv); float center_weight = getExponentialWeight(0); color += center * center_weight; total_weight += center_weight;radius *= 0.7200; for (int i = 1; i <= 8; i++) { float weight = getExponentialWeight(i); float offset = mix(0.015, 0.025, radius) * float(i)/8.; vec4 sample1 = texture(uTexture, uv + offset * dir); vec4 sample2 = texture(uTexture, uv - offset * dir); color += (sample1 + sample2) * weight; total_weight += 2.0 * weight; }return color / total_weight; }vec4 thresholdPass(vec4 color) { color.rgb = pow(color.rgb, vec3(1.0/2.2)); color.rgb = 1.2 * (color.rgb - 0.5) + 0.5; vec4 bloom = color * smoothstep(0.5000 - 0.1, 0.5000, luma(color)); return vec4(bloom.rgb, color.a); }vec4 blurCombinePass(vec2 uv, bool vertical, float radius, float intensity, bool diamond) { vec4 blurred = blur(uv, vertical, radius, diamond); return (thresholdPass(texture(uBgTexture, uv)) * 0.5 + blurred * intensity); }vec4 getColor(vec4 color) { return blurCombinePass(vTextureCoord, true, 40., 1.25, true); }void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv); fragColor = getColor(color); }","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture;uniform vec2 uResolution;out vec4 fragColor;float getExponentialWeight(int index) { switch(index) { case 0: return 1.0000000000; case 1: return 0.7165313106; case 2: return 0.5134171190; case 3: return 0.3678794412; case 4: return 0.2636050919; case 5: return 0.1888756057; case 6: return 0.1353352832; case 7: return 0.0969670595; case 8: return 0.0694877157; default: return 0.0; } }vec4 blur(vec2 uv, bool vertical, float radius, bool diamond) { vec4 color = vec4(0.0); float total_weight = 0.0; float aspectRatio = uResolution.x/uResolution.y;vec2 dir; if (diamond) { dir = vertical ? vec2(1, 1) : vec2(1, -1); } else { dir = vertical ? vec2(0, 1) : vec2(1, 0); } dir *= vec2(0.5000, 1. - 0.5000); dir.x /= aspectRatio; vec4 center = texture(uTexture, uv); float center_weight = getExponentialWeight(0); color += center * center_weight; total_weight += center_weight;radius *= 0.7200; for (int i = 1; i <= 8; i++) { float weight = getExponentialWeight(i); float offset = mix(0.015, 0.025, radius) * float(i)/8.; vec4 sample1 = texture(uTexture, uv + offset * dir); vec4 sample2 = texture(uTexture, uv - offset * dir); color += (sample1 + sample2) * weight; total_weight += 2.0 * weight; }return color / total_weight; }vec4 blurPass(vec2 uv, bool vertical, float radius, float intensity, bool diamond) { return blur(uv, vertical, radius, diamond); }vec4 getColor(vec4 color) { return blurPass(vTextureCoord, false, 15., 1.1, true); }void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv); fragColor = getColor(color); }","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform sampler2D uBgTexture;uniform vec2 uResolution;out vec4 fragColor;float luma(vec4 color) { return dot(color.rgb, vec3(0.299, 0.587, 0.114)); }float getExponentialWeight(int index) { switch(index) { case 0: return 1.0000000000; case 1: return 0.7165313106; case 2: return 0.5134171190; case 3: return 0.3678794412; case 4: return 0.2636050919; case 5: return 0.1888756057; case 6: return 0.1353352832; case 7: return 0.0969670595; case 8: return 0.0694877157; default: return 0.0; } }vec4 blur(vec2 uv, bool vertical, float radius, bool diamond) { vec4 color = vec4(0.0); float total_weight = 0.0; float aspectRatio = uResolution.x/uResolution.y;vec2 dir; if (diamond) { dir = vertical ? vec2(1, 1) : vec2(1, -1); } else { dir = vertical ? vec2(0, 1) : vec2(1, 0); } dir *= vec2(0.5000, 1. - 0.5000); dir.x /= aspectRatio; vec4 center = texture(uTexture, uv); float center_weight = getExponentialWeight(0); color += center * center_weight; total_weight += center_weight;radius *= 0.7200; for (int i = 1; i <= 8; i++) { float weight = getExponentialWeight(i); float offset = mix(0.015, 0.025, radius) * float(i)/8.; vec4 sample1 = texture(uTexture, uv + offset * dir); vec4 sample2 = texture(uTexture, uv - offset * dir); color += (sample1 + sample2) * weight; total_weight += 2.0 * weight; }return color / total_weight; }vec4 thresholdPass(vec4 color) { color.rgb = pow(color.rgb, vec3(1.0/2.2)); color.rgb = 1.2 * (color.rgb - 0.5) + 0.5; vec4 bloom = color * smoothstep(0.5000 - 0.1, 0.5000, luma(color)); return vec4(bloom.rgb, color.a); }vec4 blurCombinePass(vec2 uv, bool vertical, float radius, float intensity, bool diamond) { vec4 blurred = blur(uv, vertical, radius, diamond); return (thresholdPass(texture(uBgTexture, uv)) * 0.5 + blurred * intensity); }vec4 getColor(vec4 color) { return blurCombinePass(vTextureCoord, true, 15., 1.1, true); }void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv); fragColor = getColor(color); }","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture;uniform vec2 uResolution;out vec4 fragColor;float getExponentialWeight(int index) { switch(index) { case 0: return 1.0000000000; case 1: return 0.7165313106; case 2: return 0.5134171190; case 3: return 0.3678794412; case 4: return 0.2636050919; case 5: return 0.1888756057; case 6: return 0.1353352832; case 7: return 0.0969670595; case 8: return 0.0694877157; default: return 0.0; } }vec4 blur(vec2 uv, bool vertical, float radius, bool diamond) { vec4 color = vec4(0.0); float total_weight = 0.0; float aspectRatio = uResolution.x/uResolution.y;vec2 dir; if (diamond) { dir = vertical ? vec2(1, 1) : vec2(1, -1); } else { dir = vertical ? vec2(0, 1) : vec2(1, 0); } dir *= vec2(0.5000, 1. - 0.5000); dir.x /= aspectRatio; vec4 center = texture(uTexture, uv); float center_weight = getExponentialWeight(0); color += center * center_weight; total_weight += center_weight;radius *= 0.7200; for (int i = 1; i <= 8; i++) { float weight = getExponentialWeight(i); float offset = mix(0.015, 0.025, radius) * float(i)/8.; vec4 sample1 = texture(uTexture, uv + offset * dir); vec4 sample2 = texture(uTexture, uv - offset * dir); color += (sample1 + sample2) * weight; total_weight += 2.0 * weight; }return color / total_weight; }vec4 blurPass(vec2 uv, bool vertical, float radius, float intensity, bool diamond) { return blur(uv, vertical, radius, diamond); }vec4 getColor(vec4 color) { return blurPass(vTextureCoord, false, 7.5, 1., false); }void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv); fragColor = getColor(color); }","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture;uniform vec2 uResolution;out vec4 fragColor;float getExponentialWeight(int index) { switch(index) { case 0: return 1.0000000000; case 1: return 0.7165313106; case 2: return 0.5134171190; case 3: return 0.3678794412; case 4: return 0.2636050919; case 5: return 0.1888756057; case 6: return 0.1353352832; case 7: return 0.0969670595; case 8: return 0.0694877157; default: return 0.0; } }vec4 blur(vec2 uv, bool vertical, float radius, bool diamond) { vec4 color = vec4(0.0); float total_weight = 0.0; float aspectRatio = uResolution.x/uResolution.y;vec2 dir; if (diamond) { dir = vertical ? vec2(1, 1) : vec2(1, -1); } else { dir = vertical ? vec2(0, 1) : vec2(1, 0); } dir *= vec2(0.5000, 1. - 0.5000); dir.x /= aspectRatio; vec4 center = texture(uTexture, uv); float center_weight = getExponentialWeight(0); color += center * center_weight; total_weight += center_weight;radius *= 0.7200; for (int i = 1; i <= 8; i++) { float weight = getExponentialWeight(i); float offset = mix(0.015, 0.025, radius) * float(i)/8.; vec4 sample1 = texture(uTexture, uv + offset * dir); vec4 sample2 = texture(uTexture, uv - offset * dir); color += (sample1 + sample2) * weight; total_weight += 2.0 * weight; }return color / total_weight; }vec4 blurPass(vec2 uv, bool vertical, float radius, float intensity, bool diamond) { return blur(uv, vertical, radius, diamond); }vec4 getColor(vec4 color) { return blurPass(vTextureCoord, true, 7.5, 1., false); }void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv); fragColor = getColor(color); }","#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform sampler2D uBgTexture;uint fibonacciHash(uint x) { const uint FIB_HASH = 2654435769u; uint hash = x * FIB_HASH; hash ^= hash >> 16; hash *= 0x85ebca6bu; hash ^= hash >> 13; hash *= 0xc2b2ae35u; hash ^= hash >> 16; return hash; }float randFibo(vec2 xy) { uint x_bits = floatBitsToUint(xy.x); uint y_bits = floatBitsToUint(xy.y); uint y_hash = fibonacciHash(y_bits); uint x_xor_y = x_bits ^ y_hash; uint final_hash = fibonacciHash(x_xor_y); return float(final_hash) / float(0xffffffffu); }out vec4 fragColor;float luma(vec4 color) { return dot(color.rgb, vec3(0.299, 0.587, 0.114)); }vec4 finalPass(vec4 bloomColor) { float dither = (randFibo(gl_FragCoord.xy) - 0.5) / 255.0; bloomColor.rgb *= vec3(1, 1, 1); bloomColor.rgb += dither; bloomColor.a = luma(bloomColor); vec4 sceneColor = texture(uBgTexture, vTextureCoord); vec4 finalColor = mix(sceneColor, sceneColor + bloomColor, 0.2400 * 1.75); return finalColor; }vec4 getColor(vec4 color) { return finalPass(color); }void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv); fragColor = getColor(color); }"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"downSample":0.5,"depth":false,"uniforms":{},"isBackground":false,"passes":[{"prop":"pass","value":1,"downSample":0.25},{"prop":"pass","value":2,"downSample":0.25,"includeBg":true},{"prop":"pass","value":3,"downSample":0.25},{"prop":"pass","value":4,"downSample":0.25,"includeBg":true},{"prop":"pass","value":5,"downSample":0.5},{"prop":"pass","value":6,"downSample":0.5,"includeBg":true},{"prop":"pass","value":7,"downSample":1,"includeBg":true}]},"id":"effect11"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"chromab","usesPingPong":false,"speed":0.25,"trackMouse":0,"mouseMomentum":0,"texture":false,"animating":false,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision highp float;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform float uTime;uniform vec2 uMousePos;out vec4 fragColor; const float PI = 3.1415926;void main() { vec2 uv = vTextureCoord; vec2 pos = vec2(0.1227741330834114, 0.5805998125585754) + mix(vec2(0), (uMousePos-0.5), 0.0000); float angle = ((0.0000 + uTime * 0.05) * 360.0) * PI / 180.0; vec2 rotation = vec2(sin(angle), cos(angle)); vec2 aberrated;aberrated = 0.2200 * rotation * 0.03 * mix(1.0, distance(uv, pos) * (1.0 + 0.0000), 0.0000); vec4 left = vec4(0); vec4 right = vec4(0); vec4 center = vec4(0);float steps = max(2., min(float(6), 24.)); float invSteps = 1.0 / (steps + 1.0);for (float i = 0.0; i <= steps; i++) { vec2 offset = aberrated * (i * invSteps); left += texture(uTexture, uv - offset) * invSteps; right += texture(uTexture, uv + offset) * invSteps; } for (float i = 0.0; i <= steps; i++) { vec2 offset = aberrated * ((i / steps) - 0.5); center += texture(uTexture, uv + offset) * invSteps; }vec4 color = texture(uTexture, uv); if(0 == 0) { color.r = left.r; color.g = mix(color.g, center.g, float(1)); color.b = right.b; } else if(0 == 1) { color.r = mix(color.r, center.r, float(1)); color.g = left.g; color.b = right.b; } else if(0 == 2) { color.r = right.r; color.g = left.g; color.b = mix(color.b, center.b, float(1)); }color.a = max(max(left.a, center.a), right.a); fragColor = color;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"depth":false,"uniforms":{},"isBackground":false},"id":"effect12"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"grain","usesPingPong":false,"speed":0.5,"texture":false,"animating":true,"mouseMomentum":0,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision highp float; precision highp int;in vec3 vVertexPosition; in vec2 vTextureCoord;uniform sampler2D uTexture; uniform float uTime; uniform vec2 uResolution; vec3 blend (int blendMode, vec3 src, vec3 dst) { return vec3((dst.x <= 0.5) ? (2.0 * src.x * dst.x) : (1.0 - 2.0 * (1.0 - dst.x) * (1.0 - src.x)), (dst.y <= 0.5) ? (2.0 * src.y * dst.y) : (1.0 - 2.0 * (1.0 - dst.y) * (1.0 - src.y)), (dst.z <= 0.5) ? (2.0 * src.z * dst.z) : (1.0 - 2.0 * (1.0 - dst.z) * (1.0 - src.z))); } uint fibonacciHash(uint x) { const uint FIB_HASH = 2654435769u; uint hash = x * FIB_HASH; hash ^= hash >> 16; hash *= 0x85ebca6bu; hash ^= hash >> 13; hash *= 0xc2b2ae35u; hash ^= hash >> 16; return hash; }float randFibo(vec2 xy) { uint x_bits = floatBitsToUint(xy.x); uint y_bits = floatBitsToUint(xy.y); uint y_hash = fibonacciHash(y_bits); uint x_xor_y = x_bits ^ y_hash; uint final_hash = fibonacciHash(x_xor_y); return float(final_hash) / float(0xffffffffu); }out vec4 fragColor;void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv);if(color.a == 0.) { fragColor = vec4(0); return; }vec2 st = uv; vec3 grainRGB = vec3(0);st *= uResolution;float delta = fract((floor(uTime)/20.));if(0 == 1) { grainRGB = vec3( randFibo(st + vec2(1, 2) + delta), randFibo(st + vec2(2, 3) + delta), randFibo(st + vec2(3, 4) + delta) ); } else { grainRGB = vec3(randFibo(st + vec2(delta))); } color.rgb = mix(color.rgb, blend(5, grainRGB, color.rgb), 0.1500); fragColor = color;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"depth":false,"uniforms":{},"isBackground":false},"id":"effect13"},{"breakpoints":[],"visible":true,"aspectRatio":1,"userDownsample":1,"layerType":"effect","type":"vignette","usesPingPong":false,"trackMouse":0,"mouseMomentum":0,"texture":false,"animating":false,"isMask":0,"compiledFragmentShaders":["#version 300 es\nprecision highp float; in vec3 vVertexPosition; in vec2 vTextureCoord; uniform sampler2D uTexture; uniform vec2 uResolution;out vec4 fragColor; mat2 rot(float a) { return mat2(cos(a),-sin(a),sin(a),cos(a)); } void main() { vec2 uv = vTextureCoord; vec4 color = texture(uTexture, uv); float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114)); float displacement = (luma - 0.5) * 0.0000 * 0.5; vec2 aspectRatio = vec2(uResolution.x/uResolution.y, 1.0); vec2 skew = vec2(0.5000, 1.0 - 0.5000); float halfRadius = 0.5000 * 0.5; float innerEdge = halfRadius - 1.0000 * halfRadius * 0.5; float outerEdge = halfRadius + 1.0000 * halfRadius * 0.5; vec2 pos = vec2(0.5006944444444444, 0.5); const float TWO_PI = 6.28318530718; vec2 scaledUV = uv * aspectRatio * rot(0.0000 * TWO_PI) * skew; vec2 scaledPos = pos * aspectRatio * rot(0.0000 * TWO_PI) * skew; float radius = distance(scaledUV, scaledPos); float falloff = smoothstep(innerEdge + displacement, outerEdge + displacement, radius); vec3 finalColor;finalColor = mix(color.rgb, mix(color.rgb, vec3(0, 0, 0), 1.0000), falloff); color = mix(color * (1.-falloff), vec4(finalColor * color.a, color.a), 1.0000); fragColor = color;}"],"compiledVertexShaders":["#version 300 es\nprecision mediump float;in vec3 aVertexPosition; in vec2 aTextureCoord;uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uTextureMatrix;out vec2 vTextureCoord; out vec3 vVertexPosition;void main() { gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); vTextureCoord = (uTextureMatrix * vec4(aTextureCoord, 0.0, 1.0)).xy; }"],"data":{"depth":false,"uniforms":{},"isBackground":false},"id":"effect14"}],"options":{"name":"Copy of zero (8)","fps":60,"dpi":1.5,"scale":1,"includeLogo":false,"isProduction":false},"version":"1.4.29","id":"u6y1wqsDX7oNKKlkVPpN"} No newline at end of file | |||
There was a problem hiding this comment.
🧹 Nitpick (assertive)
1 MB JSON shader payload—consider async loading or CDN caching
Shipping this monster inline will inflate first paint and possibly break CSP with the external https://assets.unicorn.studio/… texture.
Recommend hosting the JSON & texture on your own CDN and lazy-loading after the main UI is interactive.
🤖 Prompt for AI Agents
In apps/mail/public/mesh.json at line 1, the entire large JSON shader payload is
included inline, which can significantly slow down the initial page load and may
cause Content Security Policy (CSP) issues due to external texture URLs. To fix
this, move the JSON file and its associated textures to your own CDN or static
asset hosting, then implement lazy loading or asynchronous fetching of this JSON
after the main UI has loaded and become interactive. This will reduce the
initial payload size and improve performance and security compliance.
| </Button> | ||
| </a> | ||
| </div> | ||
| <main className="relative flex flex-col overflow-x-hidden bg-[#000000] px-2 flex-1"> |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Use semantic color utility instead of hex value
Same issue as before. We need consistency across the codebase like we need reusable rockets.
- <main className="relative flex flex-col overflow-x-hidden bg-[#000000] px-2 flex-1">
+ <main className="relative flex flex-col overflow-x-hidden bg-black px-2 flex-1">📝 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.
| <main className="relative flex flex-col overflow-x-hidden bg-[#000000] px-2 flex-1"> | |
| <main className="relative flex flex-col overflow-x-hidden bg-black px-2 flex-1"> |
🤖 Prompt for AI Agents
In apps/mail/app/(full-width)/about.tsx at line 14, replace the hardcoded hex
color value #000000 in the className with the appropriate semantic color utility
class defined in the project's design system or tailwind config to maintain
consistency and reusability of colors across the codebase.
| <h2 className="text-3xl font-bold text-white mb-6 text-center">Our Team</h2> | ||
|
|
||
| <div className="text-center mb-8"> | ||
| <p className="text-white/70 text-lg mb-6"> | ||
| Zero is a team with one mission: bring agents to email. | ||
| </p> | ||
|
|
||
| <Button | ||
| className="rounded-[10px] px-4 py-1 h-9 bg-white text-[14px] font-medium leading-[1.43] text-[#262626] hover:bg-white/90 transition-colors" | ||
| onClick={() => window.location.href = '/team'} | ||
| > | ||
| Meet the rest of the team | ||
| </Button> | ||
| </div> | ||
|
|
||
|
|
||
| <div className="aspect-[16/9] relative overflow-hidden bg-white/5 rounded-xl border max-w-2xl mx-auto"> | ||
| <img | ||
| src="/founders.jpg" | ||
| alt="Adam and Nizar, Zero founders" | ||
| className="w-full h-full object-cover" | ||
| /> | ||
| </div> | ||
| <p className="text-xs text-white/50 text-center mt-3"> | ||
| Adam and Nizar at Y Combinator Demo Day | ||
| </p> | ||
| </section> */} | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Remove commented code
Dead code is like fossil fuels - we need to move on. If this isn't needed, delete it.
🤖 Prompt for AI Agents
In apps/mail/app/(full-width)/about.tsx between lines 59 and 86, there is a
large block of commented-out code representing a team section. Since this code
is not currently needed and is considered dead code, remove the entire commented
section to clean up the file and improve maintainability.
| if (isInLastRow && lastRowCount === 2) { | ||
| const positionInLastRow = index - (totalInvestors - lastRowCount); | ||
| if (positionInLastRow === 0) gridPositionClass = 'lg:col-start-2'; | ||
| if (positionInLastRow === 1) gridPositionClass = 'lg:col-start-3'; | ||
| } | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Add explanatory comment for grid centering logic
This grid positioning wizardry needs documentation. Future developers shouldn't need to reverse-engineer this like it's alien technology.
const totalInvestors = investors.length;
const investorsPerRow = 4;
const lastRowCount = totalInvestors % investorsPerRow;
const isInLastRow = index >= totalInvestors - lastRowCount;
+ // Center the last row if it has exactly 2 investors by adjusting grid column start positions
let gridPositionClass = '';
if (isInLastRow && lastRowCount === 2) {
const positionInLastRow = index - (totalInvestors - lastRowCount);
if (positionInLastRow === 0) gridPositionClass = 'lg:col-start-2';
if (positionInLastRow === 1) gridPositionClass = 'lg:col-start-3';
}📝 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.
| let gridPositionClass = ''; | |
| if (isInLastRow && lastRowCount === 2) { | |
| const positionInLastRow = index - (totalInvestors - lastRowCount); | |
| if (positionInLastRow === 0) gridPositionClass = 'lg:col-start-2'; | |
| if (positionInLastRow === 1) gridPositionClass = 'lg:col-start-3'; | |
| } | |
| // Center the last row if it has exactly 2 investors by adjusting grid column start positions | |
| let gridPositionClass = ''; | |
| if (isInLastRow && lastRowCount === 2) { | |
| const positionInLastRow = index - (totalInvestors - lastRowCount); | |
| if (positionInLastRow === 0) gridPositionClass = 'lg:col-start-2'; | |
| if (positionInLastRow === 1) gridPositionClass = 'lg:col-start-3'; | |
| } |
🤖 Prompt for AI Agents
In apps/mail/app/(full-width)/about.tsx around lines 105 to 110, add a clear
explanatory comment above the grid positioning logic that describes why and how
the gridPositionClass is set for centering the last row when it has exactly two
items. Explain the calculation of positionInLastRow and how it determines the
column start classes to center the items in the grid layout. This will help
future developers understand the purpose and mechanism of this code without
needing to reverse-engineer it.
apps/mail/app/(full-width)/about.tsx
Outdated
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Remove trailing empty lines
These empty lines are like unnecessary meetings - they waste space and serve no purpose.
];
-
-
📝 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.
| ]; |
🤖 Prompt for AI Agents
In apps/mail/app/(full-width)/about.tsx around lines 169 to 170, remove the
trailing empty lines at the end of the file to eliminate unnecessary whitespace
and keep the code clean.
| const [showAllContributors, setShowAllContributors] = useState(false); | ||
| const [isLoading, setIsLoading] = useState(true); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const REPOSITORY = 'Mail-0/Zero'; |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Hardcoded repository like a fixed trajectory. Make it configurable.
The repository should be an environment variable for flexibility.
-const REPOSITORY = 'Mail-0/Zero';
+const REPOSITORY = process.env.NEXT_PUBLIC_GITHUB_REPOSITORY || 'Mail-0/Zero';📝 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 REPOSITORY = 'Mail-0/Zero'; | |
| - const REPOSITORY = 'Mail-0/Zero'; | |
| + const REPOSITORY = process.env.NEXT_PUBLIC_GITHUB_REPOSITORY || 'Mail-0/Zero'; |
🤖 Prompt for AI Agents
In apps/mail/app/(full-width)/contributors.tsx at line 58, the repository name
is hardcoded as 'Mail-0/Zero'. Replace this hardcoded string with a reference to
an environment variable to make the repository configurable. Use a suitable
environment variable access method (e.g., process.env.REPOSITORY) and ensure to
provide a fallback or handle the case where the environment variable is not set.
| <a | ||
| className="flex h-full w-full select-none flex-col justify-end rounded-lg p-[25px] no-underline outline-none" | ||
| style={{ | ||
| backgroundImage: "url('/couple.jpeg')", | ||
| backgroundSize: "cover", | ||
| backgroundPosition: "center", | ||
| backgroundRepeat: "no-repeat" | ||
| }} | ||
| href="/" | ||
| > | ||
|
|
||
| </a> |
There was a problem hiding this comment.
Add screen reader accessible content to image links
These empty links are like sending a rocket to space without telemetry - nobody knows what's happening. Add aria-labels or screen reader text.
<a
className="flex h-full w-full select-none flex-col justify-end rounded-lg p-[25px] no-underline outline-none"
style={{
backgroundImage: "url('/couple.jpeg')",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat"
}}
href="/"
+ aria-label="Navigate to home page"
>
-
+ <span className="sr-only">Navigate to home page</span>
</a>Apply similar fixes to the other empty links with background images.
Also applies to: 85-96, 177-188
🧰 Tools
🪛 Biome (2.1.2)
[error] 45-56: Provide screen reader accessible content when using a elements.
All links on a page should have content that is accessible to screen readers.
Accessible content refers to digital content that is designed and structured in a way that makes it easy for people with disabilities to access, understand, and interact with using assistive technologies.
Follow these links for more information,
WCAG 2.4.4
WCAG 4.1.2
(lint/a11y/useAnchorContent)
🤖 Prompt for AI Agents
In apps/mail/components/ui/navigation-menu.tsx at lines 45 to 56, the anchor tag
with a background image is empty and lacks accessible content, making it
invisible to screen readers. Add an aria-label attribute describing the link's
purpose or include visually hidden text inside the anchor to provide screen
reader accessible content. Apply the same fix to the similar empty links with
background images at lines 85-96 and 177-188.
| function parseFrontmatter(content: string): { metadata: BlogPostMetadata; content: string } { | ||
| const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); | ||
|
|
||
| if (!frontmatterMatch) { | ||
| throw new Error('No frontmatter found in MDX file'); | ||
| } | ||
|
|
||
| const [, frontmatterStr, markdownContent] = frontmatterMatch; | ||
| const metadata: any = {}; | ||
|
|
||
| // Parse YAML-like frontmatter | ||
| frontmatterStr.split('\n').forEach(line => { | ||
| const colonIndex = line.indexOf(':'); | ||
| if (colonIndex > 0) { | ||
| const key = line.substring(0, colonIndex).trim(); | ||
| let value = line.substring(colonIndex + 1).trim(); | ||
|
|
||
| // Remove quotes | ||
| value = value.replace(/^["']|["']$/g, ''); | ||
|
|
||
| // Handle arrays (for authorIds) | ||
| if (value.startsWith('[') && value.endsWith(']')) { | ||
| const arrayContent = value.slice(1, -1); | ||
| if (arrayContent) { | ||
| metadata[key] = arrayContent.split(',').map(item => item.trim().replace(/^["']|["']$/g, '')); | ||
| return; // Skip the normal assignment below | ||
| } else { | ||
| metadata[key] = []; | ||
| return; // Skip the normal assignment below | ||
| } | ||
| } | ||
|
|
||
| metadata[key] = value; | ||
| } | ||
| }); | ||
|
|
||
| return { | ||
| metadata: metadata as BlogPostMetadata, | ||
| content: markdownContent.trim() | ||
| }; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Replace manual YAML parsing with a proper parser
Rolling your own YAML parser is like building your own rocket engine when Merlin already exists. This implementation doesn't handle edge cases like multiline values, special characters, or nested structures.
Use a battle-tested YAML parser:
import { parse } from 'yaml';
function parseFrontmatter(content: string): { metadata: BlogPostMetadata; content: string } {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!frontmatterMatch) {
throw new Error('No frontmatter found in MDX file');
}
const [, frontmatterStr, markdownContent] = frontmatterMatch;
const metadata = parse(frontmatterStr) as BlogPostMetadata;
return {
metadata,
content: markdownContent.trim()
};
}🤖 Prompt for AI Agents
In apps/mail/lib/mdx-utils.ts between lines 30 and 70, replace the manual YAML
frontmatter parsing logic with a proper YAML parser like the 'yaml' package.
Import the 'parse' function from 'yaml' and use it to parse the frontmatter
string instead of splitting and manually processing lines. This will handle
multiline values, special characters, and nested structures correctly and
simplify the code.
| const { readFileSync, readdirSync } = await import('fs'); | ||
| const { join } = await import('path'); | ||
|
|
||
| const postsDirectory = join(process.cwd(), 'apps/mail/blog/posts'); |
There was a problem hiding this comment.
Fix incorrect blog posts directory path
According to the AI summary, blog posts are stored in apps/mail/public/blog/posts/, not apps/mail/blog/posts. This is like aiming for Mars but pointing at Venus.
- const postsDirectory = join(process.cwd(), 'apps/mail/blog/posts');
+ const postsDirectory = join(process.cwd(), 'apps/mail/public/blog/posts');📝 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 postsDirectory = join(process.cwd(), 'apps/mail/blog/posts'); | |
| const postsDirectory = join(process.cwd(), 'apps/mail/public/blog/posts'); |
🤖 Prompt for AI Agents
In apps/mail/lib/mdx-utils.ts at line 86, the path to the blog posts directory
is incorrect; it currently points to 'apps/mail/blog/posts' but should point to
'apps/mail/public/blog/posts'. Update the join function argument to reflect the
correct directory path by inserting 'public' in the path string.
| const { readFileSync } = await import('fs'); | ||
| const { join } = await import('path'); | ||
|
|
||
| const filePath = join(process.cwd(), 'apps/mail/blog/posts', `${slug}.mdx`); |
There was a problem hiding this comment.
Critical: Path traversal vulnerability and incorrect directory
Two issues here that need immediate attention - it's like having both a hull breach AND wrong navigation coordinates.
- const filePath = join(process.cwd(), 'apps/mail/blog/posts', `${slug}.mdx`);
+ // Sanitize slug to prevent path traversal
+ const sanitizedSlug = slug.replace(/[^a-zA-Z0-9-]/g, '');
+ const filePath = join(process.cwd(), 'apps/mail/public/blog/posts', `${sanitizedSlug}.mdx`);📝 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 filePath = join(process.cwd(), 'apps/mail/blog/posts', `${slug}.mdx`); | |
| // Sanitize slug to prevent path traversal | |
| const sanitizedSlug = slug.replace(/[^a-zA-Z0-9-]/g, ''); | |
| const filePath = join(process.cwd(), 'apps/mail/public/blog/posts', `${sanitizedSlug}.mdx`); |
🤖 Prompt for AI Agents
In apps/mail/lib/mdx-utils.ts at line 124, the construction of filePath using
join with process.cwd() and user-provided slug is vulnerable to path traversal
attacks and points to an incorrect directory. To fix this, sanitize and validate
the slug input to prevent directory traversal (e.g., disallow '..' or absolute
paths), and ensure the base directory is correctly set to the intended safe
folder for blog posts. Use path resolution methods to enforce that the final
resolved path remains within the allowed directory.
| // In a real app, you might have an API endpoint that lists all available posts | ||
| const knownSlugs = [ | ||
| 'a-faster-zero', | ||
| 'zero-modernized-email', | ||
| 'rebranding-zero-email', | ||
| // Add more as you create them | ||
| ]; |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
🛠️ Refactor suggestion
Hardcoded slugs won't scale - need dynamic discovery
This is like manually updating Tesla's software in each car instead of OTA updates. We need a scalable solution.
Create an API endpoint that returns available blog posts dynamically, or generate a manifest file during build time. Want me to implement a proper blog post discovery system?
🤖 Prompt for AI Agents
In apps/mail/lib/mdx-utils.ts around lines 169 to 175, the hardcoded list of
knownSlugs is not scalable. Replace this static array with a dynamic solution by
either creating an API endpoint that fetches available blog post slugs at
runtime or generating a manifest file during the build process that lists all
blog posts. This will automate slug discovery and eliminate manual updates.
READ CAREFULLY THEN REMOVE
Remove bullet points that are not relevant.
PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI.
Description
Please provide a clear description of your changes.
Type of Change
Please delete options that are not relevant.
Areas Affected
Please check all that apply:
Testing Done
Describe the tests you've done:
Security Considerations
For changes involving data or authentication:
Checklist
Additional Notes
Add any other context about the pull request here.
Screenshots/Recordings
Add screenshots or recordings here if applicable.
By submitting this pull request, I confirm that my contribution is made under the terms of the project's license.
Summary by cubic
Added a new marketing site for Zero with About, Team, Contributors, Pricing, Privacy, Terms, and Blog pages, including new layouts, navigation, and team data. Improved the login and signup flows, updated UI components, and enhanced styling and translations.
New Features
Refactors
Summary by CodeRabbit
New Features
UI/UX Improvements
Localization
Chores
Documentation
Bug Fixes