Working on the README.md

This commit is contained in:
charlesgauthereau
2025-11-08 20:07:49 +01:00
parent cf8e5b7094
commit 4644ed2f41
25 changed files with 514 additions and 88 deletions
+235
View File
@@ -0,0 +1,235 @@
'use client';
import * as React from 'react';
import {
useSpring,
useTransform,
motion,
useInView,
type MotionValue,
type SpringOptions,
type UseInViewOptions,
} from 'motion/react';
import useMeasure from 'react-use-measure';
import {cn} from "@/lib/utils";
type SlidingNumberRollerProps = {
prevValue: number;
value: number;
place: number;
transition: SpringOptions;
};
function SlidingNumberRoller({
prevValue,
value,
place,
transition,
}: SlidingNumberRollerProps) {
const startNumber = Math.floor(prevValue / place) % 10;
const targetNumber = Math.floor(value / place) % 10;
const animatedValue = useSpring(startNumber, transition);
React.useEffect(() => {
animatedValue.set(targetNumber);
}, [targetNumber, animatedValue]);
const [measureRef, { height }] = useMeasure();
return (
<span
ref={measureRef}
data-slot="sliding-number-roller"
className="relative inline-block w-[1ch] overflow-x-visible overflow-y-clip leading-none tabular-nums"
>
<span className="invisible">0</span>
{Array.from({ length: 10 }, (_, i) => (
<SlidingNumberDisplay
key={i}
motionValue={animatedValue}
number={i}
height={height}
transition={transition}
/>
))}
</span>
);
}
type SlidingNumberDisplayProps = {
motionValue: MotionValue<number>;
number: number;
height: number;
transition: SpringOptions;
};
function SlidingNumberDisplay({
motionValue,
number,
height,
transition,
}: SlidingNumberDisplayProps) {
const y = useTransform(motionValue, (latest) => {
if (!height) return 0;
const currentNumber = latest % 10;
const offset = (10 + number - currentNumber) % 10;
let translateY = offset * height;
if (offset > 5) translateY -= 10 * height;
return translateY;
});
if (!height) {
return <span className="invisible absolute">{number}</span>;
}
return (
<motion.span
data-slot="sliding-number-display"
style={{ y }}
className="absolute inset-0 flex items-center justify-center"
transition={{ ...transition, type: 'spring' }}
>
{number}
</motion.span>
);
}
type SlidingNumberProps = React.ComponentProps<'span'> & {
number: number | string;
inView?: boolean;
inViewMargin?: UseInViewOptions['margin'];
inViewOnce?: boolean;
padStart?: boolean;
decimalSeparator?: string;
decimalPlaces?: number;
transition?: SpringOptions;
};
function SlidingNumber({
ref,
number,
className,
inView = false,
inViewMargin = '0px',
inViewOnce = true,
padStart = false,
decimalSeparator = '.',
decimalPlaces = 0,
transition = {
stiffness: 200,
damping: 20,
mass: 0.4,
},
...props
}: SlidingNumberProps) {
const localRef = React.useRef<HTMLSpanElement>(null);
React.useImperativeHandle(ref, () => localRef.current!);
const inViewResult = useInView(localRef, {
once: inViewOnce,
margin: inViewMargin,
});
const isInView = !inView || inViewResult;
const prevNumberRef = React.useRef<number>(0);
const effectiveNumber = React.useMemo(
() => (!isInView ? 0 : Math.abs(Number(number))),
[number, isInView],
);
const formatNumber = React.useCallback(
(num: number) =>
decimalPlaces != null ? num.toFixed(decimalPlaces) : num.toString(),
[decimalPlaces],
);
const numberStr = formatNumber(effectiveNumber);
const [newIntStrRaw, newDecStrRaw = ''] = numberStr.split('.');
const newIntStr =
padStart && newIntStrRaw?.length === 1 ? '0' + newIntStrRaw : newIntStrRaw;
const prevFormatted = formatNumber(prevNumberRef.current);
const [prevIntStrRaw = '', prevDecStrRaw = ''] = prevFormatted.split('.');
const prevIntStr =
padStart && prevIntStrRaw.length === 1
? '0' + prevIntStrRaw
: prevIntStrRaw;
const adjustedPrevInt = React.useMemo(() => {
return prevIntStr.length > (newIntStr?.length ?? 0)
? prevIntStr.slice(-(newIntStr?.length ?? 0))
: prevIntStr.padStart(newIntStr?.length ?? 0, '0');
}, [prevIntStr, newIntStr]);
const adjustedPrevDec = React.useMemo(() => {
if (!newDecStrRaw) return '';
return prevDecStrRaw.length > newDecStrRaw.length
? prevDecStrRaw.slice(0, newDecStrRaw.length)
: prevDecStrRaw.padEnd(newDecStrRaw.length, '0');
}, [prevDecStrRaw, newDecStrRaw]);
React.useEffect(() => {
if (isInView) prevNumberRef.current = effectiveNumber;
}, [effectiveNumber, isInView]);
const intDigitCount = newIntStr?.length ?? 0;
const intPlaces = React.useMemo(
() =>
Array.from({ length: intDigitCount }, (_, i) =>
Math.pow(10, intDigitCount - i - 1),
),
[intDigitCount],
);
const decPlaces = React.useMemo(
() =>
newDecStrRaw
? Array.from({ length: newDecStrRaw.length }, (_, i) =>
Math.pow(10, newDecStrRaw.length - i - 1),
)
: [],
[newDecStrRaw],
);
const newDecValue = newDecStrRaw ? parseInt(newDecStrRaw, 10) : 0;
const prevDecValue = adjustedPrevDec ? parseInt(adjustedPrevDec, 10) : 0;
return (
<span
ref={localRef}
data-slot="sliding-number"
className={cn('flex items-center', className)}
{...props}
>
{isInView && Number(number) < 0 && <span className="mr-1">-</span>}
{intPlaces.map((place) => (
<SlidingNumberRoller
key={`int-${place}`}
prevValue={parseInt(adjustedPrevInt, 10)}
value={parseInt(newIntStr ?? '0', 10)}
place={place}
transition={transition}
/>
))}
{newDecStrRaw && (
<>
<span>{decimalSeparator}</span>
{decPlaces.map((place) => (
<SlidingNumberRoller
key={`dec-${place}`}
prevValue={prevDecValue}
value={newDecValue}
place={place}
transition={transition}
/>
))}
</>
)}
</span>
);
}
export { SlidingNumber, type SlidingNumberProps };
@@ -0,0 +1,47 @@
'use client';
import {GithubButton} from "@/components/ui/github-button";
import * as React from "react";
import {useEffect} from "react";
export const GitHubStarsButtonCustom = () => {
const [stars, setStars] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(true);
const username = "Soluce-Technologies"
const repo = "portabase"
useEffect(() => {
fetch(`https://api.github.com/repos/${username}/${repo}`)
.then((response) => response.json())
.then((data) => {
if (data && typeof data.stargazers_count === 'number') {
setStars(data.stargazers_count);
}
})
.catch(console.error)
.finally(() => setIsLoading(false));
}, [username, repo]);
if (isLoading) return null;
return (
<div className=" gap-4 flex-wrap hidden md:block">
<GithubButton
initialStars={0}
targetStars={stars}
label="Github Stars"
size="sm"
separator={true}
roundStars={true}
repoUrl={`https://github.com/${username}/${repo}`}
variant="outline"
/>
</div>
);
};
export default GitHubStarsButtonCustom;
@@ -17,7 +17,7 @@ export const TablePaginationSize = (props: tablePaginationSizeProps) => {
return (
<div className={cn("flex items-center justify-end sm:justify-center space-x-2", className)}>
<p className="whitespace-nowrap text-sm font-medium">Rows per page</p>
<p className="whitespace-nowrap text-sm font-medium hidden md:block">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
@@ -15,9 +15,9 @@ export function TablePagination(props: tablePaginationProps) {
const { className, table, maxVisiblePages = 3, pageSizeOptions = [10, 20, 30, 40, 50] } = props;
return (
<div className={cn("flex flex-col gap-x-4 sm:flex-row", className)}>
<div className={cn("flex gap-x-4 flex-row w-full", className)}>
<TablePaginationSize table={table} pageSizeOptions={pageSizeOptions} />
<TablePaginationNavigation table={table} maxVisiblePages={maxVisiblePages} className="justify-end mt-5 sm:mt-0" />
<TablePaginationNavigation table={table} maxVisiblePages={maxVisiblePages} className="justify-end mt-0" />
</div>
);
}
@@ -50,10 +50,10 @@ export const AdminTabs = ({users, settings, organizations}: AdminTabsProps) => {
Storage
</TabsTrigger>
</TabsList>
<TabsContent value="users">
<TabsContent className="h-full" value="users">
<AdminUsersTable users={users}/>
</TabsContent>
<TabsContent value="organizations">
<TabsContent className="h-full" value="organizations">
<AdminOrganizationsTable organizations={organizations}/>
</TabsContent>
<TabsContent value="email">
@@ -11,13 +11,13 @@ export type AdminOrganizationsTableProps = {
export const AdminOrganizationsTable = (props: AdminOrganizationsTableProps) => {
const {organizations} = props;
return (
<div className="flex flex-col gap-y-4 h-full py-4">
<Card>
<div className="flex flex-col gap-y-4 h-full py-4 ">
<Card className="h-full">
<CardHeader>
<CardTitle>Active organizations</CardTitle>
<CardDescription>Manage all system organizations</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="h-full">
<AdminOrganizationList organizations={organizations} />
</CardContent>
</Card>
@@ -11,7 +11,6 @@ import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-d
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {UserWithAccounts} from "@/db/schema/02_user";
import {authClient, useSession} from "@/lib/auth/auth-client";
import {formatFrenchDate} from "@/utils/date-formatting";
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
import {Organization} from "@/db/schema/03_organization";
@@ -12,12 +12,12 @@ export const AdminUsersTable = (props: AdminUsersTableProps) => {
const {users} = props;
return (
<div className="flex flex-col gap-y-4 h-full py-4">
<Card>
<Card className="h-full ">
<CardHeader>
<CardTitle>Active users</CardTitle>
<CardDescription>Manage your users</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="h-full">
<DataTable
enableSelect={false}
columns={usersColumnsAdmin}
@@ -6,14 +6,11 @@ import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {useState} from "react";
import {Trash2} from "lucide-react";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {UserWithAccounts} from "@/db/schema/02_user";
import {authClient, useSession} from "@/lib/auth/auth-client";
import {formatFrenchDate} from "@/utils/date-formatting";
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/tabs/admin-user-tab/button-delete-use";
import {formatLocalizedDate} from "@/utils/date-formatting";
export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
{
@@ -88,7 +85,7 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
accessorKey: "updatedAt",
header: "Updated At",
cell: ({row}) => {
return formatFrenchDate(row.getValue("updatedAt"))
return formatLocalizedDate(row.getValue("updatedAt"))
},
},
{
+1 -1
View File
@@ -37,7 +37,7 @@ export const schemas = {
export const db = drizzle({
client: pool,
logger: process.env.NODE_ENV != 'production',
// logger: process.env.NODE_ENV != 'production',
schema: schemas,
});
+1
View File
@@ -39,6 +39,7 @@ export const env = createEnv({
RETENTION_CRON: z
.string()
.default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"),
},
client: {
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
+3 -3
View File
@@ -22,7 +22,6 @@ import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {StatusBadge} from "@/components/wrappers/common/status-badge";
import {Backup, DatabaseWith} from "@/db/schema/07_database";
import {formatFrenchDate} from "@/utils/date-formatting";
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
import {Setting} from "@/db/schema/01_setting";
import {SafeActionResult} from "next-safe-action";
@@ -31,6 +30,7 @@ import {ServerActionResult} from "@/types/action-type";
import {cn} from "@/lib/utils";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
import {MemberWithUser} from "@/db/schema/03_organization";
import {formatLocalizedDate} from "@/utils/date-formatting";
export function backupColumns(
@@ -52,7 +52,7 @@ export function backupColumns(
</TooltipTrigger>
{row.original.deletedAt != null && (
<TooltipContent>
<p>{formatFrenchDate(row.original.deletedAt)}</p>
<p>{formatLocalizedDate(row.original.deletedAt)}</p>
</TooltipContent>
)}
</Tooltip>
@@ -68,7 +68,7 @@ export function backupColumns(
accessorKey: "createdAt",
header: "Created At",
cell: ({row}) => {
return formatFrenchDate(row.getValue("createdAt"))
return formatLocalizedDate(row.getValue("createdAt"))
},
},
{
+3 -4
View File
@@ -13,11 +13,10 @@ import { Button } from "@/components/ui/button";
import {MoreHorizontal, Trash2} from "lucide-react";
import { ReloadIcon } from "@radix-ui/react-icons";
import { StatusBadge } from "@/components/wrappers/common/status-badge";
import {Backup, Restoration} from "@/db/schema/07_database";
import {formatFrenchDate} from "@/utils/date-formatting";
import {Restoration} from "@/db/schema/07_database";
import {formatLocalizedDate} from "@/utils/date-formatting";
import {useMutation} from "@tanstack/react-query";
import {
deleteBackupAction,
deleteRestoreAction,
rerunRestorationAction
} from "@/features/dashboard/restore/restore.action";
@@ -40,7 +39,7 @@ export function restoreColumns(
accessorKey: "createdAt",
header: "Created At",
cell: ({ row }) => {
return formatFrenchDate(row.getValue("createdAt"))
return formatLocalizedDate(row.getValue("createdAt"))
},
},
{
+3
View File
@@ -5,6 +5,8 @@ import {ModeToggle} from "@/features/theme/ModeToggle";
import {currentUser} from "@/lib/auth/current-user";
import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/logged-in-button";
import {BreadCrumbsWrapper} from "@/components/wrappers/common/bread-crumbs/bread-crumbs";
import GitHubStarsButtonCustom from "@/components/wrappers/common/github/github-button";
// import GitHubStarsButtonCustom from "@/components/wrappers/common/github-button";
export const Header = async () => {
const user = await currentUser();
@@ -19,6 +21,7 @@ export const Header = async () => {
</div>
<div className="flex items-center gap-2">
<GitHubStarsButtonCustom/>
<ModeToggle/>
<LoggedInButton/>
</div>
+26 -11
View File
@@ -1,7 +1,29 @@
import {format, formatDistanceToNow} from "date-fns";
import {formatDistanceToNow} from "date-fns";
/**
* Get user's locale and timezone from the browser
*/
const TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
const LOCALE = Intl.DateTimeFormat().resolvedOptions().locale;
/**
* Format a date in DD/MM/YYYY HH:mm 24-hour format
*/
export function formatLocalizedDate(date: string | number | Date) {
const d = new Date(date);
return new Intl.DateTimeFormat(LOCALE, {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false, // 24-hour format
timeZone: TIMEZONE,
}).format(d);
}
export function humanReadableDate(rawDate: string | number | Date) {
return formatFrenchDate(rawDate);
return formatLocalizedDate(rawDate);
}
export function timeAgo(rawDate: string | number | Date) {
@@ -10,14 +32,7 @@ export function timeAgo(rawDate: string | number | Date) {
}
export function formatDateLastContact(lastContact: string | number | Date | null) {
console.log(lastContact)
return lastContact
? formatFrenchDate(lastContact)
: "Never connected."
? formatLocalizedDate(lastContact)
: "Never connected.";
}
export function formatFrenchDate(date: string | number | Date) {
return new Date(date).toLocaleString("fr-FR", {
timeZone: "Europe/Paris",
});
}