base con autenticacion, registro, modulo encuestas
This commit is contained in:
14
apps/web/app/(auth)/page.tsx
Normal file
14
apps/web/app/(auth)/page.tsx
Normal 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;
|
||||
2
apps/web/app/api/auth/[...nextauth]/route.ts
Normal file
2
apps/web/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { handlers } from '@/lib/auth'; // Referring to the auth.ts we just created
|
||||
export const { GET, POST } = handlers;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
37
apps/web/app/dashboard/administracion/encuestas/page.tsx
Normal file
37
apps/web/app/dashboard/administracion/encuestas/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
37
apps/web/app/dashboard/administracion/usuario/page.tsx
Normal file
37
apps/web/app/dashboard/administracion/usuario/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
19
apps/web/app/dashboard/configuraciones/caja-ahorro/page.tsx
Normal file
19
apps/web/app/dashboard/configuraciones/caja-ahorro/page.tsx
Normal 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;
|
||||
24
apps/web/app/dashboard/encuestas/[id]/responder/page.tsx
Normal file
24
apps/web/app/dashboard/encuestas/[id]/responder/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
21
apps/web/app/dashboard/encuestas/page.tsx
Normal file
21
apps/web/app/dashboard/encuestas/page.tsx
Normal 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>
|
||||
|
||||
);
|
||||
}
|
||||
19
apps/web/app/dashboard/estadisticas/encuestas/page.tsx
Normal file
19
apps/web/app/dashboard/estadisticas/encuestas/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
apps/web/app/dashboard/inicio/page.tsx
Normal file
11
apps/web/app/dashboard/inicio/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
31
apps/web/app/dashboard/layout.tsx
Normal file
31
apps/web/app/dashboard/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
apps/web/app/dashboard/page.tsx
Normal file
12
apps/web/app/dashboard/page.tsx
Normal 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');
|
||||
}
|
||||
}
|
||||
17
apps/web/app/dashboard/profile/page.tsx
Normal file
17
apps/web/app/dashboard/profile/page.tsx
Normal 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
37
apps/web/app/error.tsx
Normal 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;
|
||||
BIN
apps/web/app/fonts/GeistMonoVF.woff
Normal file
BIN
apps/web/app/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
apps/web/app/fonts/GeistVF.woff
Normal file
BIN
apps/web/app/fonts/GeistVF.woff
Normal file
Binary file not shown.
64
apps/web/app/layout.tsx
Normal file
64
apps/web/app/layout.tsx
Normal 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;
|
||||
29
apps/web/app/not-found.tsx
Normal file
29
apps/web/app/not-found.tsx
Normal 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
BIN
apps/web/app/og/mono.ttf
Normal file
Binary file not shown.
62
apps/web/app/og/route.tsx
Normal file
62
apps/web/app/og/route.tsx
Normal 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
48
apps/web/app/opengraph-image.tsx
Normal file
48
apps/web/app/opengraph-image.tsx
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
13
apps/web/app/register/page.tsx
Normal file
13
apps/web/app/register/page.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user