base con autenticacion, registro, modulo encuestas

This commit is contained in:
2025-06-16 12:02:22 -04:00
commit 475e0754df
411 changed files with 26265 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
'use client';
import { useBreadcrumbs } from '@/hooks/use-breadcrumbs';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@repo/shadcn/breadcrumb';
import { Slash } from 'lucide-react';
import { Fragment } from 'react';
export function Breadcrumbs() {
const items = useBreadcrumbs();
if (items.length === 0) return null;
return (
<Breadcrumb>
<BreadcrumbList>
{items.map((item, index) => (
<Fragment key={item.title}>
{index !== items.length - 1 && (
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href={item.link}>{item.title}</BreadcrumbLink>
</BreadcrumbItem>
)}
{index < items.length - 1 && (
<BreadcrumbSeparator className="hidden md:block">
<Slash />
</BreadcrumbSeparator>
)}
{index === items.length - 1 && (
<BreadcrumbItem>
<BreadcrumbPage>{item.title}</BreadcrumbPage>
</BreadcrumbItem>
)}
</Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
);
}

View File

@@ -0,0 +1,92 @@
import {
AlertTriangle,
ArrowRight,
Check,
ChevronLeft,
ChevronRight,
CircuitBoardIcon,
Command,
CreditCard,
File,
FileText,
HelpCircle,
Image,
Laptop,
LayoutDashboardIcon,
Loader2,
LogIn,
LucideIcon,
LucideProps,
LucideShoppingBag,
Moon,
MoreVertical,
Pizza,
Plus,
Settings,
SunMedium,
Trash,
Twitter,
User,
UserCircle2Icon,
UserPen,
UserX2Icon,
X,
Settings2,
ChartColumn,
NotepadText
} from 'lucide-react';
export type Icon = LucideIcon;
export const Icons = {
dashboard: LayoutDashboardIcon,
logo: Command,
login: LogIn,
close: X,
product: LucideShoppingBag,
spinner: Loader2,
kanban: CircuitBoardIcon,
chevronLeft: ChevronLeft,
chevronRight: ChevronRight,
trash: Trash,
employee: UserX2Icon,
post: FileText,
page: File,
userPen: UserPen,
user2: UserCircle2Icon,
media: Image,
settings: Settings,
billing: CreditCard,
ellipsis: MoreVertical,
add: Plus,
warning: AlertTriangle,
user: User,
arrowRight: ArrowRight,
help: HelpCircle,
pizza: Pizza,
sun: SunMedium,
moon: Moon,
laptop: Laptop,
settings2: Settings2,
chartColumn: ChartColumn,
notepadText: NotepadText,
gitHub: ({ ...props }: LucideProps) => (
<svg
aria-hidden='true'
focusable='false'
data-prefix='fab'
data-icon='github'
role='img'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 496 512'
{...props}
>
<path
fill='currentColor'
d='M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z'
></path>
</svg>
),
twitter: Twitter,
check: Check
};

View File

@@ -0,0 +1,13 @@
'use client';
import {
ThemeProvider as NextThemesProvider,
ThemeProviderProps
} from 'next-themes';
export default function ThemeProvider({
children,
...props
}: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@@ -0,0 +1,37 @@
'use client';
import { MoonIcon, SunIcon } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@repo/shadcn/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@repo/shadcn/dropdown-menu';
type CompProps = {};
export default function ThemeToggle({}: CompProps) {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,58 @@
'use client';
import { NavMain as ConfigMain, NavMain as GeneralMain, NavMain as AdministrationMain, NavMain as StatisticsMain, } from '@/components/nav-main';
import { GeneralItems, AdministrationItems, StatisticsItems } from '@/constants/data';
import {
Sidebar,
SidebarContent,
SidebarHeader,
SidebarRail,
} from '@repo/shadcn/sidebar';
import { GalleryVerticalEnd } from 'lucide-react';
import * as React from 'react';
// import { NavItem } from '@/types';
import { useSession } from 'next-auth/react';
export const company = {
name: 'Sistema',
logo: GalleryVerticalEnd,
plan: 'FONDEMI',
};
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const { data: session } = useSession();
const userRole = session?.user.role[0]?.rol ? session.user.role[0].rol :'';
// console.log(AdministrationItems[0]?.role);
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<div className="flex gap-2 py-2 text-sidebar-accent-foreground">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<company.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{company.name}</span>
<span className="truncate text-xs">{company.plan}</span>
</div>
</div>
</SidebarHeader>
<SidebarContent>
<GeneralMain titleGroup={'General'} items={GeneralItems} role={userRole}/>
{StatisticsItems[0]?.role?.includes(userRole) &&
<StatisticsMain titleGroup={'Estadisticas'} items={StatisticsItems} role={userRole}/>
}
{AdministrationItems[0]?.role?.includes(userRole) &&
<AdministrationMain titleGroup={'Administracion'} items={AdministrationItems} role={userRole}/>
}
{/* <NavProjects projects={data.projects} /> */}
</SidebarContent>
<SidebarRail />
</Sidebar>
);
}

View File

@@ -0,0 +1,25 @@
import { Separator } from '@repo/shadcn/separator';
import { SidebarTrigger } from '@repo/shadcn/sidebar';
import { Breadcrumbs } from '../breadcrumbs';
import ThemeToggle from './ThemeToggle/theme-toggle';
import { UserNav } from './user-nav';
export default function Header() {
return (
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator
orientation="vertical"
className="mr-2 data-[orientation=vertical]:h-4"
/>
<Breadcrumbs />
</div>
<div className="flex items-center gap-2 px-4 ml-auto">
<ThemeToggle />
<UserNav />
</div>
</header>
);
}

View File

@@ -0,0 +1,22 @@
import { ScrollArea } from '@repo/shadcn/scroll-area';
import React from 'react';
export default function PageContainer({
children,
scrollable = true,
}: {
children: React.ReactNode;
scrollable?: boolean;
}) {
return (
<>
{scrollable ? (
<ScrollArea className="h-[calc(100dvh-52px)]">
<div className="flex flex-1 p-4 md:px-6">{children}</div>
</ScrollArea>
) : (
<div className="flex flex-1 p-4 md:px-6">{children}</div>
)}
</>
);
}

View File

@@ -0,0 +1,48 @@
'use client';
import { ThemeProvider } from '@repo/shadcn/themes-provider';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SessionProvider, SessionProviderProps } from 'next-auth/react';
import { NuqsAdapter } from 'nuqs/adapters/next/app';
import { ReactNode } from 'react';
type ProvidersProps = {
children: ReactNode;
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
gcTime: 60 * 60 * 1000, // 1 hora para garbage collection
staleTime: 60 * 60 * 1000, // 1 hora para considerar datos obsoletos
refetchOnWindowFocus: false, // No recargar al enfocar la ventana
refetchOnMount: true, // Recargar al montar el componente
},
},
});
const Providers = ({
session,
children,
}: {
session: SessionProviderProps['session'];
children: ReactNode;
}) => {
return (
<>
<QueryClientProvider client={queryClient}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange={false}
>
<NuqsAdapter>
<SessionProvider session={session}>{children}</SessionProvider>
</NuqsAdapter>
</ThemeProvider>
</QueryClientProvider>
</>
);
};
export default Providers;

