diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index f6e3efb5..39f35b2b 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/README.md b/README.md index beb76d8c..7100fbd0 100644 --- a/README.md +++ b/README.md @@ -5,31 +5,37 @@

Portabase

- -

- Easily backup and restore your database instances with Portabase -
- Explore the Docs » -
-
- View Demo - · - Report Bug - · - Request Feature - - ![portabase-ezgif com-video-to-gif-converter](https://github.com/user-attachments/assets/e3929e58-fefd-44cc-ab26-6f8e1a860c43) - +

+ Free, open-source, and self-hosted solution for automated backup and restoration of your database instances.

+ + [![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) + +

+ + Documentation • + Demo • + Installation • + Contributing • + Report Bug • + Request Feature + +

+ + ![portabase-ezgif com-video-to-gif-converter](https://github.com/user-attachments/assets/e3929e58-fefd-44cc-ab26-6f8e1a860c43) + - - - ## 📚 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/ diff --git a/components.json b/components.json index bcec1f91..38a8e6a9 100644 --- a/components.json +++ b/components.json @@ -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" } -} \ No newline at end of file +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index cefb92df..64efecb7 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -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: diff --git a/docker-compose.yml b/docker-compose.yml index a7524bce..1eaa8803 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/docker/dockerfile/Dockerfile b/docker/dockerfile/Dockerfile index 4ce19996..9103e889 100644 --- a/docker/dockerfile/Dockerfile +++ b/docker/dockerfile/Dockerfile @@ -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 \ diff --git a/docker/entrypoints/app-prod-entrypoint.sh b/docker/entrypoints/app-prod-entrypoint.sh index 2fbfb3cb..23e73a04 100644 --- a/docker/entrypoints/app-prod-entrypoint.sh +++ b/docker/entrypoints/app-prod-entrypoint.sh @@ -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 "$@" \ No newline at end of file diff --git a/package.json b/package.json index 50a0844a..a371c59d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/portabase.config.ts b/portabase.config.ts index a6d6a4b6..7a29222a 100644 --- a/portabase.config.ts +++ b/portabase.config.ts @@ -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'"], diff --git a/src/components/ui/sliding-number.tsx b/src/components/ui/sliding-number.tsx new file mode 100644 index 00000000..ac9c085d --- /dev/null +++ b/src/components/ui/sliding-number.tsx @@ -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 ( + + 0 + {Array.from({ length: 10 }, (_, i) => ( + + ))} + + ); +} + +type SlidingNumberDisplayProps = { + motionValue: MotionValue; + 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 {number}; + } + + return ( + + {number} + + ); +} + +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(null); + React.useImperativeHandle(ref, () => localRef.current!); + + const inViewResult = useInView(localRef, { + once: inViewOnce, + margin: inViewMargin, + }); + const isInView = !inView || inViewResult; + + const prevNumberRef = React.useRef(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 ( + + {isInView && Number(number) < 0 && -} + + {intPlaces.map((place) => ( + + ))} + + {newDecStrRaw && ( + <> + {decimalSeparator} + {decPlaces.map((place) => ( + + ))} + + )} + + ); +} + +export { SlidingNumber, type SlidingNumberProps }; \ No newline at end of file diff --git a/src/components/wrappers/common/github/github-button.tsx b/src/components/wrappers/common/github/github-button.tsx new file mode 100644 index 00000000..163f4874 --- /dev/null +++ b/src/components/wrappers/common/github/github-button.tsx @@ -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 ( +
+ +
+ ); +}; +export default GitHubStarsButtonCustom; \ No newline at end of file diff --git a/src/components/wrappers/common/table/table-pagination-size.tsx b/src/components/wrappers/common/table/table-pagination-size.tsx index 98803040..2ba0d1e8 100644 --- a/src/components/wrappers/common/table/table-pagination-size.tsx +++ b/src/components/wrappers/common/table/table-pagination-size.tsx @@ -17,7 +17,7 @@ export const TablePaginationSize = (props: tablePaginationSizeProps) => { return (
-

Rows per page

+

Rows per page