Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"next": "^15.1.8",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.6.0",
"reframe": ".",
"tailwind-merge": "^3.6.0",
"wasm-feature-detect": "^1.8.0"
Expand Down
20 changes: 3 additions & 17 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,12 @@
--error-hover: #991b1b;
}

/* ── High contrast mode tokens ── */
[data-theme="high-contrast"] {
--bg: #000000;
--surface: #000000;
--border: #FFFFFF;

--text: #FFFFFF;
--muted: #FFFFFF;

--accent: #FFFF00;
--accent-hover: #FFFF00;

--error: #FF6666;
--success: #66FF66;

--film-600: #FF6666;
--film-400: #66FF66;
}

/* ── Base styles ── */
html {
scroll-behavior: smooth;
}
body {
background-color: var(--bg);
color: var(--text);
Expand Down
16 changes: 3 additions & 13 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { Bebas_Neue, Syne, DM_Sans } from "next/font/google";
import ErrorBoundary from "@/components/ErrorBoundary";
import "./globals.css";
import { ThemeProvider } from "@/components/ThemeProvider";
import { ThemeToggle } from "@/components/ThemeToggle";
import Header from "@/components/Header";
import ScrollToTop from "@/components/ScrollToTop";
import BrandLogo from "@/components/BrandLogo";

export const metadata: Metadata = {
title: "Reframe — Resize, trim, and export videos in your browser",
Expand Down Expand Up @@ -47,7 +46,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<html lang="en" suppressHydrationWarning>
<head>
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
Expand Down Expand Up @@ -87,16 +86,7 @@ export default function RootLayout({
</a>
<ThemeProvider>
<ErrorBoundary>
<header
role="banner"
className="sticky top-0 z-50 flex items-center justify-between px-6 py-3 border-b border-[var(--border)] bg-[var(--bg)]"
>
<div className="flex items-center gap-2">
<BrandLogo size={24} />
<h1 className="text-lg font-semibold">Reframe</h1>
</div>
<ThemeToggle />
</header>
<Header />
<main id="main-content" tabIndex={-1}>
{children}
</main>
Expand Down
25 changes: 3 additions & 22 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,5 @@
import VideoEditor from "@/components/VideoEditor";
import Footer from "@/components/Footer";
import LandingWrapper from "@/components/landing/LandingWrapper";

export default function Home() {
return (
<>
<a
href="https://github.com/magic-peach/reframe"
target="_blank"
rel="noopener noreferrer"
className="hidden min-[300px]:flex fixed top-4 right-16 z-50 items-center gap-1.5 px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[var(--surface)] text-[10px] font-heading font-semibold uppercase tracking-wider transition-all duration-200 ease-in-out hover:scale-105 hover:shadow-[0_0_10px_rgba(255,255,255,0.15)] hover:bg-white/10"
>
⭐ Star on GitHub
</a>

<main id="main-content" tabIndex={-1}>
<VideoEditor />
</main>

<Footer />
</>
);
export default function Page() {
return <LandingWrapper />;
}

23 changes: 23 additions & 0 deletions src/app/reframe/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import VideoEditor from "@/components/VideoEditor";
import Footer from "@/components/Footer";

export default function ReframePage() {
return (
<>
<a
href="https://github.com/magic-peach/reframe"
target="_blank"
rel="noopener noreferrer"
className="hidden min-[300px]:flex fixed top-4 right-16 z-50 items-center gap-1.5 px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[var(--surface)] text-[10px] font-heading font-semibold uppercase tracking-wider transition-all duration-200 ease-in-out hover:scale-105 hover:shadow-[0_0_10px_rgba(255,255,255,0.15)] hover:bg-white/10"
>
⭐ Star on GitHub
</a>

<main id="main-content" tabIndex={-1}>
<VideoEditor />
</main>

<Footer />
</>
);
}
27 changes: 27 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { usePathname } from "next/navigation";
import BrandLogo from "@/components/BrandLogo";
import { ThemeToggle } from "@/components/ThemeToggle";

export default function Header() {
const pathname = usePathname();

// Hide the default editor header on the landing page
if (pathname === "/") {
return null;
}

return (
<header
role="banner"
className="sticky top-0 z-50 flex items-center justify-between px-6 py-3 border-b border-[var(--border)] bg-[var(--bg)]"
>
<div className="flex items-center gap-2">
<BrandLogo size={24} />
<span className="text-lg font-semibold">Reframe</span>
</div>
<ThemeToggle />
</header>
);
}
36 changes: 12 additions & 24 deletions src/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
type ReactNode,
} from "react";

type Theme = "light" | "dark" | "high-contrast";
type Theme = "light" | "dark";

interface ThemeContextValue {
theme: Theme;
Expand All @@ -27,15 +27,15 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
// we just sync React state here so the toggle button shows the right icon.
useEffect(() => {
try {
const stored = localStorage.getItem("theme") as Theme | null;
if (stored === "light" || stored === "dark" || stored === "high-contrast") {
setThemeState(stored);
} else {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
setThemeState(prefersDark ? "dark" : "light");
}
const stored = localStorage.getItem("theme") as Theme | null;
if (stored === "light" || stored === "dark") {
setThemeState(stored);
} else {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
setThemeState(prefersDark ? "dark" : "light");
}
} catch {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
Expand All @@ -62,14 +62,8 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
} else {
document.documentElement.classList.remove("dark");
}
if (next === "high-contrast") {
document.documentElement.setAttribute(
"data-theme",
"high-contrast"
);
} else {
// Always remove high-contrast data theme attribute
document.documentElement.removeAttribute("data-theme");
}
if (persist) {
localStorage.setItem("theme", next);
}
Expand All @@ -78,13 +72,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
);

const toggleTheme = useCallback(() => {
applyTheme(
theme === "light"
? "dark"
: theme === "dark"
? "high-contrast"
: "light"
);
applyTheme(theme === "light" ? "dark" : "light");
}, [theme, applyTheme]);

const setTheme = useCallback(
Expand Down
22 changes: 5 additions & 17 deletions src/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,10 @@ export function ThemeToggle() {

return (
<button
type="button"
onClick={toggleTheme}
aria-label={
theme === "light"
? "Switch to dark mode"
: theme === "dark"
? "Switch to high contrast mode"
: "Switch to light mode"
}
title={
theme === "light"
? "Switch to dark mode"
: theme === "dark"
? "Switch to high contrast mode"
: "Switch to light mode"
}
type="button"
onClick={toggleTheme}
aria-label={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
title={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
className="
relative flex items-center justify-center
w-9 h-9 rounded-full
Expand Down Expand Up @@ -57,7 +45,7 @@ export function ThemeToggle() {
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
</svg>
) : (
/* Moon icon — shown in dark/high contrast mode */
/* Moon icon — shown in dark mode */
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
Expand Down
90 changes: 90 additions & 0 deletions src/components/landing/CTASection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import Link from "next/link";
import { FiArrowRight, FiGithub, FiShield, FiHeart } from "react-icons/fi";
import BrandLogo from "@/components/BrandLogo";
import Sparkles from "./Sparkles";

export default function CTASection() {
return (
<section className="relative overflow-hidden min-h-screen flex flex-col justify-between pt-24 pb-6">
<Sparkles />
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-blue-500/3 to-indigo-500/5 -z-10" />

{/* CTA */}
<div className="my-auto mx-auto w-full max-w-6xl px-6 md:px-10 text-center">
<h2 className="text-4xl sm:text-5xl font-semibold tracking-tight text-[var(--text)] mb-5 leading-tight">
Ready to edit?
</h2>
<p className="mx-auto max-w-md text-base text-[var(--muted)] mb-10 leading-relaxed">
No signups, no watermarks, no usage limits. Just you and your video.
</p>
<div className="flex flex-wrap items-center justify-center gap-4">
<Link
href="/reframe"
className="inline-flex items-center gap-2 rounded-lg bg-[var(--accent)] px-7 py-3 text-sm text-white shadow-md shadow-blue-500/10 hover:shadow-blue-500/20 hover:bg-[var(--accent-hover)] transition-all hover:-translate-y-px"
>
Open Editor
<FiArrowRight className="h-4 w-4" />
</Link>
<a
href="https://github.com/magic-peach/reframe"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-[var(--border)] bg-[var(--surface)]/60 px-7 py-3 text-sm text-[var(--text)] hover:bg-[var(--surface)] transition-all hover:-translate-y-px"
>
<FiGithub className="h-4 w-4" />
GitHub
</a>
</div>
</div>

{/* Footer */}
<footer className="w-full mt-auto pt-10 pb-4">
<div className="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-12 gap-8 px-6 md:px-10">

<div className="md:col-span-5 space-y-3">
<div className="flex items-center gap-1.5">
<BrandLogo size={20} className="text-film-600" />
<span className="text-sm font-medium tracking-tight text-[var(--text)]">Reframe</span>
</div>
<p className="text-xs text-[var(--muted)] leading-relaxed max-w-xs">
Free, open-source video editor. Resize, trim, and format videos without uploading a single byte.
</p>
<div className="flex items-center gap-1.5 text-xs text-[var(--muted)]">
<FiShield className="h-3.5 w-3.5 text-emerald-500" />
Videos never leave your computer.
</div>
</div>

<div className="md:col-span-3 md:col-start-8 space-y-3">
<p className="text-[10px] uppercase tracking-widest text-[var(--muted)]">Product</p>
<nav className="flex flex-col gap-1.5 text-xs text-[var(--muted)]">
<Link href="/reframe" className="hover:text-[var(--text)] transition-colors">Open Editor</Link>
<a href="#features" onClick={(e) => { e.preventDefault(); document.getElementById("features")?.scrollIntoView({ behavior: "smooth" }); }} className="hover:text-[var(--text)] transition-colors">Features</a>
<a href="#workflow" onClick={(e) => { e.preventDefault(); document.getElementById("workflow")?.scrollIntoView({ behavior: "smooth" }); }} className="hover:text-[var(--text)] transition-colors">How It Works</a>
</nav>
</div>

<div className="md:col-span-2 space-y-3">
<p className="text-[10px] uppercase tracking-widest text-[var(--muted)]">Legal</p>
<nav className="flex flex-col gap-1.5 text-xs text-[var(--muted)]">
<Link href="/privacy" className="hover:text-[var(--text)] transition-colors">Privacy Policy</Link>
<Link href="/contact" className="hover:text-[var(--text)] transition-colors">Contact</Link>
<a href="https://github.com/magic-peach/reframe" target="_blank" rel="noopener noreferrer" className="flex items-center gap-1 hover:text-[var(--text)] transition-colors">
<FiGithub className="h-3.5 w-3.5" /> GitHub
</a>
</nav>
</div>
</div>

<div className="max-w-6xl mx-auto mt-8 pt-4 border-t border-[var(--border)]/30 flex flex-col sm:flex-row justify-between items-center gap-2 text-[10px] text-[var(--muted)] px-6 md:px-10">
<p>&copy; {new Date().getFullYear()} Reframe. MIT License.</p>
<p className="flex items-center gap-1">
Made with <FiHeart className="h-3 w-3 text-rose-500 fill-rose-500" /> for privacy.
</p>
</div>
</footer>
</section>
);
}
Loading
Loading