Sistema de etiquetas y posicionamiento
This commit is contained in:
130
src/components/ui/drawer.tsx
Normal file
130
src/components/ui/drawer.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
46
src/components/ui/popover.tsx
Normal file
46
src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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 z-50 w-72 rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
56
src/components/ui/scroll-area.tsx
Normal file
56
src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
179
src/components/ui/select.tsx
Normal file
179
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger>) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex h-9 w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md",
|
||||
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={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("px-2 py-1.5 text-sm font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
61
src/components/ui/slider.tsx
Normal file
61
src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max]
|
||||
)
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
data-slot="slider"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Slider }
|
||||
@@ -4,6 +4,7 @@ import App from './pages/App.tsx'
|
||||
import { BrowserRouter, Route, Routes } from "react-router";
|
||||
import Login from './pages/Login.tsx';
|
||||
import Previewer from './pages/Previewer.tsx';
|
||||
import NotFound from './pages/NotFound.tsx';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<BrowserRouter>
|
||||
@@ -11,6 +12,7 @@ createRoot(document.getElementById('root')!).render(
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/previewer" element={<Previewer modelUrl="/3d/motor_de_combustion/scene.gltf" />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Label } from '../components/ui/label';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { NavLink } from 'react-router';
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -15,9 +14,11 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { useNavigate, useNavigation } from 'react-router';
|
||||
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<div className="w-full grow bg-gradient-to-br from-gray-100/50 to-gray-300/15 shadow-xl shadow-gray-400/10 flex flex-col items-center justify-center p-5 gap-20 rounded-lg backdrop-blur-sm">
|
||||
<Card className="w-fit">
|
||||
@@ -59,11 +60,11 @@ function App() {
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction className="font-display">Confirmar</AlertDialogAction>
|
||||
<AlertDialogAction className="font-display" onClick={() => navigate('/login')}>Confirmar</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Button className="font-display px-6 py-3 text-lg font-bold text-white bg-gradient-to-b from-blue-500 to-blue-700 rounded-lg shadow-lg shadow-blue-500/50 transform transition-all duration-300 hover:shadow-xl hover:scale-105 active:translate-y-1 active:shadow-md cursor-pointer">
|
||||
<Button className="font-display px-6 py-3 text-lg font-bold text-white bg-gradient-to-b from-blue-500 to-blue-700 rounded-lg shadow-lg shadow-blue-500/50 transform transition-all duration-300 hover:shadow-xl hover:scale-105 active:translate-y-1 active:shadow-md cursor-pointer" onClick={() => navigate('/previewer')}>
|
||||
Presentar
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useNavigate } from 'react-router';
|
||||
|
||||
function Login() {
|
||||
let navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="w-full grow bg-gradient-to-br from-gray-100 to-gray-300/20 shadow-xl shadow-gray-400/10 flex flex-col items-center justify-center p-5 gap-20 rounded-lg backdrop-blur-lg">
|
||||
<Card className="w-full max-w-md p-6 bg-white rounded-xl shadow-lg">
|
||||
@@ -29,7 +30,7 @@ function Login() {
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-4">
|
||||
<Button
|
||||
className="w-full font-bold text-lg py-2 bg-gradient-to-r from-blue-500 to-blue-700 text-white rounded-lg shadow-md transition-all duration-300 hover:scale-105 hover:shadow-lg active:shadow-sm" onClick={() => {
|
||||
className="font-display px-6 py-3 text-lg font-bold text-white bg-gradient-to-b from-blue-500 to-blue-700 rounded-lg shadow-lg shadow-blue-500/50 transform transition-all duration-300 hover:shadow-xl hover:scale-105 active:translate-y-1 active:shadow-md cursor-pointer" onClick={() => {
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
|
||||
19
src/pages/NotFound.tsx
Normal file
19
src/pages/NotFound.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NavLink } from 'react-router';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center text-white">
|
||||
<div className="w-full max-w-lg bg-gradient-to-br from-gray-100 to-gray-300/20 shadow-2xl shadow-gray-400/20 flex flex-col items-center justify-center p-10 gap-8 rounded-2xl backdrop-blur-lg border border-gray-200/30">
|
||||
<h1 className="text-6xl font-extrabold text-red-500 drop-shadow-lg font-display">404</h1>
|
||||
<p className="text-lg text-gray-700 text-center">La página que estás buscando no existe.</p>
|
||||
|
||||
<NavLink
|
||||
to="/"
|
||||
className="px-6 py-3 text-lg font-semibold text-white bg-blue-600 rounded-lg shadow-lg shadow-blue-600/30 transition-transform duration-200 hover:scale-105 hover:bg-blue-700"
|
||||
>
|
||||
Regresar
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +1,194 @@
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { useGLTF, useAnimations, Environment, Loader } from "@react-three/drei";
|
||||
import { Suspense, useEffect } from "react";
|
||||
"use client";
|
||||
|
||||
function Model({ url }: { url: string }) {
|
||||
import type { OrbitControls as OrbitControlsImpl } from 'three-stdlib';
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { useGLTF, useAnimations, Environment, Loader, OrbitControls, Html } from "@react-three/drei";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import { RotateCw, Play, Pause, Film, ListTree } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
// Model component
|
||||
function Model({ url, position, rotation, scale, onAnimationsLoaded, onNodesLoaded }: {
|
||||
url: string;
|
||||
position: [number, number, number];
|
||||
rotation: number;
|
||||
scale: number;
|
||||
onAnimationsLoaded: (animations: string[], actions: Record<string, any>) => void;
|
||||
onNodesLoaded: (nodes: string[], nodePositions: Record<string, [number, number, number]>) => void;
|
||||
}) {
|
||||
const { scene, animations } = useGLTF(url);
|
||||
const { actions } = useAnimations(animations, scene);
|
||||
|
||||
useEffect(() => {
|
||||
if (actions && animations.length > 0) {
|
||||
actions[animations[0].name]?.play(); // Play the first animation if available
|
||||
if (actions) {
|
||||
onAnimationsLoaded(animations.map(anim => anim.name), actions);
|
||||
}
|
||||
}, [actions, animations]);
|
||||
|
||||
return <primitive object={scene} scale={0.01} position={[0, -2, 0]} rotation={[0, -3 * Math.PI / 5, 0]} />;
|
||||
if (scene.children) {
|
||||
const nodeList = scene.children.map(node => node.name);
|
||||
const nodePositions = scene.children.reduce((acc, node) => {
|
||||
acc[node.name] = [node.position.x, node.position.y, node.position.z];
|
||||
return acc;
|
||||
}, {} as Record<string, [number, number, number]>);
|
||||
|
||||
onNodesLoaded(nodeList, nodePositions);
|
||||
}
|
||||
}, [actions, animations, onAnimationsLoaded, onNodesLoaded]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<primitive object={scene} scale={scale} position={position} rotation={[0, rotation, 0]} />
|
||||
{scene.children.map((node, index) => (
|
||||
<Html key={index} position={[node.position.x, node.position.y + 0.5, node.position.z]}>
|
||||
<div className="bg-blue-500 text-white px-2 py-1 rounded-md text-xs shadow-md">
|
||||
{node.name}
|
||||
</div>
|
||||
</Html>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
// Preload the model
|
||||
useGLTF.preload("/3d/motor_de_combustion/scene.gltf");
|
||||
|
||||
// Preload the model for smoother loading
|
||||
useGLTF.preload("/models/sample.glb");
|
||||
const InitialParams = {
|
||||
position: [0, -2, 0] as [number, number, number],
|
||||
rotation: -Math.PI / 2,
|
||||
scale: 0.01,
|
||||
};
|
||||
|
||||
// Main 3D Viewer
|
||||
export default function Previewer({ modelUrl }: { modelUrl: string }) {
|
||||
const controlsRef = useRef<OrbitControlsImpl>(null);
|
||||
const [animations, setAnimations] = useState<string[]>([]);
|
||||
const [actions, setActions] = useState<Record<string, any>>({});
|
||||
const [currentAnimation, setCurrentAnimation] = useState<string | null>(null);
|
||||
|
||||
const [nodes, setNodes] = useState<string[]>([]);
|
||||
const [nodePositions, setNodePositions] = useState<Record<string, [number, number, number]>>({});
|
||||
|
||||
|
||||
const resetView = () => {
|
||||
if (controlsRef.current) {
|
||||
controlsRef.current.reset();
|
||||
}
|
||||
};
|
||||
|
||||
const playAnimation = (name: string) => {
|
||||
if (actions[name]) {
|
||||
Object.values(actions).forEach(action => action.stop()); // Stop other animations
|
||||
actions[name].play();
|
||||
setCurrentAnimation(name);
|
||||
}
|
||||
};
|
||||
|
||||
const pauseAnimation = () => {
|
||||
if (currentAnimation && actions[currentAnimation]) {
|
||||
actions[currentAnimation].stop();
|
||||
setCurrentAnimation(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grow w-full bg-white/10 shadow-xl shadow-gray-300/25 rounded-lg p-6 backdrop-blur-lg">
|
||||
<div className="w-full h-100 grow">
|
||||
<div className="relative w-full grow bg-white/10 shadow-xl shadow-gray-300/25 rounded-lg p-6 backdrop-blur-lg h-100">
|
||||
<div className="z-10 absolute top-4 w-100 flex wrap gap-5">
|
||||
<Popover>
|
||||
<PopoverTrigger className="flex items-center gap-2 bg-gradient-to-r from-blue-600 to-blue-800 text-white px-4 py-2 rounded-lg shadow-lg transition-all duration-300 hover:from-blue-500 hover:to-blue-700">
|
||||
<Film className="w-5 h-5" />
|
||||
<span>Animaciones</span>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="p-4 bg-blue-900 text-white w-64 my-5 mx-15">
|
||||
<ScrollArea className="max-h-60">
|
||||
<div className="flex flex-col gap-2">
|
||||
{animations.length > 0 ? (
|
||||
animations.map((anim) => (
|
||||
<Button
|
||||
key={anim}
|
||||
className="w-full flex justify-between italic"
|
||||
onClick={() => {
|
||||
if (currentAnimation === anim) {
|
||||
pauseAnimation(); // Pause if it's already playing
|
||||
} else {
|
||||
playAnimation(anim); // Play if it's not currently playing
|
||||
}
|
||||
}}
|
||||
>
|
||||
{anim}
|
||||
{currentAnimation === anim ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
||||
</Button>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500 text-center">Este modelo no tiene animaciones.</p>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger className="flex items-center gap-2 bg-gradient-to-r from-blue-100 to-blue-50 text-blue-500 px-4 py-2 rounded-lg shadow-lg transition-all duration-300 hover:from-blue-100 hover:to-blue-200">
|
||||
<ListTree />
|
||||
Partes del modelo
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-4 bg-blue-50 text-white w-64 my-5 mx-15">
|
||||
<ScrollArea className="max-h-60">
|
||||
<div className="flex flex-col gap-2">
|
||||
{nodes.length > 0 ? (
|
||||
nodes.map((node, index) => (
|
||||
<div key={index} className="bg-blue-900/10 p-2 rounded-lg text-center text-blue-700">
|
||||
{node}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-blue-500 text-center">Este modelo no tiene partes.</p>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
{/* Reset Camera Button */}
|
||||
<div className="absolute bottom-4 right-4 z-10">
|
||||
<Button
|
||||
className="bg-gradient-to-r from-red-600 to-red-800 w-40 text-white px-5 py-3 rounded-lg shadow-lg transition-all duration-300 hover:from-red-500 hover:to-red-700 active:scale-95 flex items-center gap-2 justify-center"
|
||||
onClick={resetView}
|
||||
>
|
||||
<RotateCw className="w-5 h-5" />
|
||||
Reset Camera
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full">
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Canvas
|
||||
camera={{ position: [0, 0, 12], fov: 20 }}>
|
||||
<Canvas camera={{ position: [0, 0, 12], fov: 20 }}>
|
||||
<Environment preset="city" />
|
||||
<Model url={modelUrl} />
|
||||
<OrbitControls ref={controlsRef} />
|
||||
<Model
|
||||
url={modelUrl}
|
||||
position={InitialParams.position}
|
||||
rotation={InitialParams.rotation}
|
||||
scale={InitialParams.scale}
|
||||
onAnimationsLoaded={(animNames, actionMap) => {
|
||||
setAnimations(animNames);
|
||||
setActions(actionMap);
|
||||
}}
|
||||
onNodesLoaded={(nodeList, nodePos) => {
|
||||
setNodes(nodeList);
|
||||
setNodePositions(nodePos);
|
||||
}}
|
||||
/>
|
||||
</Canvas>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user