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,14 @@
import { LoginForm } from '@/feactures/auth/components/sigin-view';
const Page = () => {
return (
<div className="flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-3xl">
<LoginForm />
</div>
</div>
)
};
export default Page;

View File

@@ -0,0 +1,2 @@
import { handlers } from '@/lib/auth'; // Referring to the auth.ts we just created
export const { GET, POST } = handlers;

View File

@@ -0,0 +1,13 @@
import PageContainer from '@/components/layout/page-container';
import { SurveyBuilder } from '@/feactures/surveys/components/admin/survey-builder';
export default function CreateSurveyPage() {
return (
<PageContainer>
<div className="w-full">
<h1 className="text-2xl font-bold mb-6">Crear Nueva Encuesta</h1>
<SurveyBuilder />
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,13 @@
import PageContainer from "@/components/layout/page-container";
import { SurveyBuilder } from "@/feactures/surveys/components/admin/survey-builder";
export default function EditSurveyPage() {
return (
<PageContainer>
<div className="w-full">
<h1 className="text-2xl font-bold mb-6">Editar Encuesta</h1>
<SurveyBuilder />
</div>
</PageContainer>
)
}

View File

@@ -0,0 +1,37 @@
import PageContainer from '@/components/layout/page-container';
import SurveysAdminList from '@/feactures/surveys/components/admin/surveys-admin-list';
import { SurveysHeader } from '@/feactures/surveys/components/admin/surveys-header';
import SurveysTableAction from '@/feactures/surveys/components/admin/surveys-tables/survey-table-action';
import { searchParamsCache, serialize } from '@/feactures/surveys/utils/searchparams';
import { SearchParams } from 'nuqs';
type pageProps = {
searchParams: Promise<SearchParams>;
};
export default async function SurveyAdminPage(props: pageProps) {
const searchParams = await props.searchParams;
searchParamsCache.parse(searchParams);
const key = serialize({ ...searchParams });
const page = Number(searchParamsCache.get('page')) || 1;
const search = searchParamsCache.get('q');
const pageLimit = Number(searchParamsCache.get('limit')) || 10;
const type = searchParamsCache.get('type');
return (
<PageContainer scrollable={false}>
<div className="flex flex-1 flex-col space-y-4">
<SurveysHeader />
<SurveysTableAction />
<SurveysAdminList
initialPage={page}
initialSearch={search}
initialLimit={pageLimit}
initialType={type}
/>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,37 @@
import PageContainer from '@/components/layout/page-container';
import UsersAdminList from '@/feactures/users/components/admin/users-admin-list';
import { UsersHeader } from '@/feactures/users/components/admin/users-header';
import UsersTableAction from '@/feactures/users/components/admin/surveys-tables/users-table-action';
import { searchParamsCache, serialize } from '@/feactures/users/utils/searchparams';
import { SearchParams } from 'nuqs';
type pageProps = {
searchParams: Promise<SearchParams>;
};
export default async function SurveyAdminPage(props: pageProps) {
const searchParams = await props.searchParams;
searchParamsCache.parse(searchParams);
const key = serialize({ ...searchParams });
const page = Number(searchParamsCache.get('page')) || 1;
const search = searchParamsCache.get('q');
const pageLimit = Number(searchParamsCache.get('limit')) || 10;
const type = searchParamsCache.get('type');
return (
<PageContainer scrollable={false}>
<div className="flex flex-1 flex-col space-y-4">
<UsersHeader />
<UsersTableAction />
<UsersAdminList
initialPage={page}
initialSearch={search}
initialLimit={pageLimit}
initialType={type}
/>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,19 @@
import PageContainer from '@/components/layout/page-container';
const Page = () => {
return (
<PageContainer>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
En mantenimiento
</div>
</PageContainer>
// <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
// <div className="flex w-full max-w-sm flex-col gap-6">
// </div>
// </div>
);
};
export default Page;

View File

@@ -0,0 +1,24 @@
import PageContainer from '@/components/layout/page-container';
import { getSurveyByIdAction } from '@/feactures/surveys/actions/surveys-actions';
import { SurveyResponse } from '@/feactures/surveys/components/survey-response';
// La función ahora recibe 'params' con el parámetro dinámico 'id'
export default async function SurveyResponsePage({ params }: { params: { id: string } }) {
const { id } = await params; // Obtienes el id desde los params de la URL
if (!id || id === '') {
// Maneja el caso en el que no se proporciona un id
return null;
}
// Llamas a la función pasando el id dinámico
const data = await getSurveyByIdAction(Number(id));
return (
<PageContainer>
<div className="flex flex-1 flex-col space-y-4">
<SurveyResponse survey={data?.data!} />
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,21 @@
import PageContainer from '@/components/layout/page-container';
import { SurveyList } from '@/feactures/surveys/components/survey-list';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Encuestas - Fondemi',
description: 'Listado de encuestas disponibles',
};
export default function SurveysPage() {
return (
<PageContainer>
<div className="w-full">
<h1 className="text-2xl font-bold mb-6">Encuestas Disponibles</h1>
<SurveyList />
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,19 @@
import PageContainer from '@/components/layout/page-container';
import { SurveyStatistics } from '@/feactures/statistics/components/survey-statistics';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Estadísticas de Encuestas - Fondemi',
description: 'Análisis y estadísticas de las encuestas realizadas',
};
export default function SurveyStatisticsPage() {
return (
<PageContainer>
<div className="w-full">
<h1 className="text-2xl font-bold mb-6">Estadísticas de Encuestas</h1>
<SurveyStatistics />
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,11 @@
export default async function Page() {
return (
// <PageContainer>
<div className="flex justify-center items-center h-full">
<img src="../logo.png" alt="Image" className="w-1/4"/>
</div>
// </PageContainer>
);
}

View File

@@ -0,0 +1,31 @@
import { AppSidebar } from '@/components/layout/app-sidebar';
import Header from '@/components/layout/header';
import { SidebarInset, SidebarProvider } from '@repo/shadcn/sidebar';
import type { Metadata } from 'next';
import { cookies } from 'next/headers';
export const metadata: Metadata = {
title: 'Dashboard',
description: 'Sistema integral para Cajas de Ahorro',
};
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
// Persisting the sidebar state in the cookie.
const cookieStore = await cookies();
//const defaultOpen = cookieStore.get('sidebar:state')?.value === 'false';
return (
<SidebarProvider defaultOpen={true}>
<AppSidebar />
<SidebarInset>
<Header />
{/* page main content */}
{children}
{/* page main content ends */}
</SidebarInset>
</SidebarProvider>
);
}

View File

@@ -0,0 +1,12 @@
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function Dashboard() {
const session = await auth();
if (!session?.user) {
return redirect('/');
} else {
redirect('/dashboard/inicio');
}
}

View File

@@ -0,0 +1,17 @@
import PageContainer from "@/components/layout/page-container";
import {Profile} from '@/feactures/users/components/user-profile'
export default function ProfilePage() {
return (
<PageContainer>
<div className="w-full">
<h1 className="text-2xl font-bold">Perfil</h1>
<p className="text-muted-foreground mb-6">Aquí puede ver y editar sus datos de perfil</p>
<Profile />
</div>
</PageContainer>
);
}

37
apps/web/app/error.tsx Normal file
View File

@@ -0,0 +1,37 @@
'use client';
import { Button } from '@repo/shadcn/button';
import { RotateCw } from '@repo/shadcn/icon';
import { cn } from '@repo/shadcn/lib/utils';
import { useRouter } from 'next/navigation';
import { useEffect, useTransition } from 'react';
const Error = ({ error, reset }: { error: Error; reset: () => void }) => {
const router = useRouter();
const [isPending, startTransition] = useTransition();
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
return (
<div className="w-full flex flex-col min-h-dvh gap-9 items-center justify-center">
<p className="font-semibold">
Oh no, something went wrong... maybe refresh?
</p>
<Button
onClick={() => {
startTransition(() => {
reset();
router.refresh();
});
}}
>
Try Again
<RotateCw className={cn('size-5', isPending && 'animate-spin')} />
</Button>
</div>
);
};
export default Error;

Binary file not shown.

Binary file not shown.

64
apps/web/app/layout.tsx Normal file
View File

@@ -0,0 +1,64 @@
import Providers from '@/components/layout/providers';
import { auth } from '@/lib/auth';
import { cn } from '@repo/shadcn/lib/utils';
import '@repo/shadcn/shadcn.css';
import { Metadata } from 'next';
import localFont from 'next/font/local';
import NextTopLoader from 'nextjs-toploader';
import { ReactNode } from 'react';
import { Toaster } from 'sonner';
const GeistSans = localFont({
src: './fonts/GeistVF.woff',
variable: '--font-geist-sans',
});
const GeistMono = localFont({
src: './fonts/GeistMonoVF.woff',
variable: '--font-geist-mono',
});
export const metadata = {
metadataBase: new URL('https://turbo-npn.onrender.com'),
title: {
default: 'Caja de Ahorro',
template: '%s | Caja de Ahorro',
},
openGraph: {
type: 'website',
title: 'Caja de Ahorro',
description: 'Sistema integral para cajas de ahorro',
url: 'https://turbo-npn.onrender.com',
images: [
{
url: '/og-bg.png',
width: 1200,
height: 628,
alt: 'Turbo NPN Logo',
},
],
},
} satisfies Metadata;
const RootLayout = async ({
children,
}: Readonly<{
children: ReactNode;
}>) => {
const session = await auth();
return (
<html lang="en" suppressHydrationWarning>
<body
className={cn(GeistMono.variable, GeistSans.variable, 'antialiased')}
suppressHydrationWarning
>
<NextTopLoader showSpinner={false} />
<Providers session={session}>
<Toaster />
{children}
</Providers>
</body>
</html>
);
};
export default RootLayout;

View File

@@ -0,0 +1,29 @@
'use client';
import { Button } from '@repo/shadcn/button';
import { RotateCw } from '@repo/shadcn/icon';
import { cn } from '@repo/shadcn/lib/utils';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
const NotFound = () => {
const router = useRouter();
const [isPending, startTransition] = useTransition();
return (
<div className="w-full flex flex-col min-h-dvh gap-9 items-center justify-center">
<h1 className="font-semibold text-base">404 | Notfound</h1>
<p className="font-semibold">{"Oh no! This page doesn't exist."}</p>
<Button
onClick={() => {
startTransition(() => {
router.push('/');
});
}}
>
Return Home
<RotateCw className={cn('size-5', isPending && 'animate-spin')} />
</Button>
</div>
);
};
export default NotFound;

BIN
apps/web/app/og/mono.ttf Normal file

Binary file not shown.

62
apps/web/app/og/route.tsx Normal file
View File

@@ -0,0 +1,62 @@
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
export const runtime = 'edge';
export async function GET(req: NextRequest) {
const { searchParams } = req.nextUrl;
const title = searchParams.get('title');
const description = searchParams.get('description');
const font = fetch(new URL('./mono.ttf', import.meta.url)).then((res) =>
res.arrayBuffer(),
);
const fontData = await font;
return new ImageResponse(
(
<div
tw="flex h-full w-full bg-black text-white"
style={{ fontFamily: 'Geist Sans' }}
>
<div tw="flex border absolute border-stone-700 border-dashed inset-y-0 left-16 w-[1px]" />
<div tw="flex border absolute border-stone-700 border-dashed inset-y-0 right-16 w-[1px]" />
<div tw="flex border absolute border-stone-700 inset-x-0 h-[1px] top-16" />
<div tw="flex border absolute border-stone-700 inset-x-0 h-[1px] bottom-16" />
<div tw="flex absolute flex-row bottom-24 right-24 text-white"></div>
<div tw="flex flex-col absolute w-[896px] justify-center inset-32">
<div
tw="tracking-tight flex-grow-1 flex flex-col justify-center leading-[1.1]"
style={{
textWrap: 'balance',
fontWeight: 600,
fontSize: title && title.length > 20 ? 64 : 80,
letterSpacing: '-0.04em',
}}
>
{title}
</div>
<div
tw="text-[40px] leading-[1.5] flex-grow-1 text-stone-400"
style={{
fontWeight: 500,
textWrap: 'balance',
}}
>
{`${description?.slice(0, 100)}`}
</div>
</div>
</div>
),
{
width: 1200,
height: 628,
fonts: [
{
name: 'Jetbrains Mono',
data: fontData,
style: 'normal',
},
],
},
);
}

View File

@@ -0,0 +1,48 @@
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
// Image metadata
export const alt = `Opengraph Image`;
export const size = {
width: 800,
height: 400,
};
export const contentType = 'image/png';
export default async function Image() {
return new ImageResponse(
(
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: 24,
fontWeight: 600,
textAlign: 'left',
padding: 70,
color: 'red',
backgroundImage: 'linear-gradient(to right, #334d50, #cbcaa5)',
height: '100%',
width: '100%',
}}
>
<img
src={`http://localhost:3000/og-logo.png`}
alt="opengraph logo"
style={{
width: '400px',
height: '400px',
borderRadius: '50%',
objectFit: 'cover',
}}
/>
</div>
),
{
...size,
},
);
}

View File

@@ -0,0 +1,13 @@
import { LoginForm } from "@/feactures/auth/components/signup-view";
const Page = () => {
return (
<div className="flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-3xl">
<LoginForm />
</div>
</div>
)
}
export default Page;