feat: Add logo with text to navigation menu
This commit is contained in:
@@ -2,19 +2,20 @@
|
||||
|
||||
import { ny } from "@/lib/utils";
|
||||
import GridPattern from "./ui/grid-pattern";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
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'
|
||||
|
||||
const BASE_URL = "https://github.com/zen-browser/desktop/releases/download/latest";
|
||||
|
||||
const releases: any = {
|
||||
Windows: [
|
||||
"zen.win64.zip",
|
||||
],
|
||||
Windows: "zen.installer.exe",
|
||||
WindowsZip: "zen.win64.zip",
|
||||
//MacOS: [],
|
||||
Linux: [
|
||||
"zen.linux.tar.bz2",
|
||||
],
|
||||
Linux: "zen.linux.tar.bz2",
|
||||
};
|
||||
|
||||
function getDefaultPlatformBasedOnUserAgent() {
|
||||
@@ -34,7 +35,30 @@ function getDefaultPlatformBasedOnUserAgent() {
|
||||
return "Windows";
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
platform: z.string().nonempty(),
|
||||
});
|
||||
|
||||
export default function DownloadPage() {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
platform: getDefaultPlatformBasedOnUserAgent(),
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
const platform = data.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.location.href = url;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full relative h-screen flex items-center justify-center">
|
||||
<div className="w-1/2 relative h-full px-64 flex items-cetner justify-center flex-col">
|
||||
@@ -60,38 +84,37 @@ export default function DownloadPage() {
|
||||
Get started with Zen Browser today. Get back to browsing the web with peace of mind.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-1/2 relative flex items-cetner justify-start">
|
||||
<Tabs defaultValue={getDefaultPlatformBasedOnUserAgent()} className="mx-auto w-fit flex flex-col items-start justify-center">
|
||||
<TabsList>
|
||||
{Object.keys(releases).map((platform) => (
|
||||
<TabsTrigger key={platform} value={platform}>{platform}</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{Object.keys(releases).map((platform) => (
|
||||
<TabsContent key={platform} value={platform} className="border rounded-md p-5 border-gray">
|
||||
<table>
|
||||
<thead className="">
|
||||
<tr>
|
||||
<th className="text-start pb-4 min-w-64">File</th>
|
||||
<th className="text-start pb-4">Download</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{releases[platform].map((release: string) => (
|
||||
<tr key={release} className="relative w-full">
|
||||
<td className="min-w-64">{release}</td>
|
||||
<td className="flex items-center w-full justify-center">
|
||||
<a href={`${BASE_URL}/${release}`} className="text-blue-500">
|
||||
<DownloadIcon size={24} />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
<div className="w-1/2 relative flex flex-col relative items-cetner justify-start">
|
||||
<div className="w-1/2 relative">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="platform"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Select your operating system</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<SelectTrigger className="w-full mb-5">
|
||||
<SelectValue placeholder="Operating System" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Operating System</SelectLabel>
|
||||
<SelectItem value="Windows">Windows Installer</SelectItem>
|
||||
<SelectItem value="WindowsZip">Windows (Zip)</SelectItem>
|
||||
<SelectItem value="MacOS" disabled>MacOS</SelectItem>
|
||||
<SelectItem value="Linux">Linux</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Download Zen 🎉</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -63,7 +63,9 @@ export default function Header() {
|
||||
text="Documentation"
|
||||
/>
|
||||
</a>
|
||||
<ShinyButton text="Download now" />
|
||||
<a href="/download">
|
||||
<ShinyButton text="Download now" />
|
||||
</a>
|
||||
</div>
|
||||
<ChevronDown className="absolute bottom-5 left-1/2 size-7 mb-10 animate-bounce" style={{
|
||||
transform: 'translateX(-50%)',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
|
||||
export default function Logo() {
|
||||
export default function Logo({ withText, ...props }: any) {
|
||||
return (
|
||||
<img src="/logo.png" alt="Zen Logo" className="w-12 h-12" />
|
||||
<div className="flex items-center m-0" {...props}>
|
||||
<img src="/logo.png" alt="Zen Logo" className="w-12 h-12" />
|
||||
{withText && <span className="text-2xl font-bold ml-2">Zen</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,12 @@ export function Navigation() {
|
||||
return (
|
||||
<div className="backdrop-blur fixed z-10 top-0 left-0 w-full flex fixed border-b border-grey p-2 items-center justify-center">
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuList className="w-full">
|
||||
<NavigationMenuItem className="cursor-pointer mr-20">
|
||||
<NavigationMenuLink href="/" asChild>
|
||||
<Logo withText />
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
|
||||
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp
|
||||
className={ny(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { CheckIcon } from '@radix-ui/react-icons'
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={ny('flex items-center justify-center text-current')}
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
239
src/components/ui/cool-mode.tsx
Normal file
239
src/components/ui/cool-mode.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
'use client'
|
||||
import type { ReactNode } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
|
||||
export interface BaseParticle {
|
||||
element: HTMLElement | SVGSVGElement
|
||||
left: number
|
||||
size: number
|
||||
top: number
|
||||
}
|
||||
|
||||
export interface BaseParticleOptions {
|
||||
particle?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
export interface CoolParticle extends BaseParticle {
|
||||
direction: number
|
||||
speedHorz: number
|
||||
speedUp: number
|
||||
spinSpeed: number
|
||||
spinVal: number
|
||||
}
|
||||
|
||||
export interface CoolParticleOptions extends BaseParticleOptions {
|
||||
particleCount?: number
|
||||
speedHorz?: number
|
||||
speedUp?: number
|
||||
}
|
||||
|
||||
function getContainer() {
|
||||
const id = '_coolMode_effect'
|
||||
const existingContainer = document.getElementById(id)
|
||||
|
||||
if (existingContainer)
|
||||
return existingContainer
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.setAttribute('id', id)
|
||||
container.setAttribute(
|
||||
'style',
|
||||
'overflow:hidden; position:fixed; height:100%; top:0; left:0; right:0; bottom:0; pointer-events:none; z-index:2147483647',
|
||||
)
|
||||
|
||||
document.body.appendChild(container)
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
let instanceCounter = 0
|
||||
|
||||
function applyParticleEffect(
|
||||
element: HTMLElement,
|
||||
options?: CoolParticleOptions,
|
||||
): () => void {
|
||||
instanceCounter++
|
||||
|
||||
const defaultParticle = 'circle'
|
||||
const particleType = options?.particle || defaultParticle
|
||||
const sizes = [15, 20, 25, 35, 45]
|
||||
const limit = 45
|
||||
|
||||
let particles: CoolParticle[] = []
|
||||
let autoAddParticle = false
|
||||
let mouseX = 0
|
||||
let mouseY = 0
|
||||
|
||||
const container = getContainer()
|
||||
|
||||
function generateParticle() {
|
||||
const size
|
||||
= options?.size || sizes[Math.floor(Math.random() * sizes.length)]
|
||||
const speedHorz = options?.speedHorz || Math.random() * 10
|
||||
const speedUp = options?.speedUp || Math.random() * 25
|
||||
const spinVal = Math.random() * 360
|
||||
const spinSpeed = Math.random() * 35 * (Math.random() <= 0.5 ? -1 : 1)
|
||||
const top = mouseY - size / 2
|
||||
const left = mouseX - size / 2
|
||||
const direction = Math.random() <= 0.5 ? -1 : 1
|
||||
|
||||
const particle = document.createElement('div')
|
||||
|
||||
if (particleType === 'circle') {
|
||||
const svgNS = 'http://www.w3.org/2000/svg'
|
||||
const circleSVG = document.createElementNS(svgNS, 'svg')
|
||||
const circle = document.createElementNS(svgNS, 'circle')
|
||||
circle.setAttributeNS(null, 'cx', (size / 2).toString())
|
||||
circle.setAttributeNS(null, 'cy', (size / 2).toString())
|
||||
circle.setAttributeNS(null, 'r', (size / 2).toString())
|
||||
circle.setAttributeNS(
|
||||
null,
|
||||
'fill',
|
||||
`hsl(${Math.random() * 360}, 70%, 50%)`,
|
||||
)
|
||||
|
||||
circleSVG.appendChild(circle)
|
||||
circleSVG.setAttribute('width', size.toString())
|
||||
circleSVG.setAttribute('height', size.toString())
|
||||
|
||||
particle.appendChild(circleSVG)
|
||||
}
|
||||
else {
|
||||
particle.innerHTML = `<img src="${particleType}" width="${size}" height="${size}" style="border-radius: 50%">`
|
||||
}
|
||||
|
||||
particle.style.position = 'absolute'
|
||||
particle.style.transform = `translate3d(${left}px, ${top}px, 0px) rotate(${spinVal}deg)`
|
||||
|
||||
container.appendChild(particle)
|
||||
|
||||
particles.push({
|
||||
direction,
|
||||
element: particle,
|
||||
left,
|
||||
size,
|
||||
speedHorz,
|
||||
speedUp,
|
||||
spinSpeed,
|
||||
spinVal,
|
||||
top,
|
||||
})
|
||||
}
|
||||
|
||||
function refreshParticles() {
|
||||
particles.forEach((p) => {
|
||||
p.left = p.left - p.speedHorz * p.direction
|
||||
p.top = p.top - p.speedUp
|
||||
p.speedUp = Math.min(p.size, p.speedUp - 1)
|
||||
p.spinVal = p.spinVal + p.spinSpeed
|
||||
|
||||
if (
|
||||
p.top
|
||||
>= Math.max(window.innerHeight, document.body.clientHeight) + p.size
|
||||
) {
|
||||
particles = particles.filter(o => o !== p)
|
||||
p.element.remove()
|
||||
}
|
||||
|
||||
p.element.setAttribute(
|
||||
'style',
|
||||
[
|
||||
'position:absolute',
|
||||
'will-change:transform',
|
||||
`top:${p.top}px`,
|
||||
`left:${p.left}px`,
|
||||
`transform:rotate(${p.spinVal}deg)`,
|
||||
].join(';'),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let animationFrame: number | undefined
|
||||
|
||||
let lastParticleTimestamp = 0
|
||||
const particleGenerationDelay = 30
|
||||
|
||||
function loop() {
|
||||
const currentTime = performance.now()
|
||||
if (
|
||||
autoAddParticle
|
||||
&& particles.length < limit
|
||||
&& currentTime - lastParticleTimestamp > particleGenerationDelay
|
||||
) {
|
||||
generateParticle()
|
||||
lastParticleTimestamp = currentTime
|
||||
}
|
||||
|
||||
refreshParticles()
|
||||
animationFrame = requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
loop()
|
||||
|
||||
const isTouchInteraction = 'ontouchstart' in window
|
||||
|
||||
const tap = isTouchInteraction ? 'touchstart' : 'mousedown'
|
||||
const tapEnd = isTouchInteraction ? 'touchend' : 'mouseup'
|
||||
const move = isTouchInteraction ? 'touchmove' : 'mousemove'
|
||||
|
||||
const updateMousePosition = (e: MouseEvent | TouchEvent) => {
|
||||
if ('touches' in e) {
|
||||
mouseX = e.touches?.[0].clientX
|
||||
mouseY = e.touches?.[0].clientY
|
||||
}
|
||||
else {
|
||||
mouseX = e.clientX
|
||||
mouseY = e.clientY
|
||||
}
|
||||
}
|
||||
|
||||
const tapHandler = (e: MouseEvent | TouchEvent) => {
|
||||
updateMousePosition(e)
|
||||
autoAddParticle = true
|
||||
}
|
||||
|
||||
const disableAutoAddParticle = () => {
|
||||
autoAddParticle = false
|
||||
}
|
||||
|
||||
element.addEventListener(move, updateMousePosition, { passive: true })
|
||||
element.addEventListener(tap, tapHandler, { passive: true })
|
||||
element.addEventListener(tapEnd, disableAutoAddParticle, { passive: true })
|
||||
element.addEventListener('mouseleave', disableAutoAddParticle, {
|
||||
passive: true,
|
||||
})
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(move, updateMousePosition)
|
||||
element.removeEventListener(tap, tapHandler)
|
||||
element.removeEventListener(tapEnd, disableAutoAddParticle)
|
||||
element.removeEventListener('mouseleave', disableAutoAddParticle)
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (animationFrame && particles.length === 0) {
|
||||
cancelAnimationFrame(animationFrame)
|
||||
clearInterval(interval)
|
||||
|
||||
if (--instanceCounter === 0)
|
||||
container.remove()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
interface CoolModeProps {
|
||||
children: ReactNode
|
||||
options?: CoolParticleOptions
|
||||
}
|
||||
|
||||
export const CoolMode: React.FC<CoolModeProps> = ({ children, options }) => {
|
||||
const ref = useRef<HTMLElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current)
|
||||
return applyParticleEffect(ref.current, options)
|
||||
}, [options])
|
||||
|
||||
return React.cloneElement(children as React.ReactElement, { ref })
|
||||
}
|
||||
189
src/components/ui/form.tsx
Normal file
189
src/components/ui/form.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import * as React from 'react'
|
||||
import type * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'
|
||||
import { Controller, FormProvider, useFormContext, useFormState } from 'react-hook-form'
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
interface FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
)
|
||||
|
||||
function FormField<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({ ...props }: ControllerProps<TFieldValues, TName>) {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function useFormField() {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext)
|
||||
throw new Error('useFormField should be used within <FormField>')
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
interface FormItemContextValue {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={ny('space-y-2', className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = 'FormItem'
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={ny(error && 'text-destructive', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = 'FormLabel'
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId }
|
||||
= useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = 'FormControl'
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={ny('text-[0.8rem] text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = 'FormDescription'
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body)
|
||||
return null
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={ny('text-[0.8rem] font-medium text-destructive', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = 'FormMessage'
|
||||
|
||||
const FormGlobalError = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { errors } = useFormState()
|
||||
const rootError = errors.root
|
||||
if (!rootError)
|
||||
return null
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
className={ny('text-sm font-medium text-destructive', className)}
|
||||
{...props}
|
||||
>
|
||||
{rootError.message}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormGlobalError.displayName = 'FormGlobalError'
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormGlobalError,
|
||||
FormField,
|
||||
}
|
||||
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={ny(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
168
src/components/ui/select.tsx
Normal file
168
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
CaretSortIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-left text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 [&>span]:text-left',
|
||||
className,
|
||||
)}
|
||||
onPointerDown={(e) => {
|
||||
if (e.pointerType === 'touch')
|
||||
e.preventDefault()
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<CaretSortIcon className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName
|
||||
= SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
position === 'popper'
|
||||
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={ny(
|
||||
'p-1',
|
||||
position === 'popper'
|
||||
&& 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={ny('px-2 py-1.5 text-sm font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={ny('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
Reference in New Issue
Block a user