View File

@@ -0,0 +1,52 @@
'use client';
import { Avatar, AvatarFallback, AvatarImage } from '@repo/shadcn/avatar';
import { Button } from '@repo/shadcn/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@repo/shadcn/dropdown-menu';
import { signOut, useSession } from 'next-auth/react';
import { redirect } from 'next/navigation';
export function UserNav() {
const { data: session } = useSession();
if (session) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-9 w-9 rounded-full">
<Avatar className="h-9 w-9">
<AvatarImage src={''} alt={session.user?.fullname ?? ''} />
<AvatarFallback>{session.user?.fullname?.[0]}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{session.user?.fullname}
</p>
<p className="text-xs leading-none text-muted-foreground">
{session.user?.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onClick={()=> redirect('/dashboard/profile')}>Perfil</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut()}>
Cerrar Sessión
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
}

View File

@@ -0,0 +1,50 @@
'use client';
import { Button } from '@repo/shadcn/button';
import { Modal } from '@repo/shadcn/modal';
import { useEffect, useState } from 'react';
interface AlertModalProps {
isOpen: boolean;
title?: string;
description?: string;
onClose: () => void;
onConfirm: () => void;
loading: boolean;
}
export const AlertModal: React.FC<AlertModalProps> = ({
title = 'Are you sure?',
description = 'This action cannot be undone.',
isOpen,
onClose,
onConfirm,
loading,
}) => {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) {
return null;
}
return (
<Modal
title={title}
description={description}
isOpen={isOpen}
onClose={onClose}
>
<div className="flex w-full items-center justify-end space-x-2 pt-6">
<Button disabled={loading} variant="outline" onClick={onClose}>
Cancelar
</Button>
<Button disabled={loading} variant="destructive" onClick={onConfirm}>
Continuar
</Button>
</div>
</Modal>
);
};

