From ac79dea1f7daa0bf0c898ad35c771dfc62eebbde Mon Sep 17 00:00:00 2001 From: Mauro Balades Date: Thu, 1 Aug 2024 20:15:50 +0200 Subject: [PATCH] Redesigned the website! --- package-lock.json | 10 + package.json | 1 + src/app/globals.css | 44 ++- src/components/download.tsx | 452 +++++++++++++++++++++++------- src/components/features.tsx | 57 ++-- src/components/header.tsx | 170 ++++++----- src/components/ui/blur-fade.tsx | 60 ++++ src/components/ui/border-beam.tsx | 49 ++++ src/components/ui/confetti.tsx | 120 ++++++++ src/components/ui/particles.tsx | 271 ++++++++++++++++++ src/lib/releases.ts | 53 +++- tailwind.config.ts | 169 ++++++----- 12 files changed, 1161 insertions(+), 295 deletions(-) create mode 100644 src/components/ui/blur-fade.tsx create mode 100644 src/components/ui/border-beam.tsx create mode 100644 src/components/ui/confetti.tsx create mode 100644 src/components/ui/particles.tsx diff --git a/package-lock.json b/package-lock.json index 4fbe9be..a7b066f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@vercel/postgres": "^0.9.0", + "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cobe": "^0.6.3", @@ -5465,6 +5466,15 @@ } ] }, + "node_modules/canvas-confetti": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz", + "integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", diff --git a/package.json b/package.json index 908cd58..5a4267e 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@vercel/postgres": "^0.9.0", + "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cobe": "^0.6.3", diff --git a/src/app/globals.css b/src/app/globals.css index 8b663d5..76e5131 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,9 +1,9 @@ @tailwind base; - @tailwind components; - @tailwind utilities; +@tailwind components; +@tailwind utilities; - @layer base { - :root { +@layer base { + :root { --background: 0 0% 100%; --foreground: 0 0% 3.9%; @@ -33,10 +33,23 @@ --ring: 0 0% 3.9%; --radius: 0.5rem; - } - .dark { - --background: 0 0% 3.9%; + /* Custom properties */ + --navigation-height: 3.5rem; + --color-one: #ffbd7a; + + --color-two: #fe8bbb; + --color-three: #9e7aff; + + /* + --color-one: #37ecba; + --color-two: #72afd3; + --color-three: #ff2e63; + */ + } + + .dark { + --background: 0 0% 0%; --foreground: 0 0% 98%; --card: 0 0% 3.9%; @@ -63,14 +76,15 @@ --border: 0 0% 14.9%; --input: 0 0% 14.9%; --ring: 0 0% 83.1%; - } - } + --color-one: #6aa8e2; + } +} - @layer base { - * { +@layer base { + * { @apply border-border; - } - body { + } + body { @apply bg-background text-foreground; - } - } \ No newline at end of file + } +} diff --git a/src/components/download.tsx b/src/components/download.tsx index 56aee33..dea7e16 100644 --- a/src/components/download.tsx +++ b/src/components/download.tsx @@ -1,15 +1,15 @@ "use client"; -import { ny } from "@/lib/utils"; -import GridPattern from "./ui/grid-pattern"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "./ui/select"; -import { Button } from "./ui/button"; -import { Form, FormField, FormItem, FormLabel } from "./ui/form"; -import { useForm } from "react-hook-form"; -import { z } from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod' -import { releases } from "@/lib/releases"; import { addDownload } from "@/lib/db"; +import { useState } from "react"; +import styled, { keyframes } from "styled-components"; +import { ny } from "@/lib/utils"; +import { Checkbox } from "./ui/checkbox"; +import { ChevronLeft } from "lucide-react"; +import { Button } from "./ui/button"; +import Particles from "./ui/particles"; +import confetti from 'canvas-confetti'; +import { releases, releaseTree } from "@/lib/releases"; const BASE_URL = "https://github.com/zen-browser/desktop/releases/latest/download"; @@ -19,12 +19,10 @@ function getDefaultPlatformBasedOnUserAgent() { userAgent = window.navigator.userAgent; } if (userAgent.includes("Win")) { - return "WindowsInstaller"; + return "Windows"; } if (userAgent.includes("Mac")) { - // TODO: - // return "MacOS"; - return ""; + return "MacOS"; } if (userAgent.includes("Linux")) { return "Linux"; @@ -32,110 +30,350 @@ function getDefaultPlatformBasedOnUserAgent() { return ""; } -const formSchema = z.object({ - platform: z.string().nonempty(), -}); +const field_enter = keyframes` + 0% { + opacity: 0; + transform: scale(0.9); + filter: blur(10px); + } + 1% { + max-height: 100%; + } + 100% { + opacity: 1; + transform: scale(1); + filter: blur(0); + } +`; + +const field_exit = keyframes` + from { + display: flex; + opacity: 1; + transform: scale(1); + filter: blur(0); + } + 99% { + opacity: 0; + transform: scale(0.9); + filter: blur(10px); + } + 100% { + display: none; + } +`; + +const FormField = styled.div<{ enter: boolean, out: boolean }>` + overflow: hidden; + max-height: 0; + flex-direction: column; + margin-top: 3rem; + opacity: 0; + width: 100%; + animation: 0.2s ease-in-out forwards ${({ enter, out }) => enter ? field_enter : out ? field_exit : ""} !important; + animation-delay: ${({ enter }) => enter ? "0.4s" : "0s"}; +`; + +const FieldTitle = styled.div` + font-size: 1.35rem; + font-weight: 500; +`; + +const FieldDescription = styled.div` + font-size: 1rem; + color: #666; + margin-bottom: 1rem; +`; export default function DownloadPage() { - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - platform: getDefaultPlatformBasedOnUserAgent(), - }, - }); - const watchRelease = form.watch("platform"); + const [platform, setPlatform] = useState(null); + const [architecture, setArchitecture] = useState(null); + const [windowsDownloadType, setWindowsDownloadType] = useState(null); + const [linuxDownloadType, setLinuxDownloadType] = useState(null); - const onSubmit = async (data: any) => { - const platform = data.platform; - addDownload(platform); - console.log("Data: ", data) - console.log("Platform: ", platform) - console.log("Releases: ", releases) - const releasesForPlatform = releases[platform]; - console.log("Releases for platform: ", releasesForPlatform) - const url = `${BASE_URL}/${releasesForPlatform}`; - console.log("URL: ", url) - window.open(url, "_blank"); + const [selectedPlatform, setSelectedPlatform] = useState(getDefaultPlatformBasedOnUserAgent()); + const [selectedArchitecture, setSelectedArchitecture] = useState("64-bit"); + const [selectedWindowsDownloadType, setSelectedWindowsDownloadType] = useState("installer"); + const [selectedLinuxDownloadType, setSelectedLinuxDownloadType] = useState("portable"); + + const [hasDownloaded, setHasDownloaded] = useState(false); + + const [flowIndex, setFlowIndex] = useState(0); + + const throwConfetti = () => { + const end = Date.now() + 3 * 1000 // 3 seconds + const colors = ['#a786ff', '#fd8bbc', '#eca184', '#f8deb1'] + const frame = () => { + if (Date.now() > end) + return + + confetti({ + particleCount: 2, + angle: 60, + spread: 55, + startVelocity: 60, + origin: { x: 0, y: 0.5 }, + colors, + }) + confetti({ + particleCount: 2, + angle: 120, + spread: 55, + startVelocity: 60, + origin: { x: 1, y: 0.5 }, + colors, + }) + requestAnimationFrame(frame) + } + frame() + } + + const startDownload = () => { + const platform = releaseTree[selectedPlatform.toLowerCase()]; + let arch: string; + if (selectedArchitecture === "64-bit") { + arch = "x64"; + } else if (selectedArchitecture === "aarch64") { + arch = "arm"; + } else { + arch = "x32"; + } + let releaseTarget: string; + if (selectedPlatform === "MacOS") { + releaseTarget = platform[arch]; + } else { + releaseTarget = platform[arch][selectedPlatform === "Windows" + ? windowsDownloadType as string + : linuxDownloadType as string]; + } + console.log("Downloading: "); + console.log("platform: ", selectedPlatform); + console.log("arch: ", arch); + setHasDownloaded(true); + addDownload(releaseTarget); + //window.location.replace(`${BASE_URL}/${releases[releaseTarget]}`); + throwConfetti(); + }; + + const continueFlow = () => { + if (flowIndex === 0) + setPlatform(selectedPlatform); + if (flowIndex === 1) + setArchitecture(selectedArchitecture); + if (flowIndex === 2 || (flowIndex === 1 && platform === "MacOS")) { + setWindowsDownloadType(selectedWindowsDownloadType); + setLinuxDownloadType(selectedLinuxDownloadType); + startDownload(); + } + setFlowIndex(flowIndex + 1); + }; + + const goBackFlow = () => { + if (flowIndex === 1) { + setPlatform(null); + } else if (flowIndex === 2) { + setArchitecture(null); + } else if (flowIndex === 3) { + setWindowsDownloadType(null); + setSelectedWindowsDownloadType("installer"); + setLinuxDownloadType(null); + setSelectedLinuxDownloadType("portable"); + } + if (flowIndex > 0) + setFlowIndex(flowIndex - 1); + } + + const changeToFlatpak = () => { + if (selectedArchitecture === "64-bit") { + setSelectedLinuxDownloadType("flatpak"); + } } return ( -
-
- + + + + +
+
+ {hasDownloaded && ( +
+

Downloaded! ❤️

+

Zen Browser has been downloaded successfully. Enjoy browsing the web with Zen!

+ + {selectedPlatform === "MacOS" && ( +
+

Installation Instructions

+

To install Zen on MacOS, the process is a bit different. Please follow the instructions below:

+ +
+ )} +
+ ) || ( + <> +

Download Zen

+

We are so excited for you to try Zen Browser. But first, we need to know what kind of device you are using. It will be fast, we promise.

+ )} - /> -

