diff --git a/next.config.mjs b/next.config.mjs index 48f03d4..431d026 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,17 +3,19 @@ const nextConfig = { images: { remotePatterns: [ { - protocol: 'https', - hostname: 'raw.githubusercontent.com', + protocol: "https", + hostname: "raw.githubusercontent.com", }, ], }, experimental: { serverActions: { // edit: updated to new key. Was previously `allowedForwardedHosts` - allowedOrigins: ['localhost:3000', 'get-zen.vercel.app'], + allowedOrigins: ["localhost:3000", "get-zen.vercel.app"], }, - + }, + compiler: { + styledComponents: true, }, }; diff --git a/package-lock.json b/package-lock.json index 9a79331..845ad22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "clsx": "^2.1.1", "cobe": "^0.6.3", "dotenv": "^16.4.5", + "feed": "^4.2.2", "framer-motion": "^11.3.24", "lucide-react": "^0.400.0", "next": "14.2.4", @@ -7780,6 +7781,18 @@ "bser": "2.1.1" } }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -13396,6 +13409,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -15453,6 +15472,18 @@ } } }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 921dc8b..fa53fd6 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "clsx": "^2.1.1", "cobe": "^0.6.3", "dotenv": "^16.4.5", + "feed": "^4.2.2", "framer-motion": "^11.3.24", "lucide-react": "^0.400.0", "next": "14.2.4", diff --git a/src/app/feed.xml/route.ts b/src/app/feed.xml/route.ts new file mode 100644 index 0000000..4d761f1 --- /dev/null +++ b/src/app/feed.xml/route.ts @@ -0,0 +1,110 @@ +import { Feed } from "feed"; +import { releaseNotes } from "@/lib/release-notes"; +import type { ReleaseNote } from "@/lib/release-notes"; + +// Force feed.xml to be cached as static and remain constant for the lifetime of the current site build. +// The supplied releaseNotes array is constant per build, so this will always be the latest release notes. +export const dynamic = "force-static"; + +/** The default number of entries to include in the RSS feed. */ +const RSS_ENTRY_LIMIT = 20; + +/** + * Handles the GET request for the `feed.xml` endpoint. + * @returns The RSS feed for the Zen Browser release notes. + */ +export async function GET() { + // Just in case the release notes array is empty for whatever reason. + const latestDate = releaseNotes.length > 0 + ? formatRssDate(releaseNotes[0].date) + : new Date(); + + const feed = new Feed({ + id: "https://www.zen-browser.app/release-notes", + link: "https://www.zen-browser.app/release-notes", + title: "Zen Browser Release Notes", + description: "Release Notes for the Zen Browser", + language: "en", + favicon: "https://www.zen-browser.app/favicon.ico", + copyright: `Zen Browser © ${new Date().getFullYear()} - Made with ❤️ by the Zen team.`, + updated: latestDate, + }); + + for (const releaseNote of releaseNotes.slice(0, RSS_ENTRY_LIMIT)) { + feed.addItem({ + title: `Release notes for version ${releaseNote.version}`, + id: `https://www.zen-browser.app/release-notes/${releaseNote.version}`, + link: `https://www.zen-browser.app/release-notes/${releaseNote.version}`, + date: formatRssDate(releaseNote.date), + description: releaseNote.extra, + content: formatReleaseNote(releaseNote), + }); + } + + return new Response(feed.rss2(), { + headers: { + 'Content-Type': 'application/xml; charset=utf-8', + }, + }); +} + +/** + * Formats a date string in the format day/month/year. + * + * Note: If release notes change to ISO format, this will need to be updated. + * @param dateStr The date string to format. + * @returns The passed in date string as a Date object. + */ +function formatRssDate(dateStr: string) { + const splitDate = dateStr.split("/"); + if (splitDate.length !== 3) { + throw new Error("Invalid date format"); + } + + const day = Number(splitDate[0]); + const month = Number(splitDate[1]) - 1; + const year = Number(splitDate[2]); + return new Date(year, month, day); +} + +/** + * Formats the release note entry for use as the content of the RSS feed. + * @param releaseNote The release note to format. + * @returns The formatted release note as a HTML string. + */ +function formatReleaseNote(releaseNote: ReleaseNote) { + let content = "

If you encounter any issues, please report them on the issues page. Thanks everyone for your feedback! ❤️

"; + + if (releaseNote.extra) { + content += `

${releaseNote.extra.replace(/(\n)/g, "
")}

` + } + + if (releaseNote.breakingChanges) { + content += `

⚠️ Breaking changes

` + content += `` + } + + if (releaseNote.features) { + content += `

⭐ Features

` + content += `` + } + + if (releaseNote.fixes) { + content += `

✓ Fixes

` + content += `` + } + + return content; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3de6cf4..fd2a59b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,8 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; -import { ThemeProvider } from "@/components/theme-provider" +import { ThemeProvider } from "@/components/theme-provider"; +import StyledComponentsRegistry from "@/lib/styled-components-registry"; const inter = Inter({ subsets: ["latin"] }); @@ -18,6 +19,10 @@ export default function RootLayout({ }>) { return ( + + + + - {children} + {children} diff --git a/src/app/privacy-policy/page.tsx b/src/app/privacy-policy/page.tsx index 7a9af0a..5408dea 100644 --- a/src/app/privacy-policy/page.tsx +++ b/src/app/privacy-policy/page.tsx @@ -52,7 +52,7 @@ Zen Browser offers a "Sync" feature, this is implemented using Mozilla Firefox's # 4. Data Security Although Zen Browser does not collect your data, we are committed to protecting the information that is stored locally on your device and, if you use the Sync feature, the encrypted data stored on Mozilla's servers. We recommend that you use secure passwords, enable device encryption, and regularly update your software to ensure your data remains safe. -* Note that most of the security measures are taken care by mozilla firefox. +* Note that most of the security measures are taken care by Mozilla Firefox. # 5. Your Control ## 5.1. Data Deletion diff --git a/src/app/themes/[theme]/page.tsx b/src/app/themes/[theme]/page.tsx index bb2d5c2..ab32280 100644 --- a/src/app/themes/[theme]/page.tsx +++ b/src/app/themes/[theme]/page.tsx @@ -1,22 +1,45 @@ -"use client"; + import Footer from "@/components/footer"; import { Navigation } from "@/components/navigation"; import ThemePage from "@/components/theme-page"; import { getThemeFromId } from "@/lib/themes"; -import { useParams } from "next/navigation"; +import { Metadata, ResolvingMetadata } from "next"; + +export async function generateMetadata( + { params, searchParams }: any, + parent: ResolvingMetadata +): Promise { + const theme = params.theme + const themeData = await getThemeFromId(theme); + if (!themeData) { + return { + title: "Theme not found", + description: "Theme not found", + }; + } + return { + title: themeData.name, + description: themeData.description, + keywords: [themeData.name, themeData.description], + openGraph: { + title: themeData.name, + description: themeData.description, + images: [ + { + url: themeData.image, + width: 500, + height: 500, + alt: themeData.name, + }, + ], + }, + }; +} export default async function ThemeInfoPage() { - const params = useParams<{ theme: string }>(); - const { theme: themeID } = params; - - const theme = await getThemeFromId(themeID); - if (!theme) { - return
Theme not found
; - } - return (
- +
{/* At the bottom of the page */}
diff --git a/src/components/branding-assets.tsx b/src/components/branding-assets.tsx index fb14914..5100b07 100644 --- a/src/components/branding-assets.tsx +++ b/src/components/branding-assets.tsx @@ -1,3 +1,4 @@ + import { LOGO_COLORS } from "@/lib/logos"; export function BrandingAssets() { @@ -19,6 +20,7 @@ export function BrandingAssets() {
{color} @@ -40,7 +42,7 @@ export function BrandingAssets() {
{color} diff --git a/src/components/download.tsx b/src/components/download.tsx index 2ab7f6e..7398499 100644 --- a/src/components/download.tsx +++ b/src/components/download.tsx @@ -382,7 +382,7 @@ export default function DownloadPage() {

🍏

-

aarch64

+

AArch64

64-bit ARM architecture, for Apple's M Series Chips

clearInterval(interval); }); return ( -
+
-
-
@@ -149,8 +147,6 @@ export default function Features() {
-
-
diff --git a/src/components/theme-card.tsx b/src/components/theme-card.tsx index 988bbce..3e29af1 100644 --- a/src/components/theme-card.tsx +++ b/src/components/theme-card.tsx @@ -3,7 +3,7 @@ import { getThemeAuthorLink, ZenTheme } from "@/lib/themes"; import styled from "styled-components"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";import { Button } from "./ui/button"; -const ThemeCardWrapepr = styled.div` +const ThemeCardWrapper = styled.div` `; export default function ThemeCard({ @@ -12,7 +12,7 @@ export default function ThemeCard({ theme: ZenTheme; }) { return ( - { + { if (event.target instanceof HTMLAnchorElement) return; window.open(`/themes/${theme.id}`, "_self"); }} className="flex flex-col justify-start p-5 rounded-lg shadow-sm bg-muted dark:bg-muted/50 border border-grey-900 dark:border-muted w-full hover:shadow-lg transition duration-300 ease-in-out hover:bg-muted/100 hover:border-blue-500 cursor-pointer select-none "> @@ -35,6 +35,6 @@ export default function ThemeCard({

{theme.description}

- + ); } diff --git a/src/components/theme-page.tsx b/src/components/theme-page.tsx index b8be7fd..bfef366 100644 --- a/src/components/theme-page.tsx +++ b/src/components/theme-page.tsx @@ -1,20 +1,31 @@ +"use client"; import Image from "next/image"; -import { getThemeAuthorLink, getThemeMarkdown, ZenTheme } from "@/lib/themes"; +import { getThemeAuthorLink, getThemeFromId, getThemeMarkdown, ZenTheme } from "@/lib/themes"; import { Button } from "./ui/button"; import { useEffect, useState } from "react"; import Markdown from "react-markdown"; import '../app/privacy-policy/markdown.css'; -import { ChevronLeft, LoaderCircleIcon, LoaderIcon, LoaderPinwheelIcon, MoveLeftIcon } from "lucide-react"; +import { ChevronLeft, LoaderCircleIcon } from "lucide-react"; +import { useParams } from "next/navigation"; -export default function ThemePage({ theme }: { theme: ZenTheme }) { - const [readme, setReadme] = useState(null); - useEffect(() => { - getThemeMarkdown(theme).then(setReadme); - }, [theme]); +export default async function ThemePage() { + const params = useParams<{ theme: string }>(); + const { theme: themeID } = params; + + const theme = await getThemeFromId(themeID); + if (!theme) { + return
Theme not found
; + } + + const readme = await getThemeMarkdown(theme); return (
-
+
+
window.history.back()}> + +

Go back

+
{theme.name}

{theme.name}

{theme.description}

@@ -42,14 +53,10 @@ export default function ThemePage({ theme }: { theme: ZenTheme }) {

You need to have Zen Browser installed to install this theme. Download now!


-
-
window.history.back()}> - -

Go back

-
+
{readme === null ? ( - + ) : ( {`${readme}`} )} diff --git a/src/lib/release-notes.ts b/src/lib/release-notes.ts index eb4ea52..8d0cfaf 100644 --- a/src/lib/release-notes.ts +++ b/src/lib/release-notes.ts @@ -601,6 +601,31 @@ export const releaseNotes: ReleaseNote[] = [ } ] }, + { + version: "1.0.0-a.29", + date: "24/08/2024", + extra: "This release is the twenty-ninth alpha release of the 1.0.0-alpha series.", + features: [ + "Added Spanish translations", + "Added documentation for contributing", + "Added support for multi-tab splitting with shortcuts", + "Fixed sidebar shortcuts" + ], + fixes: [ + { + description: "Text on websites is blurry", + issue: 383 + }, + { + description: "Expanded compact mode triggers too early", + issue: 520 + }, + { + description: "Ampersand in workspace name breaks workspace menu", + issue: 439 + } + ] + } ].reverse(); export function releaseNoteIsAlpha(note: ReleaseNote) { diff --git a/src/lib/styled-components-registry.tsx b/src/lib/styled-components-registry.tsx new file mode 100644 index 0000000..fa12293 --- /dev/null +++ b/src/lib/styled-components-registry.tsx @@ -0,0 +1,29 @@ +"use client"; + +import React, { useState } from "react"; +import { useServerInsertedHTML } from "next/navigation"; +import { ServerStyleSheet, StyleSheetManager } from "styled-components"; + +export default function StyledComponentsRegistry({ + children, +}: { + children: React.ReactNode; +}) { + // Only create stylesheet once with lazy initial state + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()); + + useServerInsertedHTML(() => { + const styles = styledComponentsStyleSheet.getStyleElement(); + styledComponentsStyleSheet.instance.clearTag(); + return <>{styles}; + }); + + if (typeof window !== "undefined") return <>{children}; + + return ( + + {children} + + ); +}