+
{key}
{sidebarItems[key].map((item: SidebarItem, i) => (
diff --git a/dashboard/src/features/theme-switch/theme-toggle/index.tsx b/dashboard/src/features/theme-switch/theme-toggle/index.tsx
index 55eab04..60ed833 100644
--- a/dashboard/src/features/theme-switch/theme-toggle/index.tsx
+++ b/dashboard/src/features/theme-switch/theme-toggle/index.tsx
@@ -8,10 +8,11 @@ import {
DropdownMenuTrigger,
} from "@marzneshin/components"
import { useTheme } from "../theme-provider"
+import { useTranslation } from "react-i18next"
export function ThemeToggle() {
const { setTheme } = useTheme()
-
+ const { t } = useTranslation()
return (
@@ -23,13 +24,13 @@ export function ThemeToggle() {
setTheme("light")}>
- Light
+ {t('light')}
setTheme("dark")}>
- Dark
+ {t('dark')}
setTheme("system")}>
- System
+ {t('system')}
diff --git a/dashboard/src/features/users/index.ts b/dashboard/src/features/users/index.ts
new file mode 100644
index 0000000..c56bbc5
--- /dev/null
+++ b/dashboard/src/features/users/index.ts
@@ -0,0 +1,2 @@
+export * from './services'
+export * from './types'
diff --git a/dashboard/src/features/users/services/create-service.mutate.ts b/dashboard/src/features/users/services/create-service.mutate.ts
new file mode 100644
index 0000000..26b9ca0
--- /dev/null
+++ b/dashboard/src/features/users/services/create-service.mutate.ts
@@ -0,0 +1,40 @@
+import { UserCreateType, UsersQueryFetchKey } from "@marzneshin/features/users";
+import { useMutation } from "@tanstack/react-query";
+import { fetch, queryClient } from "@marzneshin/utils";
+import { toast } from "sonner";
+import i18n from "@marzneshin/features/i18n";
+
+export async function fetchCreateUser(user: UserCreateType): Promise
{
+ return fetch('/users', { method: 'post', body: user }).then((user) => {
+ return user;
+ });
+}
+
+const handleError = (error: Error, value: UserCreateType) => {
+ toast.error(
+ i18n.t('events.create.error', { name: value.username }),
+ {
+ description: error.message
+ })
+}
+
+const handleSuccess = (value: UserCreateType) => {
+ toast.success(
+ i18n.t('events.create.success.title', { name: value.username }),
+ {
+ description: i18n.t('events.create.success.desc')
+ })
+ queryClient.invalidateQueries({ queryKey: [UsersQueryFetchKey] })
+}
+
+
+const UsersCreateFetchKey = "users";
+
+export const useUsersCreationMutation = () => {
+ return useMutation({
+ mutationKey: [UsersCreateFetchKey],
+ mutationFn: fetchCreateUser,
+ onError: handleError,
+ onSuccess: handleSuccess,
+ })
+}
diff --git a/dashboard/src/features/users/services/delete-service.mutate.ts b/dashboard/src/features/users/services/delete-service.mutate.ts
new file mode 100644
index 0000000..f405ff6
--- /dev/null
+++ b/dashboard/src/features/users/services/delete-service.mutate.ts
@@ -0,0 +1,40 @@
+import { UserType, UsersQueryFetchKey } from "@marzneshin/features/users";
+import { useMutation } from "@tanstack/react-query";
+import { fetch, queryClient } from "@marzneshin/utils";
+import { toast } from "sonner";
+import i18n from "@marzneshin/features/i18n";
+
+export async function fetchDeleteUser(user: UserType): Promise {
+ return fetch(`/users/${user.username}`, { method: 'delete' }).then((user) => {
+ return user;
+ });
+}
+
+const UsersDeleteFetchKey = "users-delete-fetch-key";
+
+
+const handleError = (error: Error, value: UserType) => {
+ toast.error(
+ i18n.t('events.delete.error', { name: value.username }),
+ {
+ description: error.message
+ })
+}
+
+const handleSuccess = (value: UserType) => {
+ toast.success(
+ i18n.t('events.delete.success.title', { name: value.username }),
+ {
+ description: i18n.t('events.delete.success.desc')
+ })
+ queryClient.invalidateQueries({ queryKey: [UsersQueryFetchKey] })
+}
+
+export const useUsersDeletionMutation = () => {
+ return useMutation({
+ mutationKey: [UsersDeleteFetchKey],
+ mutationFn: fetchDeleteUser,
+ onError: handleError,
+ onSuccess: handleSuccess,
+ })
+}
diff --git a/dashboard/src/features/users/services/index.ts b/dashboard/src/features/users/services/index.ts
new file mode 100644
index 0000000..09bb9ea
--- /dev/null
+++ b/dashboard/src/features/users/services/index.ts
@@ -0,0 +1,4 @@
+export * from './users.query'
+export * from './update-service.mutate'
+export * from './create-service.mutate'
+export * from './delete-service.mutate'
diff --git a/dashboard/src/features/users/services/update-service.mutate.ts b/dashboard/src/features/users/services/update-service.mutate.ts
new file mode 100644
index 0000000..711ee5d
--- /dev/null
+++ b/dashboard/src/features/users/services/update-service.mutate.ts
@@ -0,0 +1,41 @@
+import { useMutation } from "@tanstack/react-query";
+import { fetch, queryClient } from "@marzneshin/utils";
+import { toast } from "sonner";
+import i18n from "@marzneshin/features/i18n";
+import { UsersQueryFetchKey } from "./users.query";
+import { UserType } from "../types";
+
+export async function updateUser(user: UserType): Promise {
+ return fetch(`/users/${user.username}`, { method: 'put', body: user }).then((user) => {
+ return user;
+ });
+}
+
+const handleError = (error: Error, value: UserType) => {
+ toast.error(
+ i18n.t('events.update.error', { name: value.username }),
+ {
+ description: error.message
+ })
+}
+
+const handleSuccess = (value: UserType) => {
+ toast.success(
+ i18n.t('events.update.success.title', { name: value.username }),
+ {
+ description: i18n.t('events.update.success.desc')
+ })
+ queryClient.invalidateQueries({ queryKey: [UsersQueryFetchKey] })
+}
+
+
+const UsersUpdateFetchKey = "users";
+
+export const useUsersUpdateMutation = () => {
+ return useMutation({
+ mutationKey: [UsersUpdateFetchKey],
+ mutationFn: updateUser,
+ onError: handleError,
+ onSuccess: handleSuccess,
+ })
+}
diff --git a/dashboard/src/features/users/services/users.query.ts b/dashboard/src/features/users/services/users.query.ts
new file mode 100644
index 0000000..7300a1f
--- /dev/null
+++ b/dashboard/src/features/users/services/users.query.ts
@@ -0,0 +1,29 @@
+import { useQuery } from "@tanstack/react-query";
+import { fetch } from "@marzneshin/utils";
+import { UserType } from "../types";
+
+interface UserResponse extends UserType {
+ service_ids: number[]
+ services: any[]
+}
+
+export async function fetchUsers(): Promise {
+ return fetch('/users').then((result) => {
+ const users: UserType[] = result.items.map((fetchedUser: UserResponse) => {
+ const user: UserType = fetchedUser;
+ user.services = fetchedUser.service_ids
+ return user
+ })
+ return users;
+ });
+}
+
+export const UsersQueryFetchKey = "users";
+
+export const useUsersQuery = () => {
+ return useQuery({
+ queryKey: [UsersQueryFetchKey],
+ queryFn: fetchUsers,
+ initialData: []
+ })
+}
diff --git a/dashboard/src/features/users/tables/index.ts b/dashboard/src/features/users/tables/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/dashboard/src/features/users/types/index.ts b/dashboard/src/features/users/types/index.ts
new file mode 100644
index 0000000..1f88b01
--- /dev/null
+++ b/dashboard/src/features/users/types/index.ts
@@ -0,0 +1,44 @@
+export type DataLimitResetStrategy =
+ | 'no_reset'
+ | 'day'
+ | 'week'
+ | 'month'
+ | 'year';
+
+export type Status =
+ | 'active'
+ | 'disabled'
+ | 'limited'
+ | 'expired'
+ | 'on_hold'
+ | 'error'
+ | 'connecting'
+ | 'healthy'
+ | 'unhealthy'
+ | 'connected';
+
+export interface UserType {
+ expire: Date | number;
+ data_limit: number | null;
+ data_limit_reset_strategy: DataLimitResetStrategy;
+ lifetime_used_traffic: number;
+ username: string;
+ used_traffic: number;
+ status: Status;
+ links: string[];
+ subscription_url: string;
+ services: number[];
+ note: string;
+ online_at: string;
+}
+
+export type UserCreateType = Pick<
+ UserType,
+ | 'services'
+ | 'expire'
+ | 'data_limit'
+ | 'data_limit_reset_strategy'
+ | 'username'
+ | 'status'
+ | 'note'
+>;
diff --git a/dashboard/src/routeTree.gen.ts b/dashboard/src/routeTree.gen.ts
index 0cbf539..91f362a 100644
--- a/dashboard/src/routeTree.gen.ts
+++ b/dashboard/src/routeTree.gen.ts
@@ -1,6 +1,6 @@
/* prettier-ignore-start */
-
+/* eslint-disable */
// @ts-nocheck
diff --git a/dashboard/src/routes/_auth.tsx b/dashboard/src/routes/_auth.tsx
index 9a7aaf4..a01a2e7 100644
--- a/dashboard/src/routes/_auth.tsx
+++ b/dashboard/src/routes/_auth.tsx
@@ -1,4 +1,5 @@
import { Outlet, createFileRoute } from '@tanstack/react-router'
+import network from '@marzneshin/assets/undraw_connected_world_wuay.svg'
const AuthLayout = () => {
return (
@@ -6,7 +7,9 @@ const AuthLayout = () => {
-
+
+

+
)
}
diff --git a/dashboard/src/routes/_dashboard.tsx b/dashboard/src/routes/_dashboard.tsx
index f08a388..dc569c2 100644
--- a/dashboard/src/routes/_dashboard.tsx
+++ b/dashboard/src/routes/_dashboard.tsx
@@ -16,7 +16,7 @@ import { cn } from '@marzneshin/utils';
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'
export const DashboardLayout = () => {
- const isDesktop = useScreenBreakpoint("sm");
+ const isDesktop = useScreenBreakpoint("md");
const { collapsed, panelRef, open, setCollapsed, setOpen, toggleCollapse, toggleOpen } = usePanelToggle(isDesktop);
return (
@@ -37,11 +37,11 @@ export const DashboardLayout = () => {
setCollapsed(true)}
onExpand={() => setCollapsed(false)}
minSize={15}
- className={cn("w-[120px] min-w-[100px]")}
+ className={cn("w-[120px] min-w-[70px]")}
defaultSize={20}
ref={panelRef}
maxSize={30}>
@@ -51,6 +51,7 @@ export const DashboardLayout = () => {
+
diff --git a/dashboard/src/routes/_dashboard/hosts.tsx b/dashboard/src/routes/_dashboard/hosts.tsx
index a73508f..e7f1508 100644
--- a/dashboard/src/routes/_dashboard/hosts.tsx
+++ b/dashboard/src/routes/_dashboard/hosts.tsx
@@ -1,5 +1,26 @@
+import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@marzneshin/components'
+import { InboundHostsTable } from '@marzneshin/features/hosts'
import { createFileRoute } from '@tanstack/react-router'
+import { useTranslation } from 'react-i18next'
+
+export const HostsPage = () => {
+ const { t } = useTranslation()
+ return (
+
+
+
+ {t('hosts')}
+
+
+
+
+
+
+
+
+ )
+}
export const Route = createFileRoute('/_dashboard/hosts')({
- component: () =>
Hello /_dashboard/hosts!
-})
\ No newline at end of file
+ component: () =>
+})
diff --git a/dashboard/src/routes/_dashboard/services.tsx b/dashboard/src/routes/_dashboard/services.tsx
index 1206c52..b225bc2 100644
--- a/dashboard/src/routes/_dashboard/services.tsx
+++ b/dashboard/src/routes/_dashboard/services.tsx
@@ -1,5 +1,33 @@
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle
+} from '@marzneshin/components'
+import { ServicesTable } from '@marzneshin/features/services';
import { createFileRoute } from '@tanstack/react-router'
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+
+export const ServicesPage: FC = () => {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {t('services')}
+
+
+
+
+
+
+
+
+ )
+};
export const Route = createFileRoute('/_dashboard/services')({
- component: () =>
Hello /_dashboard/services!
+ component: () =>
})
diff --git a/dashboard/src/routes/_dashboard/users.tsx b/dashboard/src/routes/_dashboard/users.tsx
index 383e1be..90e7a81 100644
--- a/dashboard/src/routes/_dashboard/users.tsx
+++ b/dashboard/src/routes/_dashboard/users.tsx
@@ -1,5 +1,27 @@
+import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@marzneshin/components';
import { createFileRoute } from '@tanstack/react-router'
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+
+
+export const UsersPage: FC = () => {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {t('users')}
+
+
+
+
+
+
+
+ )
+};
+
export const Route = createFileRoute('/_dashboard/users')({
- component: () =>
Hello /_dashboard/users!
-})
\ No newline at end of file
+ component: () =>
+})
diff --git a/dashboard/src/types/index.ts b/dashboard/src/types/index.ts
index be5b633..a3b2f8e 100644
--- a/dashboard/src/types/index.ts
+++ b/dashboard/src/types/index.ts
@@ -1,4 +1,2 @@
-import { routeTree } from "@marzneshin/routeTree.gen";
-import { RoutePaths } from "@tanstack/react-router";
-
-export type AppRouterPaths = RoutePaths
;
+export * from './status'
+export * from './routes'
diff --git a/dashboard/src/types/routes.ts b/dashboard/src/types/routes.ts
new file mode 100644
index 0000000..be5b633
--- /dev/null
+++ b/dashboard/src/types/routes.ts
@@ -0,0 +1,4 @@
+import { routeTree } from "@marzneshin/routeTree.gen";
+import { RoutePaths } from "@tanstack/react-router";
+
+export type AppRouterPaths = RoutePaths;
diff --git a/dashboard/src/types/status.ts b/dashboard/src/types/status.ts
new file mode 100644
index 0000000..55ab09e
--- /dev/null
+++ b/dashboard/src/types/status.ts
@@ -0,0 +1,6 @@
+import { LucideIcon } from "lucide-react";
+
+export interface StatusType {
+ label: string;
+ icon: LucideIcon | null;
+}
diff --git a/dashboard/tailwind.config.js b/dashboard/tailwind.config.js
index 5085b7c..2de6fcc 100644
--- a/dashboard/tailwind.config.js
+++ b/dashboard/tailwind.config.js
@@ -2,6 +2,7 @@ import tailwindcssAnimate from 'tailwindcss-animate';
import tailwindcssForm from '@tailwindcss/forms';
import tailwindcssAspectRatio from '@tailwindcss/aspect-ratio';
import tailwindcssTypography from '@tailwindcss/typography';
+import { nextui } from '@nextui-org/react'
/** @type {import('tailwindcss').Config} */
const config = {
@@ -11,9 +12,16 @@ const config = {
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
+ "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
],
prefix: "",
theme: {
+ screens: {
+ 'sm': '0',
+ 'md': '768px',
+ 'lg': '1024px',
+ 'xl': '1280px',
+ },
container: {
center: true,
padding: "2rem",
@@ -84,6 +92,7 @@ const config = {
},
},
plugins: [
+ nextui(),
tailwindcssAnimate,
tailwindcssForm,
tailwindcssAspectRatio,