mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
adding agent pages.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
nodeLinker: "node-modules"
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
import {useSearchParams} from "next/navigation";
|
||||
import {LoginForm} from "@/components/wrappers/Auth/Login/LoginForm/LoginForm";
|
||||
|
||||
import {useEffect, useRef} from "react";
|
||||
import {useSearchParams} from "next/navigation";
|
||||
|
||||
import {LoginForm} from "@/components/wrappers/Auth/Login/LoginForm/LoginForm";
|
||||
import {toast} from "sonner";
|
||||
|
||||
export default function SignInPage(props: {
|
||||
searchParams: { callbackUrl: string | undefined }
|
||||
searchParams: Promise<{ callbackUrl: string | undefined }>
|
||||
}) {
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>
|
||||
Agent 1
|
||||
</PageTitle>
|
||||
<PageActions>
|
||||
<Button>Backup</Button>
|
||||
<Button>Restore</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
|
||||
<PageDescription>My beautiful project!</PageDescription>
|
||||
|
||||
<PageContent>
|
||||
<div className="flex flex-row sm:justify-between gap-6 mb-8">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
Backups
|
||||
</CardHeader>
|
||||
<CardContent></CardContent>
|
||||
</Card>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
Success rate
|
||||
</CardHeader>
|
||||
<CardContent></CardContent>
|
||||
</Card>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
Last contact
|
||||
</CardHeader>
|
||||
<CardContent></CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<Tabs defaultValue="backup">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="backup">Backup</TabsTrigger>
|
||||
<TabsTrigger value="restore">Restore</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent className="flex flex-col gap-6" value="backup">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Reference</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>azertyuiop</TableCell>
|
||||
<TableCell><Badge variant="outline" className="text-orange-500 border-2 border-orange-500">ongoing</Badge></TableCell>
|
||||
<TableCell>28/01/25</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent className="flex flex-col gap-6" value="restore">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Backup reference</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>azertyuiop</TableCell>
|
||||
<TableCell>ongoing</TableCell>
|
||||
<TableCell>28/01/25</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
|
||||
</Tabs>
|
||||
</PageContent>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
return (
|
||||
<div>new agent</div>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,49 @@
|
||||
import {PageParams} from "@/types/next";
|
||||
import {prisma} from "@/prisma";
|
||||
import {AgentCard} from "@/components/wrappers/Agent/AgentCard/AgentCard";
|
||||
import {CardsWithPagination} from "@/components/wrappers/cards-with-pagination";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import Link from 'next/link'
|
||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
// const agents = await prisma.agent.findMany({})
|
||||
|
||||
const agents = [
|
||||
{"id": "467911b6-1234-4321-8765-111111111111", "name": "My Agent 1", "lastContact": "null"},
|
||||
{"id": "15f03a2a-f81e-4339-bb3d-222222222222", "name": "My Agent 2", "lastContact": "null"},
|
||||
{"id": "2e9b6e1a-5678-9012-3456-333333333333", "name": "My Agent 3", "lastContact": "null"},
|
||||
{"id": "8c637541-1111-2222-3333-444444444444", "name": "My Agent 4", "lastContact": "null"},
|
||||
{"id": "9e02f789-5555-6666-7777-555555555555", "name": "My Agent 5", "lastContact": "null"},
|
||||
{"id": "1a2b3c4d-8888-9999-0000-666666666666", "name": "My Agent 6", "lastContact": "null"},
|
||||
{"id": "7d8e9f0g-3333-4444-5555-777777777777", "name": "My Agent 7", "lastContact": "null"},
|
||||
{"id": "3f4g5h6j-9999-0000-1111-888888888888", "name": "My Agent 8", "lastContact": "null"},
|
||||
{"id": "5c6d7e8f-2222-3333-4444-999999999999", "name": "My Agent 9", "lastContact": "null"},
|
||||
{"id": "9a0b1c2d-6666-7777-8888-000000000000", "name": "My Agent 10", "lastContact": "null"}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div className="aspect-video rounded-xl bg-muted/50"/>
|
||||
<div className="aspect-video rounded-xl bg-muted/50"/>
|
||||
<div className="aspect-video rounded-xl bg-muted/50"/>
|
||||
</div>
|
||||
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
|
||||
</div>
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>
|
||||
Agents
|
||||
</PageTitle>
|
||||
<PageActions>
|
||||
<Link href={"/dashboard/agents/new"}>
|
||||
<Button>+ Create Agent</Button>
|
||||
</Link>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
|
||||
<PageContent>
|
||||
<CardsWithPagination
|
||||
data={agents}
|
||||
cardItem={AgentCard}
|
||||
cardsPerPage={4}
|
||||
numberOfColumns={1}
|
||||
/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
import {SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import React from "react";
|
||||
import {redirect} from "next/navigation";
|
||||
|
||||
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar"
|
||||
import {AppSidebar} from "@/components/wrappers/Dashboard/SideBar/app-sidebar";
|
||||
import {currentUser} from "@/auth/current-user";
|
||||
import {redirect} from "next/navigation";
|
||||
import {LoggedInButton} from "@/components/wrappers/Dashboard/LoggedInButton/LoggedInButton";
|
||||
import {ModeToggle} from "@/features/theme/ModeToggle";
|
||||
import {Header} from "@/features/layout/Header";
|
||||
|
||||
|
||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
||||
|
||||
const user = await currentUser()
|
||||
if (!user) {
|
||||
redirect('/login')
|
||||
}
|
||||
if (!user) redirect('/login')
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<AppSidebar/>
|
||||
<SidebarInset>
|
||||
<Header/>
|
||||
<main>{children}</main>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
|
||||
@@ -2,7 +2,6 @@ import {redirect} from "next/navigation";
|
||||
import {currentUser} from "@/auth/current-user";
|
||||
|
||||
|
||||
|
||||
export default async function Index() {
|
||||
const user = await currentUser()
|
||||
if (user) {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import {useSearchParams} from "next/navigation"
|
||||
|
||||
enum Error {
|
||||
Configuration = "Configuration",
|
||||
|
||||
+8
-6
@@ -1,8 +1,10 @@
|
||||
import React from "react";
|
||||
import type {Metadata} from "next";
|
||||
import {Inter} from "next/font/google";
|
||||
|
||||
import "./globals.css";
|
||||
import {Providers} from "./providers";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Inter} from "next/font/google";
|
||||
|
||||
|
||||
const inter = Inter({subsets: ["latin"]});
|
||||
@@ -19,11 +21,11 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={cn(inter.className, "h-full")}>
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
</body>
|
||||
<body className={cn(inter.className, "h-full")}>
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-5
@@ -1,17 +1,18 @@
|
||||
"use client"
|
||||
|
||||
import {PropsWithChildren} from "react";
|
||||
import {ThemeProvider} from "@/features/theme/theme-provider";
|
||||
|
||||
import {Toaster} from "@/components/ui/sonner";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from '@tanstack/react-query'
|
||||
import {QueryClient, QueryClientProvider,} from '@tanstack/react-query'
|
||||
import {SessionProvider} from "next-auth/react";
|
||||
|
||||
export type ProviderProps = PropsWithChildren<{}>;
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const ThemeProvider = dynamic(() => import('@/features/theme/theme-provider'), {ssr: false});
|
||||
|
||||
export const Providers = (props: ProviderProps) => {
|
||||
return (
|
||||
<SessionProvider>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
ENV YARN_VERSION=4.2.2
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
# install and use yarn 4.x
|
||||
RUN corepack enable && corepack prepare yarn@${YARN_VERSION}
|
||||
RUN yarn set version ${YARN_VERSION}
|
||||
|
||||
|
||||
FROM base AS deps
|
||||
|
||||
WORKDIR /app
|
||||
@@ -8,7 +16,7 @@ WORKDIR /app
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
if [ -f yarn.lock ]; then yarn; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
@@ -28,7 +36,6 @@ RUN chmod +x /app/docker/entrypoints/app-dev-entrypoint.sh
|
||||
ENTRYPOINT ["sh","/app/docker/entrypoints/app-dev-entrypoint.sh"]
|
||||
|
||||
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
|
||||
|
||||
Generated
-8241
File diff suppressed because it is too large
Load Diff
+11
-7
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 80",
|
||||
"dev": "next dev --turbopack -p 80",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
@@ -51,14 +51,14 @@
|
||||
"embla-carousel-react": "^8.3.1",
|
||||
"input-otp": "^1.4.0",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "14.2.16",
|
||||
"next": "15.0.3",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-intl": "^3.24.0",
|
||||
"next-safe-action": "^7.4.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18",
|
||||
"react": "19.0.0-rc-66855b96-20241106",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18",
|
||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||
"react-hook-form": "^7.53.1",
|
||||
"react-resizable-panels": "^2.1.6",
|
||||
"react-twc": "^1.4.2",
|
||||
@@ -71,13 +71,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.16",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"postcss": "^8",
|
||||
"prisma": "^5.21.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,3 +70,42 @@ model User {
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Agent {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
lastContact DateTime? @map("last_contact")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
}
|
||||
|
||||
model Database {
|
||||
id String @id @default(cuid())
|
||||
agentId String @map("agent_id")
|
||||
name String
|
||||
description String?
|
||||
backupPolicy String? @map("backup_policy")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
}
|
||||
|
||||
enum Status {
|
||||
waiting
|
||||
ongoing
|
||||
failed
|
||||
success
|
||||
}
|
||||
|
||||
model Backup {
|
||||
id String @id @default(cuid())
|
||||
databaseId String @map("database_id")
|
||||
status Status @default(waiting)
|
||||
file String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
}
|
||||
|
||||
model Restore {
|
||||
id String @id @default(cuid())
|
||||
backupId String @map("backup_id")
|
||||
status Status @default(waiting)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
|
||||
export type agentCardProps = {
|
||||
id: string;
|
||||
name: string;
|
||||
lastContact: string;
|
||||
}
|
||||
|
||||
export const AgentCard = (props: agentCardProps) => {
|
||||
|
||||
const {id, name, lastContact} = props;
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/agents/${id}`}>
|
||||
<Card>
|
||||
<CardHeader>{name}</CardHeader>
|
||||
<CardContent>Agent's Description</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -59,6 +59,7 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
@@ -73,6 +74,7 @@ export const LoginForm = (props: loginFormProps) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ export const RegisterForm = (props: registerFormProps) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
@@ -77,6 +78,7 @@ export const RegisterForm = (props: registerFormProps) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
@@ -91,6 +93,7 @@ export const RegisterForm = (props: registerFormProps) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex">Password
|
||||
@@ -115,6 +118,7 @@ export const RegisterForm = (props: registerFormProps) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password Confirmation</FormLabel>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar"
|
||||
import {currentUser} from "@/auth/current-user";
|
||||
import {LoggedInDropdown} from "@/components/wrappers/Dashboard/LoggedInDropdown/LoggedInDropdown";
|
||||
|
||||
@@ -9,23 +9,16 @@ import {
|
||||
} from "@/components/ui/sidebar"
|
||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Calendar,
|
||||
ChartArea,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Home,
|
||||
Inbox,
|
||||
Search,
|
||||
Settings,
|
||||
ShieldHalf,
|
||||
User2
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import {signOutAction} from "@/features/auth/auth.action";
|
||||
import {UserAvatar} from "@/components/wrappers/Dashboard/UserAvatar/UserAvatar";
|
||||
import {LoggedInDropdown} from "@/components/wrappers/Dashboard/LoggedInDropdown/LoggedInDropdown";
|
||||
|
||||
import {LoggedInButton} from "@/components/wrappers/Dashboard/LoggedInButton/LoggedInButton";
|
||||
import {Button, buttonVariants} from "@/components/ui/button"
|
||||
import {buttonVariants} from "@/components/ui/button"
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -65,10 +58,10 @@ export function AppSidebar() {
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton>
|
||||
Select Workspace
|
||||
<ChevronDown className="ml-auto"/>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButton>
|
||||
Select Workspace
|
||||
<ChevronDown className="ml-auto"/>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[--radix-popper-anchor-width]">
|
||||
<DropdownMenuItem>
|
||||
@@ -89,15 +82,18 @@ export function AppSidebar() {
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild >
|
||||
<SidebarMenuButton asChild>
|
||||
<Link
|
||||
className={cn(buttonVariants({size: "lg", variant: "ghost"}), "justify-start p-0")}
|
||||
className={cn(buttonVariants({
|
||||
size: "lg",
|
||||
variant: "ghost"
|
||||
}), "justify-start p-0")}
|
||||
href={`${BASE_URL}/${item.url}`}>
|
||||
<item.icon/>
|
||||
<span>{item.title}</span>
|
||||
<item.icon/>
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuAction className="peer-data-[active=true]/menu-button:opacity-100" />
|
||||
<SidebarMenuAction className="peer-data-[active=true]/menu-button:opacity-100"/>
|
||||
|
||||
</SidebarMenuItem>
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
'use client'
|
||||
|
||||
import React, {useState} from 'react'
|
||||
import {Card} from "@/components/ui/card"
|
||||
import {cn} from "@/lib/utils";
|
||||
import {
|
||||
Pagination, PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext, PaginationPrevious
|
||||
} from "@/components/ui/pagination";
|
||||
|
||||
export type cardsWithPaginationProps = {
|
||||
data: Array<{}>;
|
||||
cardItem: React.ComponentType;
|
||||
cardsPerPage?: number
|
||||
numberOfColumns?: number
|
||||
}
|
||||
|
||||
|
||||
export const CardsWithPagination = (props: cardsWithPaginationProps) => {
|
||||
|
||||
const {data, cardItem, cardsPerPage = 5, numberOfColumns = 1} = props
|
||||
|
||||
const CardItem = cardItem
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const totalPages = Math.ceil(data.length / cardsPerPage)
|
||||
|
||||
const indexOfLastCard = currentPage * cardsPerPage
|
||||
const indexOfFirstCard = indexOfLastCard - cardsPerPage
|
||||
const currentCards = data.slice(indexOfFirstCard, indexOfLastCard)
|
||||
|
||||
const handlePageChange = (pageNumber: number) => {
|
||||
setCurrentPage(pageNumber)
|
||||
}
|
||||
|
||||
const renderPaginationItems = () => {
|
||||
const items = []
|
||||
const maxVisiblePages = 3
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
items.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
onClick={() => handlePageChange(i)}
|
||||
isActive={currentPage === i}
|
||||
>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (currentPage <= 2) {
|
||||
for (let i = 1; i <= maxVisiblePages; i++) {
|
||||
items.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
onClick={() => handlePageChange(i)}
|
||||
isActive={currentPage === i}
|
||||
>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
items.push(
|
||||
<PaginationItem key="ellipsis1">
|
||||
<PaginationEllipsis/>
|
||||
</PaginationItem>
|
||||
)
|
||||
} else if (currentPage >= totalPages - 1) {
|
||||
items.push(
|
||||
<PaginationItem key="ellipsis2">
|
||||
<PaginationEllipsis/>
|
||||
</PaginationItem>
|
||||
)
|
||||
for (let i = totalPages - 2; i <= totalPages; i++) {
|
||||
items.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
onClick={() => handlePageChange(i)}
|
||||
isActive={currentPage === i}
|
||||
>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items.push(
|
||||
<PaginationItem key="ellipsis3">
|
||||
<PaginationEllipsis/>
|
||||
</PaginationItem>
|
||||
)
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
items.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
onClick={() => handlePageChange(i)}
|
||||
isActive={currentPage === i}
|
||||
>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
items.push(
|
||||
<PaginationItem key="ellipsis4">
|
||||
<PaginationEllipsis/>
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className={cn(`grid auto-rows-min gap-4 md:grid-cols-${numberOfColumns}`)}>
|
||||
{currentCards.map((card, key) => (
|
||||
<CardItem key={key} {...card}/>
|
||||
))}
|
||||
</div>
|
||||
<Pagination className="mt-8">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => handlePageChange(Math.max(1, currentPage - 1))}
|
||||
aria-disabled={currentPage === 1}
|
||||
tabIndex={currentPage === 1 ? -1 : 0}
|
||||
/>
|
||||
</PaginationItem>
|
||||
{renderPaginationItems()}
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => handlePageChange(Math.min(totalPages, currentPage + 1))}
|
||||
aria-disabled={currentPage === totalPages}
|
||||
tabIndex={currentPage === totalPages ? -1 : 0}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server"
|
||||
import {signIn, signOut} from "@/auth/auth";
|
||||
|
||||
import {redirect} from "next/navigation";
|
||||
import {signIn, signOut} from "@/auth/auth";
|
||||
|
||||
export const signOutAction = async () => {
|
||||
await signOut({redirectTo: '/', redirect: true})
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import Image from "next/image"
|
||||
import {Layout} from "@/components/layout";
|
||||
import Link from "next/link";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {currentUser} from "@/auth/current-user";
|
||||
import {notFound} from "next/navigation";
|
||||
import {buttonVariants} from "@/components/ui/button";
|
||||
|
||||
import {currentUser} from "@/auth/current-user";
|
||||
import {LoggedInButton} from "@/components/wrappers/Dashboard/LoggedInButton/LoggedInButton";
|
||||
import {SidebarTrigger} from "@/components/ui/sidebar";
|
||||
import {ModeToggle} from "@/features/theme/ModeToggle";
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import {PropsWithChildren} from 'react';
|
||||
|
||||
export const Page = ({children}: PropsWithChildren<{}>) => {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-4 px-10 py-6">{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageHeader = ({children}: PropsWithChildren<{}>) => {
|
||||
return (
|
||||
<div className="flex justify-between">{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageTitle = ({children}: PropsWithChildren<{}>) => {
|
||||
return (
|
||||
<h1 className="text-3xl font-bold mb-6">{children}</h1>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageDescription = ({children}: PropsWithChildren<{}>) => {
|
||||
return (
|
||||
<h2 className="text-s mb-6 text-gray-700">{children}</h2>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageActions = ({children}: PropsWithChildren<{}>) => {
|
||||
return (
|
||||
<h1 className="flex gap-4">{children}</h1>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const PageContent = ({children}: PropsWithChildren<{}>) => {
|
||||
return (
|
||||
<div className="">{children}</div>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import {Moon, Sun} from "lucide-react"
|
||||
import {useTheme} from "next-themes"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
const {setTheme} = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"/>
|
||||
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"/>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -4,6 +4,6 @@ import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
export default function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
+11
-11
@@ -3,17 +3,17 @@ import * as React from "react"
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
return !!isMobile
|
||||
}
|
||||
|
||||
+131
-131
@@ -4,191 +4,191 @@
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? {...t, ...action.toast} : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
case "DISMISS_TOAST": {
|
||||
const {toastId} = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
let memoryState: State = {toasts: []}
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
function toast({...props}: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: {...props, id},
|
||||
})
|
||||
const dismiss = () => dispatch({type: "DISMISS_TOAST", toastId: id})
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({type: "DISMISS_TOAST", toastId}),
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
export {useToast, toast}
|
||||
|
||||
+1
-1
@@ -6,5 +6,5 @@ export type LayoutParams<T extends Record<string, string | string[]>> = {
|
||||
|
||||
export type PageParams<T extends Record<string, string | string[]>> = {
|
||||
params: T;
|
||||
searchParams: {[key: string]: string | string[] | undefined};
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
};
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"strict": false
|
||||
"strict": false,
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
||||
Reference in New Issue
Block a user