mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: new-release notice and colocate changelog under domain
This commit is contained in:
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const changelog = defineCollection({
|
const changelog = defineCollection({
|
||||||
name: "changelog",
|
name: "changelog",
|
||||||
directory: "content/changelog",
|
directory: "src/lib/changelog/entries",
|
||||||
include: "*.md",
|
include: "*.md",
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
@@ -12,6 +12,7 @@ const changelog = defineCollection({
|
|||||||
published: z.boolean().default(true),
|
published: z.boolean().default(true),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
summary: z.string().optional(),
|
||||||
changes: z.array(
|
changes: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
@@ -25,7 +26,8 @@ const changelog = defineCollection({
|
|||||||
const sorted = [...publishedDocs].sort((a, b) =>
|
const sorted = [...publishedDocs].sort((a, b) =>
|
||||||
b.version.localeCompare(a.version, undefined, { numeric: true }),
|
b.version.localeCompare(a.version, undefined, { numeric: true }),
|
||||||
);
|
);
|
||||||
const isLatest = doc.published !== false && sorted[0]?.version === doc.version;
|
const isLatest =
|
||||||
|
doc.published !== false && sorted[0]?.version === doc.version;
|
||||||
return { ...doc, isLatest };
|
return { ...doc, isLatest };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import Link from "next/link";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { BasePage } from "@/app/base-page";
|
import { BasePage } from "@/app/base-page";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||||
import { getReleaseByVersion, getSortedReleases } from "../utils";
|
import { getReleaseByVersion, getSortedReleases } from "@/lib/changelog/utils";
|
||||||
import {
|
import {
|
||||||
ReleaseArticle,
|
ReleaseArticle,
|
||||||
ReleaseMeta,
|
ReleaseMeta,
|
||||||
ReleaseTitle,
|
ReleaseTitle,
|
||||||
ReleaseDescription,
|
ReleaseDescription,
|
||||||
ReleaseChanges,
|
ReleaseChanges,
|
||||||
} from "../components/release";
|
} from "@/lib/changelog/components/release";
|
||||||
import { CopyMarkdownButton } from "../components/copy-markdown-button";
|
import { CopyMarkdownButton } from "@/lib/changelog/components/copy-markdown-button";
|
||||||
|
|
||||||
type Props = { params: Promise<{ version: string }> };
|
type Props = { params: Promise<{ version: string }> };
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { BasePage } from "@/app/base-page";
|
import { BasePage } from "@/app/base-page";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { type Release as ReleaseType, getSortedReleases } from "./utils";
|
import {
|
||||||
|
type Release as ReleaseType,
|
||||||
|
getSortedReleases,
|
||||||
|
} from "@/lib/changelog/utils";
|
||||||
import {
|
import {
|
||||||
ReleaseArticle,
|
ReleaseArticle,
|
||||||
ReleaseMeta,
|
ReleaseMeta,
|
||||||
ReleaseTitle,
|
ReleaseTitle,
|
||||||
ReleaseDescription,
|
ReleaseDescription,
|
||||||
ReleaseChanges,
|
ReleaseChanges,
|
||||||
} from "./components/release";
|
} from "@/lib/changelog/components/release";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Changelog - OpenCut",
|
title: "Changelog - OpenCut",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { useEditor } from "@/hooks/use-editor";
|
|||||||
import { Cancel01Icon } from "@hugeicons/core-free-icons";
|
import { Cancel01Icon } from "@hugeicons/core-free-icons";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -38,6 +39,7 @@ export default function Editor() {
|
|||||||
</div>
|
</div>
|
||||||
<Onboarding />
|
<Onboarding />
|
||||||
<MigrationDialog />
|
<MigrationDialog />
|
||||||
|
<ChangelogNotification />
|
||||||
</div>
|
</div>
|
||||||
</EditorProvider>
|
</EditorProvider>
|
||||||
</MobileGate>
|
</MobileGate>
|
||||||
@@ -51,9 +53,7 @@ function DegradedRendererBanner() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-accent border-b h-9 flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
<div className="bg-accent border-b h-9 flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
||||||
<span>
|
<span>For the best experience, open OpenCut in Chrome.</span>
|
||||||
For the best experience, open OpenCut in Chrome.
|
|
||||||
</span>
|
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ThemeProvider } from "next-themes";
|
|||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Toaster } from "../components/ui/sonner";
|
import { Toaster } from "../components/ui/sonner";
|
||||||
|
import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
|
||||||
import { TooltipProvider } from "../components/ui/tooltip";
|
import { TooltipProvider } from "../components/ui/tooltip";
|
||||||
import { baseMetaData } from "./metadata";
|
import { baseMetaData } from "./metadata";
|
||||||
import { BotIdClient } from "botid/client";
|
import { BotIdClient } from "botid/client";
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ import { DeleteProjectDialog } from "@/components/editor/dialogs/delete-project-
|
|||||||
import { ProjectInfoDialog } from "@/components/editor/dialogs/project-info-dialog";
|
import { ProjectInfoDialog } from "@/components/editor/dialogs/project-info-dialog";
|
||||||
import { RenameProjectDialog } from "@/components/editor/dialogs/rename-project-dialog";
|
import { RenameProjectDialog } from "@/components/editor/dialogs/rename-project-dialog";
|
||||||
import { cn } from "@/utils/ui";
|
import { cn } from "@/utils/ui";
|
||||||
|
import { ChangelogNotification } from "@/lib/changelog/components/changelog-notification";
|
||||||
const formatProjectDuration = ({
|
const formatProjectDuration = ({
|
||||||
duration,
|
duration,
|
||||||
}: {
|
}: {
|
||||||
@@ -107,6 +107,7 @@ export default function ProjectsPage() {
|
|||||||
<div className="bg-background min-h-screen">
|
<div className="bg-background min-h-screen">
|
||||||
<MigrationDialog />
|
<MigrationDialog />
|
||||||
<StoragePersistenceDialog />
|
<StoragePersistenceDialog />
|
||||||
|
<ChangelogNotification />
|
||||||
<ProjectsHeader />
|
<ProjectsHeader />
|
||||||
<ProjectsToolbar projectIds={projectsToDisplay.map((p) => p.id)} />
|
<ProjectsToolbar projectIds={projectsToDisplay.map((p) => p.id)} />
|
||||||
<main className="mx-auto px-4 pt-2 pb-6 flex flex-col gap-4">
|
<main className="mx-auto px-4 pt-2 pb-6 flex flex-col gap-4">
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Cancel01Icon } from "@hugeicons/core-free-icons";
|
||||||
|
import { HugeiconsIcon } from "@hugeicons/react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { getSortedReleases } from "../utils";
|
||||||
|
import type { Release } from "../utils";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "last-seen-version";
|
||||||
|
|
||||||
|
export function ChangelogNotification() {
|
||||||
|
const [release, setRelease] = useState<Release | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const releases = getSortedReleases();
|
||||||
|
const latest = releases[0];
|
||||||
|
if (!latest) return;
|
||||||
|
|
||||||
|
let storedVersion: string | null = null;
|
||||||
|
try {
|
||||||
|
storedVersion = localStorage.getItem(STORAGE_KEY);
|
||||||
|
} catch {
|
||||||
|
// localStorage unavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// First-time visitor: record the version silently, nothing to announce.
|
||||||
|
if (storedVersion === null) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, latest.version);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already seen this version.
|
||||||
|
if (storedVersion === latest.version) return;
|
||||||
|
|
||||||
|
// New version since last visit: record it and show the notification.
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, latest.version);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
setRelease(latest);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!release) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-5 left-5 z-50 flex w-72 flex-col gap-3 rounded-xl border bg-card p-4 shadow-lg">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-sm font-semibold leading-snug">
|
||||||
|
{release.title}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
v{release.version}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="-mr-1 -mt-1 shrink-0"
|
||||||
|
onClick={() => setRelease(null)}
|
||||||
|
aria-label="Dismiss"
|
||||||
|
>
|
||||||
|
<HugeiconsIcon icon={Cancel01Icon} className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{release.summary && (
|
||||||
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||||
|
{release.summary}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button asChild size="sm">
|
||||||
|
<Link href="/changelog" onClick={() => setRelease(null)}>
|
||||||
|
See full changelog
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ date: "2026-02-23"
|
|||||||
published: true
|
published: true
|
||||||
title: "Editor foundation"
|
title: "Editor foundation"
|
||||||
description: "This first release focuses on making editing faster, clearer, and more reliable for day-to-day use."
|
description: "This first release focuses on making editing faster, clearer, and more reliable for day-to-day use."
|
||||||
|
summary: "Properties panel overhaul, 1,000+ fonts, blend modes, a new color picker, and direct manipulation in the preview."
|
||||||
changes:
|
changes:
|
||||||
- type: new
|
- type: new
|
||||||
text: "Rebuilt the properties panel from scratch. Number inputs now support math expressions and click-drag scrubbing instead of just typing values in."
|
text: "Rebuilt the properties panel from scratch. Number inputs now support math expressions and click-drag scrubbing instead of just typing values in."
|
||||||
@@ -4,6 +4,7 @@ date: "2026-03-01"
|
|||||||
published: true
|
published: true
|
||||||
title: "Motion & effects"
|
title: "Motion & effects"
|
||||||
description: "This release adds the foundation for motion and effects, alongside many improvements & fixes."
|
description: "This release adds the foundation for motion and effects, alongside many improvements & fixes."
|
||||||
|
summary: "Keyframe animation, a new effects system with per-clip blur, and ripple editing mode."
|
||||||
changes:
|
changes:
|
||||||
- type: new
|
- type: new
|
||||||
text: "Keyframe animation system for animating element properties over time."
|
text: "Keyframe animation system for animating element properties over time."
|
||||||
@@ -4,6 +4,7 @@ date: "2026-04-15"
|
|||||||
published: true
|
published: true
|
||||||
title: "Masks, animation & more"
|
title: "Masks, animation & more"
|
||||||
description: "This release adds masks, a graph editor for keyframe curves, volume and speed controls, preview zoom, canvas backgrounds, stickers, and a lot more."
|
description: "This release adds masks, a graph editor for keyframe curves, volume and speed controls, preview zoom, canvas backgrounds, stickers, and a lot more."
|
||||||
|
summary: "Masks, graph editor for curves, volume & speed controls, preview zoom, canvas backgrounds, and stickers."
|
||||||
changes:
|
changes:
|
||||||
- type: new
|
- type: new
|
||||||
text: "Added a brand page with downloadable brand assets. Assets are also accessible directly from the header logo context menu."
|
text: "Added a brand page with downloadable brand assets. Assets are also accessible directly from the header logo context menu."
|
||||||
Reference in New Issue
Block a user