شرح مختصر للتعديلات اللي عملتها

This commit is contained in:
Abdelsabour
2025-08-15 00:37:16 +03:00
parent 218c904ac8
commit 5dc63cb8f3
79 changed files with 11805 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
{
"id": "d8b93139-50bd-4ba7-9adb-dd226f5e8c58",
"name": "universal-db-backup",
"projectId": "9742b893-75f0-453f-95e5-571c4e621456",
"ports": [
{
"expose": true,
"number": 3000
}
],
"publicAccess": {
"enabled": true,
"domain": "auto"
},
"resourceTier": "t1",
"dockerFileName": "Dockerfile"
}
+43
View File
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
/generated/prisma
View File
+32
View File
@@ -0,0 +1,32 @@
# المرحلة الأولى: مرحلة البناء (Builder Stage)
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# ---
# المرحلة الثانية: مرحلة الإنتاج (Production Stage)
FROM node:18-alpine
WORKDIR /app
# ✅ **التعديل: تحديث قائمة الحزم أولاً ثم تثبيت postgresql-client**
RUN apk update && apk add --no-cache postgresql-client
# نسخ الملفات النهائية من مرحلة البناء
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["npm", "start"]
+46
View File
@@ -0,0 +1,46 @@
[
{
"id": "e148ba20-033d-45b5-aff5-45c775a30cf7",
"dbName": "neondb",
"status": "PROCESSING",
"createdAt": "2025-08-13T20:14:11.810Z"
},
{
"id": "59776333-f4df-4ba7-ac4a-fe9cdd4d0e8f",
"dbName": "neondb",
"status": "PROCESSING",
"createdAt": "2025-08-13T20:24:26.547Z"
},
{
"id": "16a4bd0f-ef7a-48c2-9314-87a4d83af6bd",
"dbName": "neondb",
"status": "COMPLETED",
"createdAt": "2025-08-13T21:08:07.751Z",
"fileName": "neondb_2025-08-13T21-08-07-750Z.backup",
"downloadUrl": "https://s3.wasabisys.com/ilibrary/neondb_2025-08-13T21-08-07-750Z.backup"
},
{
"id": "6a066ebb-46c3-42c2-aa14-2d2f8e0d9c97",
"dbName": "neondb",
"status": "COMPLETED",
"createdAt": "2025-08-13T21:17:53.330Z",
"fileName": "neondb_2025-08-13T21-17-53-330Z.dump",
"downloadUrl": "https://ilibrary.s3.wasabisys.com/neondb_2025-08-13T21-17-53-330Z.dump?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=GURLVI9UGF3UNMOMU1KR%2F20250813%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250813T211914Z&X-Amz-Expires=3600&X-Amz-Signature=f2d88e7aa87189a8da6c3593ddeb27f9b346cd29082ccf45840ed355c7484a07&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject"
},
{
"id": "9a175f17-26fb-4615-8717-3d75f4e3d0b9",
"dbName": "neondb",
"status": "COMPLETED",
"createdAt": "2025-08-13T21:18:48.639Z",
"fileName": "neondb_2025-08-13T21-18-48-639Z.dump",
"downloadUrl": "https://ilibrary.s3.wasabisys.com/neondb_2025-08-13T21-18-48-639Z.dump?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=GURLVI9UGF3UNMOMU1KR%2F20250813%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250813T211933Z&X-Amz-Expires=3600&X-Amz-Signature=f02231664cb5bc6052b3d22347c7edf42299c26172c81141da7191fcaa153219&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject"
},
{
"id": "d34a0541-8d49-4779-bcf4-96a02fc64043",
"dbName": "neondb",
"status": "COMPLETED",
"createdAt": "2025-08-14T20:09:15.711Z",
"fileName": "neondb_2025-08-14T20-09-15-711Z.dump",
"downloadUrl": "https://ilibrary.s3.wasabisys.com/neondb_2025-08-14T20-09-15-711Z.dump?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=GURLVI9UGF3UNMOMU1KR%2F20250814%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250814T200939Z&X-Amz-Expires=3600&X-Amz-Signature=ee8f3fee872986fe62dd6cafc35be898c6422f7a7ab5f06a495c87a2e911eab2&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject"
}
]
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "styles/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+9
View File
@@ -0,0 +1,9 @@
"use client"
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) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
+46
View File
@@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }
+59
View File
@@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
+92
View File
@@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+30
View File
@@ -0,0 +1,30 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }
+141
View File
@@ -0,0 +1,141 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }
+24
View File
@@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }
+183
View File
@@ -0,0 +1,183 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
+23
View File
@@ -0,0 +1,23 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }
+114
View File
@@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
+52
View File
@@ -0,0 +1,52 @@
# docker-compose.yml
# نسخة معدلة ومحسّنة للعمل على المنصات السحابية
version: '3.8'
services:
# الخدمة الأولى: قاعدة البيانات PostgreSQL
postgresql:
image: postgres:16-alpine
container_name: ilibrary_db_container
restart: always
environment:
POSTGRES_DB: ilibrarydb
POSTGRES_USER: abdyousef
POSTGRES_PASSWORD: ilibrary2025!
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-network
# ✅ **الجديد هنا: تحديد حدود الموارد**
# هذا يمنع قاعدة البيانات من استهلاك كل ذاكرة السيرفر
deploy:
resources:
limits:
memory: 512M
# الخدمة الثانية: تطبيق Next.js الخاص بك
app:
build: .
container_name: ilibrary_app_container
restart: always
ports:
- "3000:3000"
depends_on:
- postgresql
environment:
DATABASE_URL: "postgresql://abdyousef:ilibrary2025!@postgresql:5432/ilibrarydb"
NEXT_PUBLIC_API_URL: "http://localhost:3000/api"
networks:
- app-network
# ✅ **الجديد هنا: تحديد حدود الموارد للتطبيق أيضاً**
deploy:
resources:
limits:
memory: 512M
networks:
app-network:
driver: bridge
volumes:
postgres_data:
+16
View File
@@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;
+15
View File
@@ -0,0 +1,15 @@
import { PrismaClient } from '@prisma/client'
// إنشاء نسخة واحدة من PrismaClient لتجنب إنشاء اتصالات متعددة
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: ['query'],
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
// استيراد النوع من Prisma مباشرة
export type { BackupRecord } from '@prisma/client'
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig
+9662
View File
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
{
"name": "universal-db-backup",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.864.0",
"@aws-sdk/lib-storage": "^3.864.0",
"@aws-sdk/s3-request-presigner": "^3.864.0",
"@prisma/client": "^6.14.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.2.3",
"aws-sdk": "^2.1641.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.395.0",
"mongodb": "^6.18.0",
"mysql2": "^3.14.3",
"next": "^14.2.31",
"next-themes": "^0.3.0",
"node-cron": "^3.0.3",
"pg": "^8.16.3",
"react": "^18",
"react-dom": "^18",
"sonner": "^1.5.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/node-cron": "^3.0.11",
"@types/pg": "^8.15.5",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.19",
"eslint": "^8",
"eslint-config-next": "14.2.4",
"postcss": "^8",
"prisma": "^6.14.0",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
+18
View File
@@ -0,0 +1,18 @@
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import { Toaster } from "@/components/ui/sonner"
import { ThemeProvider } from '@/components/theme-provider'
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Component {...pageProps} />
<Toaster />
</ThemeProvider>
)
}
+13
View File
@@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body className="antialiased">
<Main />
<NextScript />
</body>
</Html>
);
}
+170
View File
@@ -0,0 +1,170 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { spawn } from 'child_process';
import path from 'path';
import fs from 'fs';
import { randomUUID } from 'crypto';
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { PassThrough } from 'stream';
// --- Helper Types and Functions for History Logging ---
type BackupStatus = 'COMPLETED' | 'FAILED' | 'PROCESSING' | 'QUEUED' | 'CANCELLED';
type BackupRecord = {
id: string;
dbName: string;
status: BackupStatus;
createdAt: string;
fileName?: string;
error?: string;
downloadUrl?: string;
};
const DB_PATH = path.resolve(process.cwd(), 'backup-history.json');
const readRecords = async (): Promise<BackupRecord[]> => {
try {
if (!fs.existsSync(DB_PATH)) {
await fs.promises.writeFile(DB_PATH, JSON.stringify([]), 'utf-8');
return [];
}
const fileContent = await fs.promises.readFile(DB_PATH, 'utf-8');
return fileContent ? JSON.parse(fileContent) : [];
} catch (error) {
console.error("Error reading backup history:", error);
return [];
}
};
const writeRecords = async (records: BackupRecord[]): Promise<void> => {
try {
await fs.promises.writeFile(DB_PATH, JSON.stringify(records, null, 2), 'utf-8');
} catch (error) {
console.error("Error writing backup history:", error);
}
};
const sendStreamMessage = (res: NextApiResponse, data: object) => {
try {
if (!res.writableEnded) {
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
} catch (e) {
console.error("Failed to write to stream:", e);
}
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const {
dbHost, dbPort, dbUser, dbPassword, dbName, dbRequireSsl,
s3Endpoint, s3BucketName, s3AccessKey, s3SecretKey, s3Region
} = req.query;
const recordId = randomUUID();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFileName = `${dbName as string}_${timestamp}.dump`;
const backupsDir = path.resolve(process.cwd(), 'backups');
const backupFilePath = path.join(backupsDir, backupFileName);
if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir);
const newRecord: BackupRecord = { id: recordId, dbName: dbName as string, status: 'PROCESSING', createdAt: new Date().toISOString(), fileName: backupFileName };
const records = await readRecords();
records.push(newRecord);
await writeRecords(records);
const pgDumpCommand = 'pg_dump';
const args: string[] = [
'--format=c',
'--blobs',
'--verbose',
`--host=${dbHost}`,
`--port=${dbPort}`,
`--username=${dbUser}`,
`--dbname=${dbName}`,
];
const env: NodeJS.ProcessEnv = {
...process.env,
PGPASSWORD: dbPassword as string,
PGSSLMODE: dbRequireSsl === 'true' ? 'require' : 'prefer',
};
const backupProcess = spawn(pgDumpCommand, args, { env });
req.on('close', async () => {
console.log("Client disconnected. Terminating backup process...");
backupProcess.kill(); // Stop the pg_dump process
const finalRecords = await readRecords();
const recordIndex = finalRecords.findIndex(r => r.id === recordId);
if (recordIndex > -1 && finalRecords[recordIndex].status === 'PROCESSING') {
finalRecords[recordIndex].status = 'CANCELLED';
finalRecords[recordIndex].error = 'Process cancelled by user.';
await writeRecords(finalRecords);
}
res.end();
});
const s3Client = new S3Client({
endpoint: s3Endpoint as string,
region: s3Region as string,
credentials: { accessKeyId: s3AccessKey as string, secretAccessKey: s3SecretKey as string }
});
const passThrough = new PassThrough();
passThrough.pipe(fs.createWriteStream(backupFilePath));
const s3Upload = new Upload({
client: s3Client,
params: { Bucket: s3BucketName as string, Key: backupFileName, Body: passThrough, ContentType: 'application/octet-stream' },
});
if (backupProcess.stdout) backupProcess.stdout.pipe(passThrough);
let errorOutput = '';
if (backupProcess.stderr) {
backupProcess.stderr.on('data', (data: Buffer | string) => {
errorOutput += data.toString();
sendStreamMessage(res, { message: data.toString().trim() });
});
}
try {
await s3Upload.done();
sendStreamMessage(res, { message: "✅ S3 upload completed successfully." });
const command = new GetObjectCommand({ Bucket: s3BucketName as string, Key: backupFileName });
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
const finalRecords = await readRecords();
const recordIndex = finalRecords.findIndex(r => r.id === recordId);
if (recordIndex > -1) {
finalRecords[recordIndex].status = 'COMPLETED';
finalRecords[recordIndex].downloadUrl = signedUrl;
await writeRecords(finalRecords);
}
sendStreamMessage(res, { message: "All tasks finished.", status: 'completed' });
} catch (err: any) {
sendStreamMessage(res, { message: `❌ S3 Upload Failed: ${err.message}`, status: 'failed' });
const finalRecords = await readRecords();
const recordIndex = finalRecords.findIndex(r => r.id === recordId);
if (recordIndex > -1) {
finalRecords[recordIndex].status = 'FAILED';
finalRecords[recordIndex].error = `S3 Error: ${err.message}`;
await writeRecords(finalRecords);
}
} finally {
if (!res.writableEnded) {
sendStreamMessage(res, { status: 'closed' });
res.end();
}
}
}
+59
View File
@@ -0,0 +1,59 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs';
import path from 'path';
// Define a type for our backup records for type safety
type BackupRecord = {
id: string;
dbName: string;
status: 'COMPLETED' | 'FAILED' | 'PROCESSING' | 'QUEUED';
createdAt: string;
fileName?: string;
error?: string;
downloadUrl?: string;
};
// Define the path to our simple JSON database file
const DB_PATH = path.resolve(process.cwd(), 'backup-history.json');
/**
* Reads backup records from the JSON file.
* @returns {Promise<BackupRecord[]>} A promise that resolves to an array of backup records.
*/
const readRecords = async (): Promise<BackupRecord[]> => {
try {
// Check if the database file exists
if (!fs.existsSync(DB_PATH)) {
// If not, create it with an empty array
await fs.promises.writeFile(DB_PATH, JSON.stringify([]), 'utf-8');
return [];
}
// If it exists, read and parse it
const fileContent = await fs.promises.readFile(DB_PATH, 'utf-8');
return JSON.parse(fileContent);
} catch (error) {
console.error("Error reading backup history:", error);
// Return an empty array in case of any error to prevent crashes
return [];
}
};
/**
* API handler for fetching backup history.
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// This endpoint only supports GET requests
if (req.method !== 'GET') {
res.setHeader('Allow', ['GET']);
return res.status(405).json({ message: `Method ${req.method} Not Allowed` });
}
try {
const records = await readRecords();
// Sort records by date, newest first
const sortedRecords = records.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
return res.status(200).json(sortedRecords);
} catch (error) {
return res.status(500).json({ error: 'Failed to retrieve backup history.' });
}
}
+63
View File
@@ -0,0 +1,63 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs';
import path from 'path';
// Define a type for our backup records for type safety
type BackupRecord = {
id: string;
dbName: string;
status: 'COMPLETED' | 'FAILED' | 'PROCESSING' | 'QUEUED';
createdAt: string;
fileName?: string;
error?: string;
downloadUrl?: string;
};
// Define the path to our simple JSON database file
// This file will be created in the root of your project
const DB_PATH = path.resolve(process.cwd(), 'backup-history.json');
/**
* Reads backup records from the JSON file.
* Creates the file if it doesn't exist.
* @returns {Promise<BackupRecord[]>} A promise that resolves to an array of backup records.
*/
const readRecords = async (): Promise<BackupRecord[]> => {
try {
// Check if the database file exists
if (!fs.existsSync(DB_PATH)) {
// If not, create it with an empty array
await fs.promises.writeFile(DB_PATH, JSON.stringify([]), 'utf-8');
return [];
}
// If it exists, read and parse it
const fileContent = await fs.promises.readFile(DB_PATH, 'utf-8');
// Handle case where file is empty
return fileContent ? JSON.parse(fileContent) : [];
} catch (error) {
console.error("Error reading backup history:", error);
// Return an empty array in case of any error to prevent crashes
return [];
}
};
/**
* API handler for fetching backup history.
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// This endpoint only supports GET requests
if (req.method !== 'GET') {
res.setHeader('Allow', ['GET']);
return res.status(405).json({ message: `Method ${req.method} Not Allowed` });
}
try {
const records = await readRecords();
// Sort records by date, newest first, to display the latest jobs on top
const sortedRecords = records.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
return res.status(200).json(sortedRecords);
} catch (error) {
console.error("API Error fetching history:", error);
return res.status(500).json({ error: 'Failed to retrieve backup history.' });
}
}
+13
View File
@@ -0,0 +1,13 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
type Data = {
name: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
res.status(200).json({ name: "John Doe" });
}
+90
View File
@@ -0,0 +1,90 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Client as PgClient } from 'pg';
import mysql from 'mysql2/promise';
import { MongoClient } from 'mongodb';
/**
* Handles testing the connection to a database.
* This version uses native Node.js drivers for reliability and security.
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method Not Allowed' });
}
const {
dbType,
dbHost,
dbPort,
dbUser,
dbPassword,
dbName,
dbRequireSsl
} = req.body;
if (!dbType || !dbHost || !dbPort || !dbUser) {
return res.status(400).json({ error: 'Missing required database credentials for testing.' });
}
switch (dbType) {
case 'postgresql':
const pgClient = new PgClient({
host: dbHost,
port: Number(dbPort),
user: dbUser,
password: dbPassword,
database: dbName,
ssl: dbRequireSsl ? { rejectUnauthorized: false } : false,
// ✅ **التعديل: زيادة مهلة الاتصال إلى 30 ثانية**
connectionTimeoutMillis: 30000,
});
try {
await pgClient.connect();
await pgClient.end();
return res.status(200).json({ message: 'PostgreSQL connection successful!' });
} catch (error: any) {
return res.status(500).json({ error: error.message });
}
case 'mysql':
let mysqlConnection;
try {
mysqlConnection = await mysql.createConnection({
host: dbHost,
port: Number(dbPort),
user: dbUser,
password: dbPassword,
database: dbName,
ssl: dbRequireSsl ? { rejectUnauthorized: false } : undefined,
connectTimeout: 30000, // 30 second timeout
});
await mysqlConnection.end();
return res.status(200).json({ message: 'MySQL connection successful!' });
} catch (error: any) {
// Ensure connection is closed even if it fails
if (mysqlConnection) await mysqlConnection.end();
return res.status(500).json({ error: error.message });
}
case 'mongodb':
const mongoURI = `mongodb://${dbUser}:${encodeURIComponent(dbPassword || '')}@${dbHost}:${dbPort}/${dbName || ''}?authSource=admin`;
const mongoClient = new MongoClient(mongoURI, {
ssl: dbRequireSsl,
serverSelectionTimeoutMS: 30000, // 30 second timeout
});
try {
await mongoClient.connect();
await mongoClient.db("admin").command({ ping: 1 });
await mongoClient.close();
return res.status(200).json({ message: 'MongoDB connection successful!' });
} catch (error: any) {
await mongoClient.close();
return res.status(500).json({ error: error.message });
}
default:
return res.status(400).json({ error: 'Unsupported database type' });
}
}
+442
View File
@@ -0,0 +1,442 @@
import { useState, useEffect } from 'react';
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "sonner";
import { useTheme } from "next-themes";
import { Loader2, CheckCircle, XCircle, Wand2, Moon, Sun, ShieldCheck, History, ArrowRight, ArrowLeft, Download, Database, Server, Cloud } from "lucide-react";
import { Checkbox } from '@/components/ui/checkbox';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
// It's a good practice to define types for complex objects
type BackupRecord = {
id: string;
dbName: string;
status: 'COMPLETED' | 'FAILED' | 'PROCESSING' | 'QUEUED';
createdAt: string;
fileName?: string;
error?: string;
downloadUrl?: string;
};
// Define a type for the form data to ensure type safety
type BackupFormData = {
dbType: string;
dbHost: string;
dbPort: string;
dbUser: string;
dbPassword: string;
dbName: string;
dbRequireSsl: boolean;
s3Endpoint: string;
s3BucketName: string;
s3AccessKey: string;
s3SecretKey: string;
s3Region: string;
cronExpression: string;
};
type View = 'form' | 'history';
type Step = 'database' | 's3' | 'schedule';
type ConnectionStatus = 'idle' | 'success' | 'error' | 'testing';
/**
* Main component for the Backup System UI
*/
export default function HomePage() {
const { setTheme, theme } = useTheme();
const [view, setView] = useState<View>('form');
// State for the main backup form
const [formData, setFormData] = useState<BackupFormData>({
dbType: 'postgresql',
dbHost: '', dbPort: '5432', dbUser: '', dbPassword: '', dbName: '', dbRequireSsl: true,
s3Endpoint: '', s3BucketName: '', s3AccessKey: '', s3SecretKey: '', s3Region: 'us-east-1',
cronExpression: '',
});
// UI/UX State
const [currentStep, setCurrentStep] = useState<Step>('database');
const [isLoading, setIsLoading] = useState(false);
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('idle');
// State for the real-time backup process modal
const [isBackupInProgress, setIsBackupInProgress] = useState(false);
const [backupLogs, setBackupLogs] = useState<string[]>([]);
const [backupStatus, setBackupStatus] = useState<'pending' | 'completed' | 'failed'>('pending');
// --- Event Handlers ---
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Reset connection status if database details change
if (name.startsWith('db')) {
setConnectionStatus('idle');
}
};
const handleSelectChange = (name: string, value: string) => {
setFormData(prev => ({ ...prev, [name]: value }));
if (name === 'dbType') {
const ports: { [key: string]: string } = { postgresql: '5432' };
setFormData(prev => ({ ...prev, dbPort: ports[value] || '' }));
setConnectionStatus('idle');
}
};
const handleCheckboxChange = (name: string, checked: boolean) => {
setFormData(prev => ({ ...prev, [name]: checked }));
if (name.startsWith('db')) {
setConnectionStatus('idle');
}
};
// --- API Calls ---
const handleTestConnection = async () => {
setConnectionStatus('testing');
try {
const response = await fetch('/api/test-connection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const result = await response.json();
if (response.ok) {
setConnectionStatus('success');
toast.success("Connection successful!");
} else {
setConnectionStatus('error');
toast.error("Connection failed.", { description: result.error || "Please check credentials and network." });
}
} catch (error: any) {
setConnectionStatus('error');
toast.error("Connection failed.", { description: error?.message || "An unknown error occurred." });
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// If cron expression exists, it's a schedule job, not a real-time one
if (formData.cronExpression) {
setIsLoading(true);
try {
const response = await fetch('/api/backup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const result = await response.json();
if (response.ok) {
toast.success("Success! 🎉", { description: "Backup job has been scheduled." });
setView('history');
} else {
toast.error("Uh oh! Something went wrong.", { description: result.message || result.error });
}
} catch (error: any) {
toast.error("Failed to schedule backup.", { description: error?.message });
} finally {
setIsLoading(false);
}
return;
}
// --- Start Real-time Backup via Server-Sent Events ---
setIsBackupInProgress(true);
setBackupLogs([]);
setBackupStatus('pending');
const queryParams = new URLSearchParams();
// A more type-safe way to append params
Object.entries(formData).forEach(([key, value]) => {
queryParams.append(key, String(value));
});
const eventSource = new EventSource(`/api/backup-stream?${queryParams.toString()}`);
eventSource.onopen = () => {
setBackupLogs(prev => [...prev, "Connection to server established. Starting backup..."]);
};
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.message) {
setBackupLogs((prevLogs) => [...prevLogs, data.message]);
}
if (data.status) {
setBackupStatus(data.status);
if (['completed', 'failed', 'closed'].includes(data.status)) {
eventSource.close();
if (data.status === 'completed') {
toast.success("Backup completed successfully!");
} else if (data.status === 'failed') {
toast.error("Backup failed.", { description: "Check logs for more details."});
}
}
}
};
eventSource.onerror = (err) => {
toast.error("Connection to backup stream failed.");
setBackupStatus('failed');
setBackupLogs((prev) => [...prev, "Stream connection closed unexpectedly. The server might have terminated the process."]);
eventSource.close();
};
};
// --- Step Navigation ---
const nextStep = () => {
if (currentStep === 'database') setCurrentStep('s3');
if (currentStep === 's3') setCurrentStep('schedule');
};
const prevStep = () => {
if (currentStep === 'schedule') setCurrentStep('s3');
if (currentStep === 's3') setCurrentStep('database');
};
// --- Render Logic ---
const renderStepContent = () => {
switch(currentStep) {
case 'database': return <DatabaseStepContent formData={formData} connectionStatus={connectionStatus} onTestConnection={handleTestConnection} onChange={handleChange} onSelectChange={(v: string) => handleSelectChange('dbType', v)} onCheckboxChange={(c: boolean) => handleCheckboxChange('dbRequireSsl', c)} />;
case 's3': return <S3StepContent formData={formData} onChange={handleChange} />;
case 'schedule': return <ScheduleStepContent formData={formData} onChange={handleChange} />;
default: return null;
}
};
return (
<main className="container mx-auto flex min-h-screen flex-col items-center justify-center p-4">
<div className="absolute top-4 right-4 flex items-center gap-2">
<Button variant="outline" onClick={() => setView(view === 'form' ? 'history' : 'form')}>
{view === 'form' ? <History className="mr-2 h-4 w-4"/> : <Wand2 className="mr-2 h-4 w-4" />}
{view === 'form' ? 'View History' : 'New Backup'}
</Button>
<Button variant="outline" size="icon" onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</div>
{view === 'form' ? (
<Card className="w-full max-w-2xl animate-in fade-in-50 duration-500">
<form onSubmit={handleSubmit}>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-2xl"><ShieldCheck /> Secure Backup System</CardTitle>
<CardDescription>Configure and run a secure, one-time or scheduled database backup.</CardDescription>
</CardHeader>
{renderStepContent()}
<CardFooter className="flex justify-between">
<Button type="button" variant="outline" onClick={prevStep} disabled={currentStep === 'database'}>
<ArrowLeft className="mr-2 h-4 w-4" /> Previous
</Button>
{currentStep !== 'schedule' ? (
<Button type="button" onClick={nextStep} disabled={currentStep === 'database' && connectionStatus !== 'success'}>
Next <ArrowRight className="ml-2 h-4 w-4" />
</Button>
) : (
<Button type="submit" disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{formData.cronExpression ? 'Schedule Backup' : 'Run Backup Now'}
</Button>
)}
</CardFooter>
</form>
</Card>
) : (
<HistoryView />
)}
{/* Real-time Backup Progress Modal */}
<Dialog open={isBackupInProgress} onOpenChange={setIsBackupInProgress}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Backup in Progress...</DialogTitle>
<DialogDescription>Please keep this window open until the process is complete. Logs are streamed from the server.</DialogDescription>
</DialogHeader>
<div className="mt-4 h-64 overflow-y-auto rounded-md bg-muted p-4 font-mono text-xs selection:bg-primary selection:text-primary-foreground">
{backupLogs.map((log, index) => (<p key={index} className="whitespace-pre-wrap">{log}</p>))}
</div>
<DialogFooter className="sm:justify-between">
<div className="flex items-center gap-2">
{backupStatus === 'completed' && <><CheckCircle className="h-5 w-5 text-green-500" /><p className="text-sm text-green-500">Backup completed successfully!</p></>}
{backupStatus === 'failed' && <><XCircle className="h-5 w-5 text-red-500" /><p className="text-sm text-red-500">Backup failed. Check logs for details.</p></>}
{backupStatus === 'pending' && <><Loader2 className="h-5 w-5 animate-spin" /><p className="text-sm text-muted-foreground">Processing...</p></>}
</div>
<Button onClick={() => { setIsBackupInProgress(false); if (backupStatus !== 'pending') setView('history'); }}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</main>
);
}
// --- Sub-components for Form Steps with defined Prop Types ---
type DatabaseStepProps = {
formData: BackupFormData;
connectionStatus: ConnectionStatus;
onTestConnection: () => void;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSelectChange: (value: string) => void;
onCheckboxChange: (checked: boolean) => void;
};
const DatabaseStepContent = ({ formData, connectionStatus, onTestConnection, onChange, onSelectChange, onCheckboxChange }: DatabaseStepProps) => (
<CardContent className="space-y-6 pt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="dbType">Database Type</Label>
<Select onValueChange={onSelectChange} defaultValue={formData.dbType}>
<SelectTrigger id="dbType"><SelectValue /></SelectTrigger>
<SelectContent><SelectItem value="postgresql">PostgreSQL</SelectItem></SelectContent>
</Select>
</div>
<div className="space-y-2"><Label htmlFor="dbHost">Host</Label><Input id="dbHost" name="dbHost" value={formData.dbHost} onChange={onChange} placeholder="e.g., db.example.com" required /></div>
<div className="space-y-2"><Label htmlFor="dbPort">Port</Label><Input id="dbPort" name="dbPort" value={formData.dbPort} onChange={onChange} required /></div>
<div className="space-y-2"><Label htmlFor="dbUser">User</Label><Input id="dbUser" name="dbUser" value={formData.dbUser} onChange={onChange} placeholder="e.g., backup_user" required /></div>
<div className="space-y-2"><Label htmlFor="dbPassword">Password</Label><Input id="dbPassword" name="dbPassword" type="password" value={formData.dbPassword} onChange={onChange} /></div>
<div className="space-y-2"><Label htmlFor="dbName">Database Name</Label><Input id="dbName" name="dbName" value={formData.dbName} onChange={onChange} placeholder="e.g., production_db" required /></div>
</div>
<div className="flex items-center space-x-2 pt-2">
<Checkbox id="dbRequireSsl" checked={formData.dbRequireSsl} onCheckedChange={onCheckboxChange} />
<label htmlFor="dbRequireSsl" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Require SSL Connection</label>
</div>
<div className="flex items-center justify-end space-x-2 pt-4 border-t mt-4">
{connectionStatus === 'success' && <CheckCircle className="h-5 w-5 text-green-500" />}
{connectionStatus === 'error' && <XCircle className="h-5 w-5 text-red-500" />}
<Button type="button" variant="outline" onClick={onTestConnection} disabled={connectionStatus === 'testing'}>
{connectionStatus === 'testing' && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} Test Connection
</Button>
</div>
</CardContent>
);
type S3StepProps = {
formData: BackupFormData;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const S3StepContent = ({ formData, onChange }: S3StepProps) => (
<CardContent className="space-y-4 pt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2 sm:col-span-2"><Label htmlFor="s3Endpoint">S3-Compatible Endpoint URL</Label><Input id="s3Endpoint" name="s3Endpoint" value={formData.s3Endpoint} onChange={onChange} placeholder="e.g., s3.us-west-2.amazonaws.com" required /></div>
<div className="space-y-2"><Label htmlFor="s3BucketName">Bucket Name</Label><Input id="s3BucketName" name="s3BucketName" value={formData.s3BucketName} onChange={onChange} required /></div>
<div className="space-y-2"><Label htmlFor="s3Region">Region</Label><Input id="s3Region" name="s3Region" value={formData.s3Region} onChange={onChange} required /></div>
<div className="space-y-2"><Label htmlFor="s3AccessKey">Access Key</Label><Input id="s3AccessKey" name="s3AccessKey" value={formData.s3AccessKey} type="password" onChange={onChange} required /></div>
<div className="space-y-2"><Label htmlFor="s3SecretKey">Secret Key</Label><Input id="s3SecretKey" name="s3SecretKey" type="password" value={formData.s3SecretKey} onChange={onChange} required /></div>
</div>
</CardContent>
);
type ScheduleStepProps = {
formData: BackupFormData;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const ScheduleStepContent = ({ formData, onChange }: ScheduleStepProps) => (
<CardContent className="space-y-4 pt-6">
<div className="space-y-2">
<Label htmlFor="cronExpression">Cron Expression (Optional)</Label>
<Input id="cronExpression" name="cronExpression" placeholder="e.g., 0 2 * * * (for 2 AM daily)" value={formData.cronExpression} onChange={onChange} />
<p className="text-sm text-muted-foreground">Leave empty to run backup once immediately. Use <a href="https://crontab.guru/" target="_blank" rel="noopener noreferrer" className="text-primary underline">crontab.guru</a> to build expressions.</p>
</div>
</CardContent>
);
/**
* Component to display the history of backup jobs
*/
function HistoryView() {
const [records, setRecords] = useState<BackupRecord[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchRecords = async () => {
try {
const response = await fetch('/api/backups');
if(response.ok) {
const data = await response.json();
setRecords(data);
} else {
toast.error("Could not fetch backup history.");
}
} catch (error) {
console.error("Failed to fetch backup records:", error);
toast.error("Could not fetch backup history.");
} finally {
setIsLoading(false);
}
};
fetchRecords();
const interval = setInterval(fetchRecords, 5000); // Refresh every 5 seconds
return () => clearInterval(interval);
}, []);
return (
<Card className="w-full max-w-4xl animate-in fade-in-50 duration-500">
<CardHeader>
<CardTitle>Backup History</CardTitle>
<CardDescription>A list of all scheduled and completed backup jobs. Refreshes automatically.</CardDescription>
</CardHeader>
<CardContent>
{isLoading && records.length === 0 ? (
<div className="flex flex-col justify-center items-center p-8 gap-4">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<p className="text-muted-foreground">Loading history...</p>
</div>
) : records.length === 0 ? (
<p className="text-center text-muted-foreground p-8">No backup jobs found yet.</p>
) : (
<div className="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Database</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created At</TableHead>
<TableHead>Filename / Error</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{records.map((record) => (
<TableRow key={record.id}>
<TableCell className="font-medium flex items-center gap-2"><Database size={16}/> {record.dbName}</TableCell>
<TableCell>
<Badge variant={record.status === 'COMPLETED' ? 'default' : record.status === 'FAILED' ? 'destructive' : 'secondary'}>
{(record.status === 'QUEUED' || record.status === 'PROCESSING') && <Loader2 className="mr-2 h-3 w-3 animate-spin" />}
{record.status}
</Badge>
</TableCell>
<TableCell>{new Date(record.createdAt).toLocaleString()}</TableCell>
<TableCell className="text-xs max-w-xs truncate" title={record.fileName || record.error}>{record.fileName || record.error}</TableCell>
<TableCell className="text-right">
{record.status === 'COMPLETED' && record.downloadUrl && (
<Button asChild variant="outline" size="sm">
<a href={record.downloadUrl} target="_blank" rel="noopener noreferrer">
<Download className="mr-2 h-4 w-4"/> Download
</a>
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
);
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+5
View File
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
BIN
View File
Binary file not shown.
@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "BackupRecord" (
"id" TEXT NOT NULL PRIMARY KEY,
"dbName" TEXT NOT NULL,
"status" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" DATETIME,
"fileName" TEXT,
"downloadUrl" TEXT,
"error" TEXT
);
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"
+30
View File
@@ -0,0 +1,30 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// ✅ إضافة نوع جديد للحالة
enum BackupStatus {
QUEUED
PROCESSING
COMPLETED
FAILED
}
model BackupRecord {
id String @id @default(cuid())
dbName String
status BackupStatus @default(QUEUED) // ✅ استخدام النوع الجديد
createdAt DateTime @default(now())
completedAt DateTime?
fileName String?
downloadUrl String?
error String?
// ✅ إضافة حقل جديد لتخزين إعدادات النسخ الاحتياطي
configJson String
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

+63
View File
@@ -0,0 +1,63 @@
/* 📍 الملف: styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Olive Green Theme */
--background: 48 20% 95%;
--foreground: 48 10% 20%;
--card: 48 25% 98%;
--card-foreground: 48 10% 20%;
--popover: 48 25% 98%;
--popover-foreground: 48 10% 20%;
--primary: 80 44% 40%;
--primary-foreground: 60 93% 96%;
--secondary: 48 15% 90%;
--secondary-foreground: 80 44% 40%;
--muted: 48 15% 90%;
--muted-foreground: 48 10% 40%;
--accent: 48 15% 90%;
--accent-foreground: 80 44% 40%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border: 48 15% 85%;
--input: 48 15% 85%;
--ring: 80 44% 40%;
--radius: 0.5rem;
}
.dark {
/* Dark Theme Example */
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+75
View File
@@ -0,0 +1,75 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.