chore(ui): apply Biome format to time badge and headers

This commit is contained in:
Aathil Felix
2025-11-01 19:09:58 +05:30
parent 0f100c7bc8
commit 53b66e41e2
3 changed files with 1485 additions and 1442 deletions

View File

@@ -1,13 +1,13 @@
import { import {
AlertTriangle, AlertTriangle,
ArrowUpDown, ArrowUpDown,
BookIcon, BookIcon,
ExternalLinkIcon, ExternalLinkIcon,
FolderInput, FolderInput,
Loader2, Loader2,
MoreHorizontalIcon, MoreHorizontalIcon,
Search, Search,
TrashIcon, TrashIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@@ -16,41 +16,41 @@ import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
AlertDialogContent, AlertDialogContent,
AlertDialogDescription, AlertDialogDescription,
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input"; import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { HandleProject } from "./handle-project"; import { HandleProject } from "./handle-project";
@@ -58,455 +58,470 @@ import { ProjectEnvironment } from "./project-environment";
import { TimeBadge } from "@/components/ui/time-badge"; import { TimeBadge } from "@/components/ui/time-badge";
export const ShowProjects = () => { export const ShowProjects = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const { data, isLoading } = api.project.all.useQuery(); const { data, isLoading } = api.project.all.useQuery();
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation(); const { mutateAsync } = api.project.remove.useMutation();
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [sortBy, setSortBy] = useState<string>(() => { const [sortBy, setSortBy] = useState<string>(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
return localStorage.getItem("projectsSort") || "createdAt-desc"; return localStorage.getItem("projectsSort") || "createdAt-desc";
} }
return "createdAt-desc"; return "createdAt-desc";
}); });
useEffect(() => { useEffect(() => {
localStorage.setItem("projectsSort", sortBy); localStorage.setItem("projectsSort", sortBy);
}, [sortBy]); }, [sortBy]);
const filteredProjects = useMemo(() => { const filteredProjects = useMemo(() => {
if (!data) return []; if (!data) return [];
// First filter by search query // First filter by search query
const filtered = data.filter( const filtered = data.filter(
(project) => (project) =>
project.name.toLowerCase().includes(searchQuery.toLowerCase()) || project.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
project.description?.toLowerCase().includes(searchQuery.toLowerCase()) project.description?.toLowerCase().includes(searchQuery.toLowerCase()),
); );
// Then sort the filtered results // Then sort the filtered results
const [field, direction] = sortBy.split("-"); const [field, direction] = sortBy.split("-");
return [...filtered].sort((a, b) => { return [...filtered].sort((a, b) => {
let comparison = 0; let comparison = 0;
switch (field) { switch (field) {
case "name": case "name":
comparison = a.name.localeCompare(b.name); comparison = a.name.localeCompare(b.name);
break; break;
case "createdAt": case "createdAt":
comparison = comparison =
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break; break;
case "services": { case "services": {
const aTotalServices = a.environments.reduce((total, env) => { const aTotalServices = a.environments.reduce((total, env) => {
return ( return (
total + total +
(env.applications?.length || 0) + (env.applications?.length || 0) +
(env.mariadb?.length || 0) + (env.mariadb?.length || 0) +
(env.mongo?.length || 0) + (env.mongo?.length || 0) +
(env.mysql?.length || 0) + (env.mysql?.length || 0) +
(env.postgres?.length || 0) + (env.postgres?.length || 0) +
(env.redis?.length || 0) + (env.redis?.length || 0) +
(env.compose?.length || 0) (env.compose?.length || 0)
); );
}, 0); }, 0);
const bTotalServices = b.environments.reduce((total, env) => { const bTotalServices = b.environments.reduce((total, env) => {
return ( return (
total + total +
(env.applications?.length || 0) + (env.applications?.length || 0) +
(env.mariadb?.length || 0) + (env.mariadb?.length || 0) +
(env.mongo?.length || 0) + (env.mongo?.length || 0) +
(env.mysql?.length || 0) + (env.mysql?.length || 0) +
(env.postgres?.length || 0) + (env.postgres?.length || 0) +
(env.redis?.length || 0) + (env.redis?.length || 0) +
(env.compose?.length || 0) (env.compose?.length || 0)
); );
}, 0); }, 0);
comparison = aTotalServices - bTotalServices; comparison = aTotalServices - bTotalServices;
break; break;
} }
default: default:
comparison = 0; comparison = 0;
} }
return direction === "asc" ? comparison : -comparison; return direction === "asc" ? comparison : -comparison;
}); });
}, [data, searchQuery, sortBy]); }, [data, searchQuery, sortBy]);
return ( return (
<> <>
<BreadcrumbSidebar <BreadcrumbSidebar
list={[{ name: "Projects", href: "/dashboard/projects" }]} list={[{ name: "Projects", href: "/dashboard/projects" }]}
/> />
<div className="absolute top-5 right-5"> <div className="absolute top-5 right-5">
<TimeBadge /> <TimeBadge />
</div> </div>
<div className="w-full"> <div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl "> <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md "> <div className="rounded-xl bg-background shadow-md ">
<div className="flex justify-between gap-4 w-full items-center flex-wrap p-6"> <div className="flex justify-between gap-4 w-full items-center flex-wrap p-6">
<CardHeader className="p-0"> <CardHeader className="p-0">
<CardTitle className="text-xl flex flex-row gap-2"> <CardTitle className="text-xl flex flex-row gap-2">
<FolderInput className="size-6 text-muted-foreground self-center" /> <FolderInput className="size-6 text-muted-foreground self-center" />
Projects Projects
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Create and manage your projects Create and manage your projects
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
{(auth?.role === "owner" || auth?.canCreateProjects) && ( {(auth?.role === "owner" || auth?.canCreateProjects) && (
<div className=""> <div className="">
<HandleProject /> <HandleProject />
</div> </div>
)} )}
</div> </div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]"> <CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
{isLoading ? ( {isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[60vh]"> <div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[60vh]">
<span>Loading...</span> <span>Loading...</span>
<Loader2 className="animate-spin size-4" /> <Loader2 className="animate-spin size-4" />
</div> </div>
) : ( ) : (
<> <>
<div className="flex max-sm:flex-col gap-4 items-center w-full"> <div className="flex max-sm:flex-col gap-4 items-center w-full">
<div className="flex-1 relative max-sm:w-full"> <div className="flex-1 relative max-sm:w-full">
<FocusShortcutInput <FocusShortcutInput
placeholder="Filter projects..." placeholder="Filter projects..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pr-10" className="pr-10"
/> />
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" /> <Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
</div> </div>
<div className="flex items-center gap-2 min-w-48 max-sm:w-full"> <div className="flex items-center gap-2 min-w-48 max-sm:w-full">
<ArrowUpDown className="size-4 text-muted-foreground" /> <ArrowUpDown className="size-4 text-muted-foreground" />
<Select value={sortBy} onValueChange={setSortBy}> <Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="Sort by..." /> <SelectValue placeholder="Sort by..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="name-asc">Name (A-Z)</SelectItem> <SelectItem value="name-asc">Name (A-Z)</SelectItem>
<SelectItem value="name-desc">Name (Z-A)</SelectItem> <SelectItem value="name-desc">Name (Z-A)</SelectItem>
<SelectItem value="createdAt-desc"> <SelectItem value="createdAt-desc">
Newest first Newest first
</SelectItem> </SelectItem>
<SelectItem value="createdAt-asc"> <SelectItem value="createdAt-asc">
Oldest first Oldest first
</SelectItem> </SelectItem>
<SelectItem value="services-desc"> <SelectItem value="services-desc">
Most services Most services
</SelectItem> </SelectItem>
<SelectItem value="services-asc"> <SelectItem value="services-asc">
Least services Least services
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div> </div>
{filteredProjects?.length === 0 && ( {filteredProjects?.length === 0 && (
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4"> <div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
<FolderInput className="size-8 self-center text-muted-foreground" /> <FolderInput className="size-8 self-center text-muted-foreground" />
<span className="text-center font-medium text-muted-foreground"> <span className="text-center font-medium text-muted-foreground">
No projects found No projects found
</span> </span>
</div> </div>
)} )}
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5"> <div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
{filteredProjects?.map((project) => { {filteredProjects?.map((project) => {
const emptyServices = project?.environments const emptyServices = project?.environments
.map( .map(
(env) => (env) =>
env.applications.length === 0 && env.applications.length === 0 &&
env.mariadb.length === 0 && env.mariadb.length === 0 &&
env.mongo.length === 0 && env.mongo.length === 0 &&
env.mysql.length === 0 && env.mysql.length === 0 &&
env.postgres.length === 0 && env.postgres.length === 0 &&
env.redis.length === 0 && env.redis.length === 0 &&
env.applications.length === 0 && env.applications.length === 0 &&
env.compose.length === 0 env.compose.length === 0,
) )
.every(Boolean); .every(Boolean);
const totalServices = project?.environments const totalServices = project?.environments
.map( .map(
(env) => (env) =>
env.mariadb.length + env.mariadb.length +
env.mongo.length + env.mongo.length +
env.mysql.length + env.mysql.length +
env.postgres.length + env.postgres.length +
env.redis.length + env.redis.length +
env.applications.length + env.applications.length +
env.compose.length env.compose.length,
) )
.reduce((acc, curr) => acc + curr, 0); .reduce((acc, curr) => acc + curr, 0);
const haveServicesWithDomains = project?.environments const haveServicesWithDomains = project?.environments
.map( .map(
(env) => (env) =>
env.applications.length > 0 || env.applications.length > 0 ||
env.compose.length > 0 env.compose.length > 0,
) )
.some(Boolean); .some(Boolean);
return ( return (
<div <div
key={project.projectId} key={project.projectId}
className="w-full lg:max-w-md"> className="w-full lg:max-w-md"
<Link >
href={`/dashboard/project/${project.projectId}/environment/${project?.environments?.[0]?.environmentId}`}> <Link
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border"> href={`/dashboard/project/${project.projectId}/environment/${project?.environments?.[0]?.environmentId}`}
{haveServicesWithDomains ? ( >
<DropdownMenu> <Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
<DropdownMenuTrigger asChild> {haveServicesWithDomains ? (
<Button <DropdownMenu>
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100" <DropdownMenuTrigger asChild>
size="sm" <Button
variant="default"> className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
<ExternalLinkIcon className="size-3.5" /> size="sm"
</Button> variant="default"
</DropdownMenuTrigger> >
<DropdownMenuContent <ExternalLinkIcon className="size-3.5" />
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]" </Button>
onClick={(e) => e.stopPropagation()}> </DropdownMenuTrigger>
{project.environments.some( <DropdownMenuContent
(env) => env.applications.length > 0 className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
) && ( onClick={(e) => e.stopPropagation()}
<DropdownMenuGroup> >
<DropdownMenuLabel> {project.environments.some(
Applications (env) => env.applications.length > 0,
</DropdownMenuLabel> ) && (
{project.environments.map((env) => <DropdownMenuGroup>
env.applications.map((app) => ( <DropdownMenuLabel>
<div key={app.applicationId}> Applications
<DropdownMenuSeparator /> </DropdownMenuLabel>
<DropdownMenuGroup> {project.environments.map((env) =>
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between"> env.applications.map((app) => (
{app.name} <div key={app.applicationId}>
<StatusTooltip <DropdownMenuSeparator />
status={ <DropdownMenuGroup>
app.applicationStatus <DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
} {app.name}
/> <StatusTooltip
</DropdownMenuLabel> status={
<DropdownMenuSeparator /> app.applicationStatus
{app.domains.map((domain) => ( }
<DropdownMenuItem />
key={domain.domainId} </DropdownMenuLabel>
asChild> <DropdownMenuSeparator />
<Link {app.domains.map((domain) => (
className="space-x-4 text-xs cursor-pointer justify-between" <DropdownMenuItem
target="_blank" key={domain.domainId}
href={`${ asChild
domain.https >
? "https" <Link
: "http" className="space-x-4 text-xs cursor-pointer justify-between"
}://${domain.host}${ target="_blank"
domain.path href={`${
}`}> domain.https
<span className="truncate"> ? "https"
{domain.host} : "http"
</span> }://${domain.host}${
<ExternalLinkIcon className="size-4 shrink-0" /> domain.path
</Link> }`}
</DropdownMenuItem> >
))} <span className="truncate">
</DropdownMenuGroup> {domain.host}
</div> </span>
)) <ExternalLinkIcon className="size-4 shrink-0" />
)} </Link>
</DropdownMenuGroup> </DropdownMenuItem>
)} ))}
{project.environments.some( </DropdownMenuGroup>
(env) => env.compose.length > 0 </div>
) && ( )),
<DropdownMenuGroup> )}
<DropdownMenuLabel> </DropdownMenuGroup>
Compose )}
</DropdownMenuLabel> {project.environments.some(
{project.environments.map((env) => (env) => env.compose.length > 0,
env.compose.map((comp) => ( ) && (
<div key={comp.composeId}> <DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuLabel>
<DropdownMenuGroup> Compose
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between"> </DropdownMenuLabel>
{comp.name} {project.environments.map((env) =>
<StatusTooltip env.compose.map((comp) => (
status={comp.composeStatus} <div key={comp.composeId}>
/> <DropdownMenuSeparator />
</DropdownMenuLabel> <DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
{comp.domains.map((domain) => ( {comp.name}
<DropdownMenuItem <StatusTooltip
key={domain.domainId} status={comp.composeStatus}
asChild> />
<Link </DropdownMenuLabel>
className="space-x-4 text-xs cursor-pointer justify-between" <DropdownMenuSeparator />
target="_blank" {comp.domains.map((domain) => (
href={`${ <DropdownMenuItem
domain.https key={domain.domainId}
? "https" asChild
: "http" >
}://${domain.host}${ <Link
domain.path className="space-x-4 text-xs cursor-pointer justify-between"
}`}> target="_blank"
<span className="truncate"> href={`${
{domain.host} domain.https
</span> ? "https"
<ExternalLinkIcon className="size-4 shrink-0" /> : "http"
</Link> }://${domain.host}${
</DropdownMenuItem> domain.path
))} }`}
</DropdownMenuGroup> >
</div> <span className="truncate">
)) {domain.host}
)} </span>
</DropdownMenuGroup> <ExternalLinkIcon className="size-4 shrink-0" />
)} </Link>
</DropdownMenuContent> </DropdownMenuItem>
</DropdownMenu> ))}
) : null} </DropdownMenuGroup>
<CardHeader> </div>
<CardTitle className="flex items-center justify-between gap-2"> )),
<span className="flex flex-col gap-1.5"> )}
<div className="flex items-center gap-2"> </DropdownMenuGroup>
<BookIcon className="size-4 text-muted-foreground" /> )}
<span className="text-base font-medium leading-none"> </DropdownMenuContent>
{project.name} </DropdownMenu>
</span> ) : null}
</div> <CardHeader>
<CardTitle className="flex items-center justify-between gap-2">
<span className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<BookIcon className="size-4 text-muted-foreground" />
<span className="text-base font-medium leading-none">
{project.name}
</span>
</div>
<span className="text-sm font-medium text-muted-foreground"> <span className="text-sm font-medium text-muted-foreground">
{project.description} {project.description}
</span> </span>
</span> </span>
<div className="flex self-start space-x-1"> <div className="flex self-start space-x-1">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="px-2"> className="px-2"
<MoreHorizontalIcon className="size-5" /> >
</Button> <MoreHorizontalIcon className="size-5" />
</DropdownMenuTrigger> </Button>
<DropdownMenuContent </DropdownMenuTrigger>
className="w-[200px] space-y-2 overflow-y-auto max-h-[280px]" <DropdownMenuContent
onClick={(e) => e.stopPropagation()}> className="w-[200px] space-y-2 overflow-y-auto max-h-[280px]"
<DropdownMenuLabel className="font-normal"> onClick={(e) => e.stopPropagation()}
Actions >
</DropdownMenuLabel> <DropdownMenuLabel className="font-normal">
<div Actions
onClick={(e) => e.stopPropagation()}> </DropdownMenuLabel>
<ProjectEnvironment <div
projectId={project.projectId} onClick={(e) => e.stopPropagation()}
/> >
</div> <ProjectEnvironment
<div projectId={project.projectId}
onClick={(e) => e.stopPropagation()}> />
<HandleProject </div>
projectId={project.projectId} <div
/> onClick={(e) => e.stopPropagation()}
</div> >
<HandleProject
projectId={project.projectId}
/>
</div>
<div <div
onClick={(e) => e.stopPropagation()}> onClick={(e) => e.stopPropagation()}
{(auth?.role === "owner" || >
auth?.canDeleteProjects) && ( {(auth?.role === "owner" ||
<AlertDialog> auth?.canDeleteProjects) && (
<AlertDialogTrigger className="w-full"> <AlertDialog>
<DropdownMenuItem <AlertDialogTrigger className="w-full">
className="w-full cursor-pointer space-x-3" <DropdownMenuItem
onSelect={(e) => className="w-full cursor-pointer space-x-3"
e.preventDefault() onSelect={(e) =>
}> e.preventDefault()
<TrashIcon className="size-4" /> }
<span>Delete</span> >
</DropdownMenuItem> <TrashIcon className="size-4" />
</AlertDialogTrigger> <span>Delete</span>
<AlertDialogContent> </DropdownMenuItem>
<AlertDialogHeader> </AlertDialogTrigger>
<AlertDialogTitle> <AlertDialogContent>
Are you sure to delete this <AlertDialogHeader>
project? <AlertDialogTitle>
</AlertDialogTitle> Are you sure to delete this
{!emptyServices ? ( project?
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950"> </AlertDialogTitle>
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" /> {!emptyServices ? (
<span className="text-sm text-yellow-600 dark:text-yellow-400"> <div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
You have active <AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
services, please delete <span className="text-sm text-yellow-600 dark:text-yellow-400">
them first You have active
</span> services, please delete
</div> them first
) : ( </span>
<AlertDialogDescription> </div>
This action cannot be ) : (
undone <AlertDialogDescription>
</AlertDialogDescription> This action cannot be
)} undone
</AlertDialogHeader> </AlertDialogDescription>
<AlertDialogFooter> )}
<AlertDialogCancel> </AlertDialogHeader>
Cancel <AlertDialogFooter>
</AlertDialogCancel> <AlertDialogCancel>
<AlertDialogAction Cancel
disabled={!emptyServices} </AlertDialogCancel>
onClick={async () => { <AlertDialogAction
await mutateAsync({ disabled={!emptyServices}
projectId: onClick={async () => {
project.projectId, await mutateAsync({
}) projectId:
.then(() => { project.projectId,
toast.success( })
"Project deleted successfully" .then(() => {
); toast.success(
}) "Project deleted successfully",
.catch(() => { );
toast.error( })
"Error deleting this project" .catch(() => {
); toast.error(
}) "Error deleting this project",
.finally(() => { );
utils.project.all.invalidate(); })
}); .finally(() => {
}}> utils.project.all.invalidate();
Delete });
</AlertDialogAction> }}
</AlertDialogFooter> >
</AlertDialogContent> Delete
</AlertDialog> </AlertDialogAction>
)} </AlertDialogFooter>
</div> </AlertDialogContent>
</DropdownMenuContent> </AlertDialog>
</DropdownMenu> )}
</div> </div>
</CardTitle> </DropdownMenuContent>
</CardHeader> </DropdownMenu>
<CardFooter className="pt-4"> </div>
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4"> </CardTitle>
<DateTooltip date={project.createdAt}> </CardHeader>
Created <CardFooter className="pt-4">
</DateTooltip> <div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<span> <DateTooltip date={project.createdAt}>
{totalServices}{" "} Created
{totalServices === 1 </DateTooltip>
? "service" <span>
: "services"} {totalServices}{" "}
</span> {totalServices === 1
</div> ? "service"
</CardFooter> : "services"}
</Card> </span>
</Link> </div>
</div> </CardFooter>
); </Card>
})} </Link>
</div> </div>
</> );
)} })}
</CardContent> </div>
</div> </>
</Card> )}
</div> </CardContent>
</> </div>
); </Card>
</div>
</>
);
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -4,57 +4,57 @@ import { useEffect, useState } from "react";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
export function TimeBadge() { export function TimeBadge() {
const { data: serverTime } = api.server.getServerTime.useQuery(undefined, { const { data: serverTime } = api.server.getServerTime.useQuery(undefined, {
refetchInterval: 60000, // Refetch every 60 seconds refetchInterval: 60000, // Refetch every 60 seconds
}); });
const [time, setTime] = useState<Date | null>(null); const [time, setTime] = useState<Date | null>(null);
useEffect(() => { useEffect(() => {
if (serverTime?.time) { if (serverTime?.time) {
setTime(new Date(serverTime.time)); setTime(new Date(serverTime.time));
} }
}, [serverTime]); }, [serverTime]);
useEffect(() => { useEffect(() => {
const timer = setInterval(() => { const timer = setInterval(() => {
setTime((prevTime) => { setTime((prevTime) => {
if (!prevTime) return null; if (!prevTime) return null;
const newTime = new Date(prevTime.getTime() + 1000); const newTime = new Date(prevTime.getTime() + 1000);
return newTime; return newTime;
}); });
}, 1000); }, 1000);
return () => { return () => {
clearInterval(timer); clearInterval(timer);
}; };
}, []); }, []);
if (!time || !serverTime?.timezone) { if (!time || !serverTime?.timezone) {
return null; return null;
} }
const getUtcOffset = (timeZone: string) => { const getUtcOffset = (timeZone: string) => {
const date = new Date(); const date = new Date();
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" })); const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
const tzDate = new Date(date.toLocaleString("en-US", { timeZone })); const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60); const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60);
const sign = offset >= 0 ? "+" : "-"; const sign = offset >= 0 ? "+" : "-";
const hours = Math.floor(Math.abs(offset)); const hours = Math.floor(Math.abs(offset));
const minutes = (Math.abs(offset) * 60) % 60; const minutes = (Math.abs(offset) * 60) % 60;
return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes
.toString() .toString()
.padStart(2, "0")}`; .padStart(2, "0")}`;
}; };
return ( return (
<div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden"> <div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden">
<span className="hidden sm:inline">Server Time:</span> <span className="hidden sm:inline">Server Time:</span>
<span className="font-medium tabular-nums"> <span className="font-medium tabular-nums">
{time.toLocaleTimeString()} {time.toLocaleTimeString()}
</span> </span>
<span className="hidden sm:inline text-muted-foreground"> <span className="hidden sm:inline text-muted-foreground">
({serverTime.timezone} | {getUtcOffset(serverTime.timezone)}) ({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
</span> </span>
</div> </div>
); );
} }