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
Binary file not shown.
+45 -30
View File
@@ -5,31 +5,37 @@
</a>
<h3 align="center">Portabase</h3>
<p align="center">
Easily backup and restore your database instances with Portabase
<br />
<a href="https://portabase.io"><strong>Explore the Docs »</strong></a>
<br />
<br />
<a href="https://www.youtube.com/watch?v=D9uFrGxLc4s">View Demo</a>
·
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
·
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
![portabase-ezgif com-video-to-gif-converter](https://github.com/user-attachments/assets/e3929e58-fefd-44cc-ab26-6f8e1a860c43)
<p>
Free, open-source, and self-hosted solution for automated backup and restoration of your database instances.
</p>
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Docker Pulls](https://img.shields.io/docker/pulls/solucetechnologies/portabase?color=brightgreen)](https://hub.docker.com/r/solucetechnologies/portabase)
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/RostislavDugin/postgresus)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/)
[![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white)](https://www.mysql.com/)
[![MariaDB](https://img.shields.io/badge/MariaDB-003545?logo=mariadb&logoColor=white)](https://mariadb.org/)
[![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/RostislavDugin/postgresus)
<p>
<strong>
<a href="https://portabase.io">Documentation</a> •
<a href="https://www.youtube.com/watch?v=D9uFrGxLc4s">Demo</a> •
<a href="#installation">Installation</a> •
<a href="#contributing">Contributing</a> •
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
</strong>
</p>
![portabase-ezgif com-video-to-gif-converter](https://github.com/user-attachments/assets/e3929e58-fefd-44cc-ab26-6f8e1a860c43)
</div>
## 📚 Table of Contents
- [About The Project](#about-the-project)
- [Built With](#built-with)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
@@ -44,17 +50,17 @@
## About The Project
**Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with Portabase agents for managing operations securely and efficiently.
**Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It
integrates seamlessly with Portabase agents for managing operations securely and efficiently.
GitHub Repository: [Portabase](https://github.com/Soluce-Technologies/portabase)
### 🔧 Built With
- [![NextJS][NextJS]][NextJS-url] (v15 with App Router)
- [![NextJS][NextJS]][NextJS-url] (v16 with App Router)
- [![Drizzle][Drizzle]][Drizzle-url]
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
- BetterAuth
- React Email
- [![BetterAuth][BetterAuth]][BetterAuth-url]
---
@@ -147,29 +153,29 @@ Give the project a ⭐ if you like it!
# Environment
NODE_ENV=production
# Database
# Database
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
# Project Info
# Project Info
PROJECT_NAME="Portabase"
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
# SMTP (Email)
# SMTP (Email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
# Google OAuth
# Google OAuth
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_GOOGLE_METHOD=
# S3/MinIO Configuration
# S3/MinIO Configuration
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
@@ -177,10 +183,10 @@ S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
# Storage Backend: 'local' or 's3'
# Storage Backend: 'local' or 's3'
STORAGE_TYPE=local
# Retention
# Retention
RETENTION_CRON="* * * * *"
```
@@ -219,8 +225,17 @@ Thanks to all contributors and the open-source community!
---
[NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
[BetterAuth]: https://img.shields.io/badge/Better%20Auth-FFF?logo=betterauth&logoColor=000&style=for-the-badge
[Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f
[ShadcnUI]: https://img.shields.io/badge/shadcn/ui-000000?style=for-the-badge&logo=shadcn/ui&logoColor=white
[NextJS-url]: https://nextjs.org/
[BetterAuth-url]: https://www.better-auth.com/
[Drizzle-url]: https://orm.drizzle.team/
[ShadcnUI-url]: https://ui.shadcn.com/
+5 -1
View File
@@ -10,11 +10,15 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "radix",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@reui": "https://reui.io/r/{name}.json"
}
}
}
+2
View File
@@ -10,6 +10,8 @@ services:
image: solucetechnologies/portabase:1.1.3-rc.3
ports:
- '8887:80'
environment:
TZ: "Europe/Paris"
env_file:
- .env
depends_on:
+37 -16
View File
@@ -1,21 +1,42 @@
name: portabase-dev
services:
db:
image: postgres:16-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
interval: 10s
timeout: 5s
retries: 5
# app:
# build:
# context: .
# dockerfile: docker/dockerfile/Dockerfile
# target: dev
# ports:
# - "8887:8887"
# environment:
# - TZ=Europe/Paris
# - NODE_ENV=development
# depends_on:
# db:
# condition: service_healthy
# volumes:
# - .:/app
# - /app/node_modules
# container_name: portabase-app
db:
image: postgres:16-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
postgres-data:
+1
View File
@@ -3,6 +3,7 @@ FROM node:22-alpine AS base
ENV YARN_VERSION=4.9.1
RUN apk add --update --no-cache \
tzdata \
build-base \
libc6-compat \
openssl \
+10
View File
@@ -1,5 +1,15 @@
#!/bin/bash
if [ -n "$TZ" ]; then
if [ -f "/usr/share/zoneinfo/$TZ" ]; then
ln -sf /usr/share/zoneinfo/$TZ /etc/localtime
echo "$TZ" > /etc/timezone
echo "[INFO] Timezone set to $TZ"
else
echo "[WARN] Timezone '$TZ' not found. Using default."
fi
fi
node server.js
exec "$@"
+3 -1
View File
@@ -63,8 +63,9 @@
"drizzle-zod": "^0.7.1",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.510.0",
"lucide-react": "^0.553.0",
"minio": "^8.0.5",
"motion": "^12.23.24",
"next": "16.0.0",
"next-safe-action": "^7.10.8",
"next-themes": "^0.4.6",
@@ -81,6 +82,7 @@
"react-hook-form": "^7.56.3",
"react-resizable-panels": "^3.0.2",
"react-twc": "^1.4.2",
"react-use-measure": "^2.1.7",
"recharts": "^2.15.3",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
+1
View File
@@ -43,6 +43,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
"'self'",
"https://api.iconify.design",
"https://code.iconify.design",
"https://api.github.com",
],
OBJECT_SRC: ["'none'"],
+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",
});
}
+79 -5
View File
@@ -7784,6 +7784,28 @@ __metadata:
languageName: node
linkType: hard
"framer-motion@npm:^12.23.24":
version: 12.23.24
resolution: "framer-motion@npm:12.23.24"
dependencies:
motion-dom: "npm:^12.23.23"
motion-utils: "npm:^12.23.6"
tslib: "npm:^2.4.0"
peerDependencies:
"@emotion/is-prop-valid": "*"
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@emotion/is-prop-valid":
optional: true
react:
optional: true
react-dom:
optional: true
checksum: 10c0/716addd9fa85dd2c1475ab848e14c4e6d3246ced041de84afc19b1d6534c897256c8f2878864cf057039116e56f5ca1b87a4cbb943f0b1cf37199ef7174c3cf8
languageName: node
linkType: hard
"fs-constants@npm:^1.0.0":
version: 1.0.0
resolution: "fs-constants@npm:1.0.0"
@@ -9113,12 +9135,12 @@ __metadata:
languageName: node
linkType: hard
"lucide-react@npm:^0.510.0":
version: 0.510.0
resolution: "lucide-react@npm:0.510.0"
"lucide-react@npm:^0.553.0":
version: 0.553.0
resolution: "lucide-react@npm:0.553.0"
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/00b3efed64c370cd5ae97023583996ff83939baf6f4af11393d536ca9e520b0cc1a82a81aa6d0350625bff398daaa69357df74383631f2c3a0aeb6974f4237c7
checksum: 10c0/b1283f1a2302a50d6a16bd67b9f7417ae08a74cba59ae5ca9953527cb9724247df12ba8c72ca2fc6a70b745f8315cc709ac7573643c4088728766da151999ef6
languageName: node
linkType: hard
@@ -9406,6 +9428,43 @@ __metadata:
languageName: node
linkType: hard
"motion-dom@npm:^12.23.23":
version: 12.23.23
resolution: "motion-dom@npm:12.23.23"
dependencies:
motion-utils: "npm:^12.23.6"
checksum: 10c0/139705731085063519b88f23fcc5b1c13e15707a4ff3365da02ef9a4bf2a2d8ebed9a151c57e7f215ccd9e822365d93c16e28e619fbf25611f61dcff5ee81d75
languageName: node
linkType: hard
"motion-utils@npm:^12.23.6":
version: 12.23.6
resolution: "motion-utils@npm:12.23.6"
checksum: 10c0/c058e8ba6423b3baa63e985bcc669877ee7d9579d938f5348b4e60c5ea1b4b33dd7f4877434436a4a5807f3cf00370d3fd4079a6fdd6309c5c87aa62b311a897
languageName: node
linkType: hard
"motion@npm:^12.23.24":
version: 12.23.24
resolution: "motion@npm:12.23.24"
dependencies:
framer-motion: "npm:^12.23.24"
tslib: "npm:^2.4.0"
peerDependencies:
"@emotion/is-prop-valid": "*"
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
"@emotion/is-prop-valid":
optional: true
react:
optional: true
react-dom:
optional: true
checksum: 10c0/d937ddaf50816f97e4e7121f323104fd655045ef122bd03d433370649c00129a5dd928cbe54efe9add22a05b2a9b84b7cfda1fd5c5b1e9c6245cd94e1c24e823
languageName: node
linkType: hard
"ms@npm:^2.1.1, ms@npm:^2.1.3":
version: 2.1.3
resolution: "ms@npm:2.1.3"
@@ -10237,8 +10296,9 @@ __metadata:
eslint-config-next: "npm:^16.0.1"
eslint-plugin-tailwindcss: "npm:^3.18.0"
input-otp: "npm:^1.4.2"
lucide-react: "npm:^0.510.0"
lucide-react: "npm:^0.553.0"
minio: "npm:^8.0.5"
motion: "npm:^12.23.24"
next: "npm:16.0.0"
next-safe-action: "npm:^7.10.8"
next-themes: "npm:^0.4.6"
@@ -10256,6 +10316,7 @@ __metadata:
react-hook-form: "npm:^7.56.3"
react-resizable-panels: "npm:^3.0.2"
react-twc: "npm:^1.4.2"
react-use-measure: "npm:^2.1.7"
recharts: "npm:^2.15.3"
socket.io: "npm:^4.8.1"
socket.io-client: "npm:^4.8.1"
@@ -10689,6 +10750,19 @@ __metadata:
languageName: node
linkType: hard
"react-use-measure@npm:^2.1.7":
version: 2.1.7
resolution: "react-use-measure@npm:2.1.7"
peerDependencies:
react: ">=16.13"
react-dom: ">=16.13"
peerDependenciesMeta:
react-dom:
optional: true
checksum: 10c0/ff24130e6f95e853feb6892fb74af08dbc5aae3574b701169e3bc3adb392c3162f51a58ddfe39bb7337db13ae609bbec0bb51a9de8b5fae5420f9d17e1f8b542
languageName: node
linkType: hard
"react@npm:^19.2.0":
version: 19.2.0
resolution: "react@npm:19.2.0"