View File

@@ -0,0 +1,113 @@
'use client';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@repo/shadcn/collapsible';
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from '@repo/shadcn/sidebar';
import { ChevronRightIcon } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Icons } from './icons';
// import { useSession } from 'next-auth/react';
export function NavMain({
titleGroup,
items,
role
}: {
titleGroup: string,
role: string,
items: {
title: string;
url: string;
icon?: keyof typeof Icons;
isActive?: boolean;
items?: {
title: string;
url: string;
icon?: keyof typeof Icons;
role?: string[];
}[];
}[];
}) {
const pathname = usePathname();
// const { data: session } = useSession();
// const userRole = session?.user.role[0]?.rol ? session.user.role[0].rol :'';
// console.log(session?.user.role[0]?.rol);
return (
<SidebarGroup>
<SidebarGroupLabel>{titleGroup}</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => {
const Icon = item.icon ? Icons[item.icon] : Icons.logo;
return item?.items && item?.items?.length > 0 ? (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={pathname === item.url}
>
{item.icon && <Icon />}
<span>{item.title}</span>
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
subItem.role?.includes(role) &&
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
isActive={pathname === subItem.url}
>
<Link href={subItem.url}>
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
) : (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
tooltip={item.title}
isActive={pathname === item.url}
>
<Link href={item.url}>
<Icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroup>
);
}

View File

@@ -0,0 +1,111 @@
'use client';
import {
Folder,
Forward,
Frame,
PanelLeft,
PieChart,
Trash2,
type LucideIcon,
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@repo/shadcn/dropdown-menu';
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@repo/shadcn/sidebar';
const data = {
projects: [
{
name: 'Design Engineering',
url: '#',
icon: Frame,
},
{
name: 'Sales & Marketing',
url: '#',
icon: PieChart,
},
{
name: 'Travel',
url: '#',
icon: Map,
},
],
};
export function NavProjects({
projects,
}: {
projects: {
name: string;
url: string;
icon: LucideIcon;
}[];
}) {
const { isMobile } = useSidebar();
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<PanelLeft />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={isMobile ? 'bottom' : 'right'}
align={isMobile ? 'end' : 'start'}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<PanelLeft className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}

View File

@@ -0,0 +1,117 @@
'use client';
import {
AudioWaveform,
ChevronsUpDownIcon,
Command,
GalleryVerticalEnd,
PlusIcon,
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@repo/shadcn/dropdown-menu';
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@repo/shadcn/sidebar';
import * as React from 'react';
const data = {
teams: [
{
name: 'Acme Inc',
logo: GalleryVerticalEnd,
plan: 'Enterprise',
},
{
name: 'Acme Corp.',
logo: AudioWaveform,
plan: 'Startup',
},
{
name: 'Evil Corp.',
logo: Command,
plan: 'Free',
},
],
};
export function TeamSwitcher({
teams,
}: {
teams: {
name: string;
logo: React.ElementType;
plan: string;
}[];
}) {
const { isMobile } = useSidebar();
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
if (!activeTeam) {
return null;
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{activeTeam.name}</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDownIcon className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
align="start"
side={isMobile ? 'bottom' : 'right'}
sideOffset={4}
>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-xs border">
<team.logo className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="bg-background flex size-6 items-center justify-center rounded-md border">
<PlusIcon className="size-4" />
</div>
<div className="text-muted-foreground font-medium">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}