feat: Add logo with text to navigation menu

This commit is contained in:
mauro 🤙
2024-07-04 07:58:14 +00:00
parent cb35ed18c9
commit 5396e42be8
12 changed files with 1250 additions and 47 deletions

View 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 }

View 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 }

View 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
View 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,
}

View 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 }

View 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,
}