- Download Zen Browser -

-

- Get started with Zen Browser today. Get back to browsing the web with peace of mind. -

-
-
-
-
- - ( - - Select your operating system - - - )} - /> - - {watchRelease === "Linux" && ( -
-

- Linux user?
- We{"'"}ve recently added support for flatpak! You can download the flatpak version by running the following command: -

flatpak install flathub io.github.zen_browser.zen
-

+
+ {platform === null && ( + + Platform + Choose the platform you want to download Zen for. +
setSelectedPlatform("Windows")} className={ny("select-none mb-2 px-4 py-3 flex items-center rounded-lg bg-background cursor-pointer border", selectedPlatform === "Windows" ? "border-blue-400" : "")}> + + +
Windows
- )} - {watchRelease === "MacOS" && ( -
-

- Mac user?
- Setting up the MacOS version of Zen Browser is a bit more involved. We{"'"}re working on making this easier, but for now you can follow these steps: - - - -

+
setSelectedPlatform("Linux")} className={ny("select-none mb-2 px-4 py-3 flex items-center rounded-lg bg-background cursor-pointer border", selectedPlatform === "Linux" ? "border-yellow-400" : "")}> + + +
Linux
- )} - - +
setSelectedPlatform("MacOS")} className={ny("select-none mb-2 px-4 py-3 flex items-center rounded-lg bg-background cursor-pointer border", selectedPlatform === "MacOS" ? "border-purple-400" : "")}> + + +
MacOS
+
+ + )} + {/* Architecture */} + {((platform === "Windows" || platform === "Linux") && flowIndex === 1) && ( + = 1} + > + Select Architecture + Choose the architecture of your device, either 32-bit or 64-bit. +
+
setSelectedArchitecture("64-bit")} className={ny("select-none w-full h-full mb-2 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedArchitecture === "64-bit" ? "border-blue-400" : "")}> +

🚀

+

64 Bits

+

Blazing fast and compatible with modern devices

+
+
setSelectedArchitecture("32-bit")} className={ny("select-none w-full h-full mb-2 ml-10 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedArchitecture === "32-bit" ? "border-blue-400" : "")}> +

👴

+

32 Bits

+

Slow but compatible with older devices.

+
+
+
+ )} + {(platform === "MacOS" && flowIndex === 1) && ( + + Download Zen for MacOS + Click the button below to download Zen for MacOS. +
+
setSelectedArchitecture("aarch64")} className={ny("select-none w-full h-full mb-2 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedArchitecture === "aarch64" ? "border-blue-400" : "")}> +

🍏

+

aarch64

+

64-bit ARM architecture, for Apple's M1 or M2 chips

+
+
setSelectedArchitecture("64-bit")} className={ny("select-none w-full h-full mb-2 ml-10 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedArchitecture === "64-bit" ? "border-blue-400" : "")}> +

x64

+

Intel

+

64-bit Intel architecture, for older Macs

+
+
+
+ )} + {flowIndex === 2 && (platform === "Windows") && ( + = 2} + > + Download Zen for Windows {selectedArchitecture} + Choose the type of download you want for Zen for Windows. +
+
setSelectedWindowsDownloadType("installer")} className={ny("select-none w-full h-full mb-2 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedWindowsDownloadType === "installer" ? "border-blue-400" : "")}> +

🚀

+

Installer

+

Install Zen with a setup wizard

+
+
setSelectedWindowsDownloadType("portable")} className={ny("select-none w-full h-full mb-2 ml-10 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedWindowsDownloadType === "portable" ? "border-blue-400" : "")}> +

📦

+

Portable

+

Download Zen as a ZIP file

+
+
+
+ )} + {flowIndex === 2 && (platform === "Linux") && ( + = 2} + > + Download Zen for Linux {selectedArchitecture} + Choose the type of download you want for Zen for Linux. +
+
setSelectedLinuxDownloadType("AppImage")} className={ny("select-none w-full h-full mb-2 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedLinuxDownloadType === "AppImage" ? "border-blue-400" : "")}> +

🚀

+

AppImage

+

Install Zen with a setup wizard

+
+
setSelectedLinuxDownloadType("portable")} className={ny("select-none w-full h-full mb-2 ml-5 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedLinuxDownloadType === "portable" ? "border-blue-400" : "")}> +

📦

+

Portable

+

Download Zen as a ZIP file

+
+
changeToFlatpak()} className={ny("select-none w-full h-full mb-2 ml-5 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedLinuxDownloadType === "flatpak" ? "border-blue-400" : "", + selectedArchitecture === "32-bit" ? "opacity-50 cursor-not-allowed" : "")}> +

🧑‍💻

+

Flatpak

+

+ Install Zen from the Flatpak repository. +

+
+
+
+ )} +
+ {!hasDownloaded && ( +
+ + +
+ )}
-
+ + + ); } diff --git a/src/components/features.tsx b/src/components/features.tsx index 11733c9..3b0f377 100644 --- a/src/components/features.tsx +++ b/src/components/features.tsx @@ -4,6 +4,7 @@ import Feature, { FeatureCard } from "./feature"; import { Button } from "./ui/button"; import TextReveal from "./ui/text-reveal"; import styled, { css, keyframes } from "styled-components"; +import BlurFade from "./ui/blur-fade"; const profileColors = [ "#e8cd7d", @@ -70,31 +71,37 @@ export default function Features() { }, []); return (
- - - Split Views - - - Split Views - - - - - - + {/**/} + + + Split Views + + + + + Split Views + + + + + + + + +

Want more?

Zen Browser is packed with features that will change the way you browse the web. Download it today and experience a new way to browse the web.

diff --git a/src/components/header.tsx b/src/components/header.tsx index 7ee227a..7b61dd4 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,83 +1,103 @@ +'use client' -import { ny } from "@/lib/utils"; -import AnimatedGradientText from "./ui/animated-gradient-text"; -import { ChevronDown, ChevronRight } from "lucide-react"; -import WordPullUp from "./ui/word-pull-up"; -import ShinyButton from "./ui/shiny-button"; -import GridPattern from "./ui/grid-pattern"; -import BlurIn from "./ui/blur-in"; -import { FadeText } from "./ui/fade-text"; -import styled, { keyframes } from "styled-components"; +import { ArrowRightIcon } from '@radix-ui/react-icons' +import { useInView } from 'framer-motion' +import { useRef } from 'react' +import AnimatedGradientText from './ui/animated-gradient-text' +import { Button } from './ui/button' +import { BorderBeam } from './ui/border-beam' +import { ny } from '@/lib/utils' +import { ChevronRight } from 'lucide-react' +import Particles from './ui/particles' -const HeaderElement = styled.div` - background: light-dark(white, rgba(0, 0, 0, 0.5)); -`; export default function Header() { - return ( + const ref = useRef(null) + const inView = useInView(ref, { once: true, margin: '-100px' }) + return ( <> -
- - -
- - - - - - - +

+ Zen is the best way +
+ {' '} + to browse the web. +

+

+ Beautifully designed, privacy-focused, and packed with features. +
+ {' '} + We care about your experience, not your data. +

+ +
+
+ + + browser Image + browser Image +
+
+ + + - ) + ) } diff --git a/src/components/ui/blur-fade.tsx b/src/components/ui/blur-fade.tsx new file mode 100644 index 0000000..df2e1dc --- /dev/null +++ b/src/components/ui/blur-fade.tsx @@ -0,0 +1,60 @@ +'use client' + +import { useRef } from 'react' +import type { Variants } from 'framer-motion' +import { AnimatePresence, motion, useInView } from 'framer-motion' + +interface BlurFadeProps { + children: React.ReactNode + className?: string + variant?: { + hidden: { y: number } + visible: { y: number } + } + duration?: number + delay?: number + yOffset?: number + inView?: boolean + inViewMargin?: string + blur?: string +} + +export default function BlurFade({ + children, + className, + variant, + duration = 0.4, + delay = 0, + yOffset = 6, + inView = false, + inViewMargin = '-50px', + blur = '6px', +}: BlurFadeProps) { + const ref = useRef(null) + const inViewResult = useInView(ref, { once: true, margin: inViewMargin }) + const isInView = !inView || inViewResult + const defaultVariants: Variants = { + hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` }, + visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` }, + } + const combinedVariants = variant || defaultVariants + return ( + + + {children} + + + ) +} diff --git a/src/components/ui/border-beam.tsx b/src/components/ui/border-beam.tsx new file mode 100644 index 0000000..d114c64 --- /dev/null +++ b/src/components/ui/border-beam.tsx @@ -0,0 +1,49 @@ +import { ny } from '@/lib/utils' + +interface BorderBeamProps { + className?: string + size?: number + duration?: number + borderWidth?: number + anchor?: number + colorFrom?: string + colorTo?: string + delay?: number +} + +export function BorderBeam({ + className, + size = 200, + duration = 15, + anchor = 90, + borderWidth = 1.5, + colorFrom = '#ffaa40', + colorTo = '#9c40ff', + delay = 0, +}: BorderBeamProps) { + return ( +
+ ) +} diff --git a/src/components/ui/confetti.tsx b/src/components/ui/confetti.tsx new file mode 100644 index 0000000..089e9ab --- /dev/null +++ b/src/components/ui/confetti.tsx @@ -0,0 +1,120 @@ +'use client' +import confetti from 'canvas-confetti' +import type { ReactNode } from 'react' +import React, { createContext, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react' + +import type { + GlobalOptions as ConfettiGlobalOptions, + CreateTypes as ConfettiInstance, + Options as ConfettiOptions, +} from 'canvas-confetti' +import { Button } from '~/components/ui/button' +import type { ButtonProps } from '~/components/ui/button' + +interface Api { + fire: (options?: ConfettiOptions) => void +} + +type Props = React.ComponentPropsWithRef<'canvas'> & { + options?: ConfettiOptions + globalOptions?: ConfettiGlobalOptions + manualstart?: boolean + children?: ReactNode +} + +export type ConfettiRef = Api | null + +const ConfettiContext = createContext({} as Api) + +const Confetti = forwardRef((props, ref) => { + const { + options, + globalOptions = { resize: true, useWorker: true }, + manualstart = false, + children, + ...rest + } = props + const instanceRef = useRef(null) // confetti instance + + const canvasRef = useCallback( + // https://react.dev/reference/react-dom/components/common#ref-callback + // https://reactjs.org/docs/refs-and-the-dom.html#callback-refs + (node: HTMLCanvasElement) => { + if (node !== null) { + // is mounted => create the confetti instance + if (instanceRef.current) + return // if not already created + instanceRef.current = confetti.create(node, { + ...globalOptions, + resize: true, + }) + } + else { + // is unmounted => reset and destroy instanceRef + if (instanceRef.current) { + instanceRef.current.reset() + instanceRef.current = null + } + } + }, + [globalOptions], + ) + + // `fire` is a function that calls the instance() with `opts` merged with `options` + const fire = useCallback( + (opts = {}) => instanceRef.current?.({ ...options, ...opts }), + [options], + ) + + const api = useMemo( + () => ({ + fire, + }), + [fire], + ) + + useImperativeHandle(ref, () => api, [api]) + + useEffect(() => { + if (!manualstart) + fire() + }, [manualstart, fire]) + + return ( + + + {children} + + ) +}) + +interface ConfettiButtonProps extends ButtonProps { + options?: ConfettiOptions & + ConfettiGlobalOptions & { canvas?: HTMLCanvasElement } + children?: React.ReactNode +} + +function ConfettiButton({ options, children, ...props }: ConfettiButtonProps) { + const handleClick = (event: React.MouseEvent) => { + const rect = event.currentTarget.getBoundingClientRect() + const x = rect.left + rect.width / 2 + const y = rect.top + rect.height / 2 + confetti({ + ...options, + origin: { + x: x / window.innerWidth, + y: y / window.innerHeight, + }, + }) + } + + return ( + + ) +} + +export { Confetti, ConfettiButton } + +export default Confetti diff --git a/src/components/ui/particles.tsx b/src/components/ui/particles.tsx new file mode 100644 index 0000000..052bafd --- /dev/null +++ b/src/components/ui/particles.tsx @@ -0,0 +1,271 @@ +'use client' + +import React, { useEffect, useRef, useState } from 'react' + +interface MousePosition { + x: number + y: number +} + +function MousePosition(): MousePosition { + const [mousePosition, setMousePosition] = useState({ + x: 0, + y: 0, + }) + + useEffect(() => { + const handleMouseMove = (event: MouseEvent) => { + setMousePosition({ x: event.clientX, y: event.clientY }) + } + + window.addEventListener('mousemove', handleMouseMove) + + return () => { + window.removeEventListener('mousemove', handleMouseMove) + } + }, []) + + return mousePosition +} + +interface ParticlesProps { + className?: string + quantity?: number + staticity?: number + ease?: number + size?: number + refresh?: boolean + color?: string + vx?: number + vy?: number +} +function hexToRgb(hex: string): number[] { + hex = hex.replace('#', '') + const hexInt = Number.parseInt(hex, 16) + const red = (hexInt >> 16) & 255 + const green = (hexInt >> 8) & 255 + const blue = hexInt & 255 + return [red, green, blue] +} + +const Particles: React.FC = ({ + className = '', + quantity = 100, + staticity = 50, + ease = 50, + size = 0.4, + refresh = false, + color = '#ffffff', + vx = 0, + vy = 0, +}) => { + const canvasRef = useRef(null) + const canvasContainerRef = useRef(null) + const context = useRef(null) + const circles = useRef([]) + const mousePosition = MousePosition() + const mouse = useRef<{ x: number, y: number }>({ x: 0, y: 0 }) + const canvasSize = useRef<{ w: number, h: number }>({ w: 0, h: 0 }) + const dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1 + + useEffect(() => { + if (canvasRef.current) { + context.current = canvasRef.current.getContext('2d') + } + initCanvas() + animate() + window.addEventListener('resize', initCanvas) + + return () => { + window.removeEventListener('resize', initCanvas) + } + }, [color]) + + useEffect(() => { + onMouseMove() + }, [mousePosition.x, mousePosition.y]) + + useEffect(() => { + initCanvas() + }, [refresh]) + + const initCanvas = () => { + resizeCanvas() + drawParticles() + } + + const onMouseMove = () => { + if (canvasRef.current) { + const rect = canvasRef.current.getBoundingClientRect() + const { w, h } = canvasSize.current + const x = mousePosition.x - rect.left - w / 2 + const y = mousePosition.y - rect.top - h / 2 + const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2 + if (inside) { + mouse.current.x = x + mouse.current.y = y + } + } + } + + interface Circle { + x: number + y: number + translateX: number + translateY: number + size: number + alpha: number + targetAlpha: number + dx: number + dy: number + magnetism: number + } + + const resizeCanvas = () => { + if (canvasContainerRef.current && canvasRef.current && context.current) { + circles.current.length = 0 + canvasSize.current.w = canvasContainerRef.current.offsetWidth + canvasSize.current.h = canvasContainerRef.current.offsetHeight + canvasRef.current.width = canvasSize.current.w * dpr + canvasRef.current.height = canvasSize.current.h * dpr + canvasRef.current.style.width = `${canvasSize.current.w}px` + canvasRef.current.style.height = `${canvasSize.current.h}px` + context.current.scale(dpr, dpr) + } + } + + const circleParams = (): Circle => { + const x = Math.floor(Math.random() * canvasSize.current.w) + const y = Math.floor(Math.random() * canvasSize.current.h) + const translateX = 0 + const translateY = 0 + const pSize = Math.floor(Math.random() * 2) + size + const alpha = 0 + const targetAlpha = Number.parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)) + const dx = (Math.random() - 0.5) * 0.1 + const dy = (Math.random() - 0.5) * 0.1 + const magnetism = 0.1 + Math.random() * 4 + return { + x, + y, + translateX, + translateY, + size: pSize, + alpha, + targetAlpha, + dx, + dy, + magnetism, + } + } + + const rgb = hexToRgb(color) + + const drawCircle = (circle: Circle, update = false) => { + if (context.current) { + const { x, y, translateX, translateY, size, alpha } = circle + context.current.translate(translateX, translateY) + context.current.beginPath() + context.current.arc(x, y, size, 0, 2 * Math.PI) + context.current.fillStyle = `rgba(${rgb.join(', ')}, ${alpha})` + context.current.fill() + context.current.setTransform(dpr, 0, 0, dpr, 0, 0) + + if (!update) { + circles.current.push(circle) + } + } + } + + const clearContext = () => { + if (context.current) { + context.current.clearRect( + 0, + 0, + canvasSize.current.w, + canvasSize.current.h, + ) + } + } + + const drawParticles = () => { + clearContext() + const particleCount = quantity + for (let i = 0; i < particleCount; i++) { + const circle = circleParams() + drawCircle(circle) + } + } + + const remapValue = ( + value: number, + start1: number, + end1: number, + start2: number, + end2: number, + ): number => { + const remapped + = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2 + return remapped > 0 ? remapped : 0 + } + + const animate = () => { + clearContext() + circles.current.forEach((circle: Circle, i: number) => { + // Handle the alpha value + const edge = [ + circle.x + circle.translateX - circle.size, // distance from left edge + canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge + circle.y + circle.translateY - circle.size, // distance from top edge + canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge + ] + const closestEdge = edge.reduce((a, b) => Math.min(a, b)) + const remapClosestEdge = Number.parseFloat( + remapValue(closestEdge, 0, 20, 0, 1).toFixed(2), + ) + if (remapClosestEdge > 1) { + circle.alpha += 0.02 + if (circle.alpha > circle.targetAlpha) { + circle.alpha = circle.targetAlpha + } + } + else { + circle.alpha = circle.targetAlpha * remapClosestEdge + } + circle.x += circle.dx + vx + circle.y += circle.dy + vy + circle.translateX + += (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) + / ease + circle.translateY + += (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) + / ease + + drawCircle(circle, true) + + // circle gets out of the canvas + if ( + circle.x < -circle.size + || circle.x > canvasSize.current.w + circle.size + || circle.y < -circle.size + || circle.y > canvasSize.current.h + circle.size + ) { + // remove the circle from the array + circles.current.splice(i, 1) + // create a new circle + const newCircle = circleParams() + drawCircle(newCircle) + // update the circle position + } + }) + window.requestAnimationFrame(animate) + } + + return ( + + ) +} + +export default Particles diff --git a/src/lib/releases.ts b/src/lib/releases.ts index 99f4dec..3bee523 100644 --- a/src/lib/releases.ts +++ b/src/lib/releases.ts @@ -1,7 +1,46 @@ -export const releases: any = { - WindowsInstaller: "zen.installer.exe", - WindowsStubInstaller: "zen.installer.pretty.exe", - WindowsZip: "zen.win64.zip", - MacOS: "zen.macos.dmg", - Linux: "zen.linux.tar.bz2", -}; \ No newline at end of file +export const releases: any = { + WindowsInstaller: "zen.installer-x64.exe", + WindowsInstaller32: "zen.installer-x32.exe", + + WindowsZip: "zen.win-x64.zip", + WindowsZip32: "zen.win-x32.zip", + + MacOS: "zen.macos-aarch64.dmg", + MacOSIntel: "zen.macos-x64.dmg", + + Linux: "zen.linux-x64.tar.bz2", + Linux32: "zen.linux-x32.tar.bz2", + + LinuxAppImage: "zen-x64.AppImage", + LinuxAppImage32: "zen-x32.AppImage", +}; + +// platform +// -> arch +// -> file +export const releaseTree: any = { + windows: { + x64: { + installer: "WindowsInstaller", + portable: "WindowsZip", + }, + x32: { + installer: "WindowsInstaller32", + portable: "WindowsZip32", + }, + }, + macos: { + x64: "MacOSIntel", + arm: "MacOS", + }, + linux: { + x64: { + portable: "Linux", + appimage: "LinuxAppImage", + }, + x32: { + portable: "Linux32", + appimage: "LinuxAppImage32", + }, + }, +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 9546975..15faa14 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,4 +1,5 @@ import type { Config } from "tailwindcss" +import { fontFamily } from 'tailwindcss/defaultTheme' const config = { darkMode: ["class"], @@ -12,81 +13,117 @@ const config = { theme: { container: { center: true, - padding: "2rem", + padding: '2rem', screens: { - "2xl": "1400px", + '2xl': '1400px', + }, + }, + extend: { + fontFamily: { + sans: ['var(--font-sans)', ...fontFamily.sans], }, - }, - extend: { colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, }, borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', }, keyframes: { - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, - }, - grid: { - "0%": { transform: "translateY(-50%)" }, - "100%": { transform: "translateY(0)" }, - }, - "shine-pulse": { - "0%": { - "background-position": "0% 0%", - }, - "50%": { - "background-position": "100% 100%", - }, - to: { - "background-position": "0% 0%", - }, - }, + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + 'border-beam': { + '100%': { + 'offset-distance': '100%', + }, + }, + 'image-glow': { + '0%': { + 'opacity': '0', + 'animation-timing-function': 'cubic-bezier(0.74, 0.25, 0.76, 1)', + }, + '10%': { + 'opacity': '0.7', + 'animation-timing-function': 'cubic-bezier(0.12, 0.01, 0.08, 0.99)', + }, + '100%': { + opacity: '0.4', + }, + }, + 'fade-in': { + from: { opacity: '0', transform: 'translateY(-10px)' }, + to: { opacity: '1', transform: 'none' }, + }, + 'fade-up': { + from: { opacity: '0', transform: 'translateY(20px)' }, + to: { opacity: '1', transform: 'none' }, + }, + 'shimmer': { + '0%, 90%, 100%': { + 'background-position': 'calc(-100% - var(--shimmer-width)) 0', + }, + '30%, 60%': { + 'background-position': 'calc(100% + var(--shimmer-width)) 0', + }, + }, + 'marquee': { + from: { transform: 'translateX(0)' }, + to: { transform: 'translateX(calc(-100% - var(--gap)))' }, + }, + 'marquee-vertical': { + from: { transform: 'translateY(0)' }, + to: { transform: 'translateY(calc(-100% - var(--gap)))' }, + }, }, animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - grid: "grid 15s linear infinite", + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear', + 'image-glow': 'image-glow 4100ms 600ms ease-out forwards', + 'fade-in': 'fade-in 1000ms var(--animation-delay, 0ms) ease forwards', + 'fade-up': 'fade-up 1000ms var(--animation-delay, 0ms) ease forwards', + 'shimmer': 'shimmer 8s infinite', + 'marquee': 'marquee var(--duration) infinite linear', + 'marquee-vertical': 'marquee-vertical var(--duration) linear infinite', }, }, },