gitscope
← marzneshin / Diff · 3c1143b → 3c1143b
Changes 3c1143b..3c1143b
Patch commit
diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json
index dcabd5d..7fb1fea 100644
--- a/dashboard/public/locales/en.json
+++ b/dashboard/public/locales/en.json
@@ -49,6 +49,8 @@
"system": "System",
"disabled": "Disabled",
"advanced-options": "Advanced Options",
+ "language": "Language",
+ "theme": "Theme",
"support-us": {
"title": "Support Us",
"desc": "Marzneshin is free and open-source software (FOSS), join us and show your support by donating.",
diff --git a/dashboard/src/components/header/index.tsx b/dashboard/src/components/header/index.tsx
index 5bf28ec..edaa44c 100644
--- a/dashboard/src/components/header/index.tsx
+++ b/dashboard/src/components/header/index.tsx
@@ -1,27 +1,28 @@
-import { LanguageSwitch } from "@marzneshin/features/language-switch";
-import { ThemeToggle } from "@marzneshin/features/theme-switch";
-import { Logout } from "@marzneshin/features/auth";
+import { HeaderMenu } from "./menu";
+import { GithubRepo } from "@marzneshin/features/github-repo";
import { Link } from "@tanstack/react-router";
import { CommandBox } from "@marzneshin/features/search-command";
import type { FC, PropsWithChildren } from "react";
+import { useScreenBreakpoint } from "@marzneshin/hooks/use-screen-breakpoint";
export const Header: FC<PropsWithChildren> = ({ children }) => {
- return (
- <div className="flex flex-row justify-between items-center p-1 px-4 w-full h-full bg-primary text-primary-foreground dark:bg-primary-foreground">
- <div className="flex flex-row gap-4 justify-center items-center">
- <Link to="/">
- <div className="flex flex-col justify-center items-center p-2 h-10 text-2xl font-bold bg-gray-800 rounded-lg font-header text-secondary border-accent dark:text-secondary-foreground">
- M
- </div>
- </Link>
- {children}
- </div>
- <CommandBox />
- <div className="flex flex-row gap-2 h-10 justify-reverse">
- <Logout />
- <LanguageSwitch />
- <ThemeToggle />
- </div>
- </div>
- );
+ const isDesktop = useScreenBreakpoint("md");
+
+ return (
+ <div className="flex flex-row justify-between items-center p-1 px-4 w-full h-full bg-primary text-primary-foreground dark:bg-primary-foreground">
+ <div className="flex flex-row gap-4 justify-center items-center">
+ <Link to="/">
+ <div className="flex flex-col justify-center items-center p-2 h-10 text-2xl font-bold bg-gray-800 rounded-lg font-header text-secondary border-accent dark:text-secondary-foreground">
+ M
+ </div>
+ </Link>
+ {children}
+ <CommandBox />
+ </div>
+ <div className="flex flex-row gap-2 h-10">
+ <GithubRepo variant={isDesktop ? "full" : "mini"} />
+ <HeaderMenu />
+ </div>
+ </div>
+ );
};
diff --git a/dashboard/src/components/header/menu.tsx b/dashboard/src/components/header/menu.tsx
new file mode 100644
index 0000000..f155069
--- /dev/null
+++ b/dashboard/src/components/header/menu.tsx
@@ -0,0 +1,44 @@
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+ DropdownMenuGroup,
+ Button,
+} from "@marzneshin/components";
+import { FC } from 'react';
+import { MenuIcon } from "lucide-react";
+import { LanguageSwitchMenu } from "@marzneshin/features/language-switch";
+import { ThemeToggle } from "@marzneshin/features/theme-switch";
+import { Logout } from "@marzneshin/features/auth";
+
+export const HeaderMenu: FC = () => (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ variant="secondary"
+ className="bg-gray-800 text-secondary dark:hover:bg-secondary-foreground dark:hover:text-secondary dark:text-secondary-foreground"
+ size="icon"
+ >
+ <MenuIcon />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent>
+ <DropdownMenuLabel>Menu</DropdownMenuLabel>
+ <DropdownMenuSeparator />
+ <DropdownMenuGroup>
+ <LanguageSwitchMenu />
+ </DropdownMenuGroup>
+ <DropdownMenuSeparator />
+ <DropdownMenuGroup>
+ <ThemeToggle />
+ </DropdownMenuGroup>
+ <DropdownMenuSeparator />
+ <DropdownMenuItem className="w-full">
+ <Logout />
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu >
+);
diff --git a/dashboard/src/components/ui/button/button.stories.tsx b/dashboard/src/components/ui/button/button.stories.tsx
index 7c98382..b28232e 100644
--- a/dashboard/src/components/ui/button/button.stories.tsx
+++ b/dashboard/src/components/ui/button/button.stories.tsx
@@ -16,6 +16,8 @@ type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
+ variant: "default",
+ size: "default",
onMouseDown: action('clicked'),
children: 'Primary'
}
diff --git a/dashboard/src/components/ui/dropdown-menu.tsx b/dashboard/src/components/ui/dropdown-menu.tsx
index 2c4c3d5..000f13c 100644
--- a/dashboard/src/components/ui/dropdown-menu.tsx
+++ b/dashboard/src/components/ui/dropdown-menu.tsx
@@ -1,6 +1,6 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
-import { Check, ChevronRight, Circle } from "lucide-react"
+import { Check, ChevronLeft, ChevronRight, Circle } from "lucide-react"
import { cn } from "@marzneshin/utils"
@@ -17,182 +17,186 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
- inset?: boolean
- }
->(({ className, inset, children, ...props }, ref) => (
- <DropdownMenuPrimitive.SubTrigger
- ref={ref}
- className={cn(
- "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
- inset && "pl-8",
- className
- )}
- {...props}
- >
- {children}
- <ChevronRight className="ml-auto h-4 w-4" />
- </DropdownMenuPrimitive.SubTrigger>
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
+ inset?: boolean
+ arrowDir?: "right" | "left"
+ }
+>(({ className, inset, children, arrowDir = "right", ...props }, ref) => (
+ <DropdownMenuPrimitive.SubTrigger
+ ref={ref}
+ className={cn(
+ "flex flex-row cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
+ inset && "pl-8",
+ arrowDir === "left" && "flex-row-reverse",
+ className
+ )}
+ {...props}
+ >
+ {children}
+ {arrowDir === "right" ?
+ <ChevronRight className="ml-auto size-4" /> :
+ <ChevronLeft className="mr-auto size-4" />}
+ </DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
- DropdownMenuPrimitive.SubTrigger.displayName
+ DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
- <DropdownMenuPrimitive.SubContent
- ref={ref}
- className={cn(
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
- className
- )}
- {...props}
- />
+ <DropdownMenuPrimitive.SubContent
+ ref={ref}
+ className={cn(
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
+ className
+ )}
+ {...props}
+ />
))
DropdownMenuSubContent.displayName =
- DropdownMenuPrimitive.SubContent.displayName
+ DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
- <DropdownMenuPrimitive.Portal>
- <DropdownMenuPrimitive.Content
- ref={ref}
- sideOffset={sideOffset}
- className={cn(
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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",
- className
- )}
- {...props}
- />
- </DropdownMenuPrimitive.Portal>
+ <DropdownMenuPrimitive.Portal>
+ <DropdownMenuPrimitive.Content
+ ref={ref}
+ sideOffset={sideOffset}
+ className={cn(
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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",
+ className
+ )}
+ {...props}
+ />
+ </DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
- inset?: boolean
- }
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
+ inset?: boolean
+ }
>(({ className, inset, ...props }, ref) => (
- <DropdownMenuPrimitive.Item
- ref={ref}
- className={cn(
- "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- inset && "pl-8",
- className
- )}
- {...props}
- />
+ <DropdownMenuPrimitive.Item
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ />
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
- <DropdownMenuPrimitive.CheckboxItem
- ref={ref}
- className={cn(
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
- )}
- checked={checked}
- {...props}
- >
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
- <DropdownMenuPrimitive.ItemIndicator>
- <Check className="h-4 w-4" />
- </DropdownMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </DropdownMenuPrimitive.CheckboxItem>
+ <DropdownMenuPrimitive.CheckboxItem
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className
+ )}
+ checked={checked}
+ {...props}
+ >
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <Check className="h-4 w-4" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
- DropdownMenuPrimitive.CheckboxItem.displayName
+ DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
- <DropdownMenuPrimitive.RadioItem
- ref={ref}
- className={cn(
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
- )}
- {...props}
- >
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
- <DropdownMenuPrimitive.ItemIndicator>
- <Circle className="h-2 w-2 fill-current" />
- </DropdownMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </DropdownMenuPrimitive.RadioItem>
+ <DropdownMenuPrimitive.RadioItem
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className
+ )}
+ {...props}
+ >
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <Circle className="h-2 w-2 fill-current" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Label>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
- inset?: boolean
- }
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
+ inset?: boolean
+ }
>(({ className, inset, ...props }, ref) => (
- <DropdownMenuPrimitive.Label
- ref={ref}
- className={cn(
- "px-2 py-1.5 text-sm font-semibold",
- inset && "pl-8",
- className
- )}
- {...props}
- />
+ <DropdownMenuPrimitive.Label
+ ref={ref}
+ className={cn(
+ "px-2 py-1.5 text-sm font-semibold",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ />
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
- <DropdownMenuPrimitive.Separator
- ref={ref}
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
- {...props}
- />
+ <DropdownMenuPrimitive.Separator
+ ref={ref}
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
+ {...props}
+ />
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
- return (
- <span
- className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
- {...props}
- />
- )
+ return (
+ <span
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
+ {...props}
+ />
+ )
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuCheckboxItem,
- DropdownMenuRadioItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuGroup,
- DropdownMenuPortal,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuRadioGroup,
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
}
diff --git a/dashboard/src/features/auth/logout/index.tsx b/dashboard/src/features/auth/logout/index.tsx
index 16be1a5..7eda4a7 100644
--- a/dashboard/src/features/auth/logout/index.tsx
+++ b/dashboard/src/features/auth/logout/index.tsx
@@ -12,12 +12,13 @@ export const Logout: FC<LogoutProps> = () => {
}, [navigate]);
return (
<Button
- className="h-full text-secondary bg-gray-800 dark:hover:bg-secondary-foreground dark:hover:text-secondary dark:text-secondary-foreground"
- size="icon"
- variant="outline"
+ size="sm"
+ variant="ghost"
onClick={handleLogout}
+ className="hstack gap-1 items-center justify-between w-full h-fit p-0"
>
- <LogOut className="h-[1rem] w-[1rem]" />
+ Logout
+ <LogOut className="size-4" />
</Button>
);
};
diff --git a/dashboard/src/features/github-repo/github-repo.stories.tsx b/dashboard/src/features/github-repo/github-repo.stories.tsx
new file mode 100644
index 0000000..9b8ecd5
--- /dev/null
+++ b/dashboard/src/features/github-repo/github-repo.stories.tsx
@@ -0,0 +1,30 @@
+import { GithubRepo } from './index'
+import type { Meta, StoryObj } from "@storybook/react";
+
+const meta: Meta<typeof GithubRepo> = {
+ title: "Features/Github Repo Stats",
+ component: GithubRepo,
+ tags: ["autodocs"]
+};
+
+export default meta;
+
+type Story = StoryObj<typeof GithubRepo>;
+
+/**
+ * Github repo present repo fullname, description, and repo stars.
+ * */
+export const GithubRepoStats: Story = {
+ args: {}
+}
+
+
+/**
+ * Mini variant of Github repo present repo only the github logo and stars.
+ * */
+export const GithubRepoStatsMini: Story = {
+ args: {
+ variant: "mini"
+ }
+}
+
diff --git a/dashboard/src/features/github-repo/index.tsx b/dashboard/src/features/github-repo/index.tsx
new file mode 100644
index 0000000..1942960
--- /dev/null
+++ b/dashboard/src/features/github-repo/index.tsx
@@ -0,0 +1,53 @@
+import {
+ Card,
+ CardTitle,
+ CardContent,
+ CardDescription,
+ Button,
+} from "@marzneshin/components"
+import {
+ useGithubRepoStatsQuery
+} from "./services";
+import { type FC } from "react";
+import {
+ GithubIcon,
+ StarIcon,
+} from "lucide-react";
+import { projectInfo } from "@marzneshin/utils";
+
+interface GithubRepoProps {
+ variant?: "full" | "mini"
+}
+
+export const GithubRepo: FC<GithubRepoProps> = ({ variant = "full" }) => {
+ const { data: stats } = useGithubRepoStatsQuery()
+ return (
+ <Button variant="secondary" className="bg-gray-800 border-0 dark:text-primary text-secondary dark:hover:text-secondary dark:hover:bg-primary hover:bg-secondary hover:text-primary p-2" asChild>
+ <Card>
+ <a href={projectInfo.github} target="_blank">
+ <CardContent className="hstack size-fit p-0 gap-2 items-center">
+ <GithubIcon className="size-fit" />
+ {variant === "full" ? (<div className="vstack items-start">
+ <CardTitle className="font-mono text-xs hstack justify-between w-full">
+ {stats.full_name}
+ <div className="hstack gap-1 font-bold items-center text-xs">
+ <StarIcon className="size-3" />
+ {stats.stargazers_count}
+ </div>
+ </CardTitle>
+ <CardDescription className="text-xs">
+ {stats.description}
+ </CardDescription>
+ </div>
+ ) : (
+ <CardDescription className="hstack gap-1 font-bold items-center text-xs">
+ <StarIcon className="size-3" />
+ {stats.stargazers_count}
+ </CardDescription>
+ )}
+ </CardContent>
+ </a>
+ </Card>
+ </Button>
+ )
+}
diff --git a/dashboard/src/features/github-repo/services/github-repo.query.ts b/dashboard/src/features/github-repo/services/github-repo.query.ts
new file mode 100644
index 0000000..5ffa20e
--- /dev/null
+++ b/dashboard/src/features/github-repo/services/github-repo.query.ts
@@ -0,0 +1,40 @@
+import { useQuery } from "@tanstack/react-query";
+import { projectInfo } from "@marzneshin/utils";
+import { ofetch } from "ofetch";
+
+/**
+ * GithubRepoStatsResponse
+ *
+ * Github API repo statistics response
+ *
+ * @param full_name Full name of the repo (author/repo-name)
+ * @param description Github repo description
+ * @param stargazers_count Github repo star count
+ */
+export interface GithubRepoStatsResponse {
+ full_name: string;
+ description: string;
+ stargazers_count: number;
+}
+
+export const GithubRepoStatsResponseDefault: GithubRepoStatsResponse = {
+ full_name: projectInfo.repo,
+ description: "",
+ stargazers_count: 0,
+}
+
+export async function fetchGithubRepoStats(): Promise<GithubRepoStatsResponse> {
+ return await ofetch(`https://api.github.com/repos/${projectInfo.repo}`, { parseResponse: JSON.parse }).then((result) => {
+ return result;
+ });
+}
+
+
+export const useGithubRepoStatsQuery = () => {
+ return useQuery({
+ queryKey: ["github", projectInfo.repo, "stats"],
+ queryFn: fetchGithubRepoStats,
+ refetchInterval: 1000 * 60 * 1, // 5min refresh
+ initialData: GithubRepoStatsResponseDefault
+ })
+}
diff --git a/dashboard/src/features/github-repo/services/index.ts b/dashboard/src/features/github-repo/services/index.ts
new file mode 100644
index 0000000..f1f558e
--- /dev/null
+++ b/dashboard/src/features/github-repo/services/index.ts
@@ -0,0 +1 @@
+export * from "./github-repo.query"
diff --git a/dashboard/src/features/language-switch/index.tsx b/dashboard/src/features/language-switch/index.tsx
index 5862352..51d7271 100644
--- a/dashboard/src/features/language-switch/index.tsx
+++ b/dashboard/src/features/language-switch/index.tsx
@@ -1,53 +1,52 @@
import {
- Button,
- DropdownMenu,
- DropdownMenuContent,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
DropdownMenuItem,
- DropdownMenuTrigger,
+ DropdownMenuSubTrigger,
+ DropdownMenuPortal
} from "@marzneshin/components";
import { Languages } from "lucide-react";
import { FC } from "react";
import { useTranslation } from "react-i18next";
+import { cn } from "@marzneshin/utils";
-export const LanguageSwitch: FC = () => {
+const LanguageItem = ({ language, title }: { language: string, title: string }) => {
const { i18n } = useTranslation();
const changeLanguage = (lang: string) => {
i18n.changeLanguage(lang);
};
+ return (
+ <DropdownMenuItem
+ className={cn({ "bg-primary text-secondary": i18n.language === language })}
+ onClick={() => changeLanguage(language)}
+ >
+ {title}
+ </DropdownMenuItem>
+ );
+}
+
+export const LanguageSwitchMenu: FC = () => {
+ const { t } = useTranslation();
return (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="outline"
- className="bg-gray-800 text-secondary dark:hover:bg-secondary-foreground dark:hover:text-secondary dark:text-secondary-foreground"
- size="icon"
- >
- <Languages className="h-[1rem] w-[1rem]" />
- <span className="sr-only">Toggle Language</span>
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem onClick={() => changeLanguage("en")}>
- English
- </DropdownMenuItem>
- <DropdownMenuItem onClick={() => changeLanguage("kmr")}>
- Kurdî Kurmancî
- </DropdownMenuItem>
- <DropdownMenuItem onClick={() => changeLanguage("kur")}>
- Kurdî Soranî
- </DropdownMenuItem>
- <DropdownMenuItem onClick={() => changeLanguage("ckb")}>
- کوردی سورانی
- </DropdownMenuItem>
- <DropdownMenuItem onClick={() => changeLanguage("fa")}>
- فارسی
- </DropdownMenuItem>
- <DropdownMenuItem onClick={() => changeLanguage("ru")}>
- Русский
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
+ <DropdownMenuSub>
+ <DropdownMenuSubTrigger arrowDir="left">
+ <div className="hstack items-center justify-end">
+ <span className="mx-1">{t("language")}</span>
+ <Languages className="size-[1rem]" />
+ </div>
+ </DropdownMenuSubTrigger>
+ <DropdownMenuPortal>
+ <DropdownMenuSubContent>
+ <LanguageItem language="en" title="English" />
+ <LanguageItem language="kmr" title="Kurdî Kurmancî" />
+ <LanguageItem language="kur" title="Kurdî Soranî" />
+ <LanguageItem language="ckb" title="کوردی سورانی" />
+ <LanguageItem language="fa" title="فارسی" />
+ <LanguageItem language="ru" title="Русский" />
+ </DropdownMenuSubContent>
+ </DropdownMenuPortal>
+ </DropdownMenuSub>
);
};
diff --git a/dashboard/src/features/theme-switch/theme-provider.tsx b/dashboard/src/features/theme-switch/theme-provider.tsx
index c19e733..a58239a 100644
--- a/dashboard/src/features/theme-switch/theme-provider.tsx
+++ b/dashboard/src/features/theme-switch/theme-provider.tsx
@@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useState } from "react"
-type Theme = "dark" | "light" | "system"
+export type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
diff --git a/dashboard/src/features/theme-switch/theme-toggle/index.tsx b/dashboard/src/features/theme-switch/theme-toggle/index.tsx
index 00b4133..6c30410 100644
--- a/dashboard/src/features/theme-switch/theme-toggle/index.tsx
+++ b/dashboard/src/features/theme-switch/theme-toggle/index.tsx
@@ -1,42 +1,47 @@
import { Moon, Sun } from "lucide-react";
-
import {
- Button,
- DropdownMenu,
- DropdownMenuContent,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
DropdownMenuItem,
- DropdownMenuTrigger,
+ DropdownMenuPortal,
} from "@marzneshin/components";
-import { useTheme } from "../theme-provider";
+import { useTheme, Theme } from "../theme-provider";
import { useTranslation } from "react-i18next";
+import { cn } from "@marzneshin/utils";
+
+const ThemeItem = ({ schema }: { schema: Theme }) => {
+ const { theme, setTheme } = useTheme();
+ const { t } = useTranslation();
+ return (
+ <DropdownMenuItem
+ className={cn({ "bg-primary text-secondary": theme === schema })}
+ onMouseDown={() => setTheme(schema)}>
+ {t(schema)}
+ </DropdownMenuItem>
+ );
+}
export function ThemeToggle() {
- const { setTheme } = useTheme();
const { t } = useTranslation();
+
return (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="outline"
- className="bg-gray-800 text-secondary dark:hover:bg-secondary-foreground dark:hover:text-secondary dark:text-secondary-foreground"
- size="icon"
- >
- <Sun className="transition-all scale-100 rotate-0 dark:scale-0 dark:-rotate-90 h-[1rem] w-[1rem]" />
- <Moon className="absolute transition-all scale-0 rotate-90 dark:scale-100 dark:rotate-0 h-[1rem] w-[1rem]" />
- <span className="sr-only">Toggle theme</span>
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem onClick={() => setTheme("light")}>
- {t("light")}
- </DropdownMenuItem>
- <DropdownMenuItem onClick={() => setTheme("dark")}>
- {t("dark")}
- </DropdownMenuItem>
- <DropdownMenuItem onClick={() => setTheme("system")}>
- {t("system")}
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
+ <DropdownMenuSub>
+ <DropdownMenuSubTrigger arrowDir="left" className="w-full flex">
+ <div className="flex items-center ml-auto">
+ <Sun className="w-4 h-4 m-0 transition-all transform scale-100 rotate-0 dark:scale-0 dark:-rotate-90" />
+ <Moon className="w-4 h-4 m-0 transition-all transform scale-0 rotate-90 dark:scale-100 dark:rotate-0" />
+ </div>
+ <span>{t('theme')}</span>
+ </DropdownMenuSubTrigger>
+ <DropdownMenuPortal>
+ <DropdownMenuSubContent className="space-y-1">
+ <ThemeItem schema="light" />
+ <ThemeItem schema="dark" />
+ <ThemeItem schema="system" />
+ </DropdownMenuSubContent>
+ </DropdownMenuPortal>
+ </DropdownMenuSub>
);
+
}
diff --git a/dashboard/src/globals.css b/dashboard/src/globals.css
index 806c598..72b8b04 100644
--- a/dashboard/src/globals.css
+++ b/dashboard/src/globals.css
@@ -88,6 +88,14 @@
font-family: 'Ubuntu';
}
+ .vstack {
+ @apply flex flex-col;
+ }
+
+ .hstack {
+ @apply flex flex-row;
+ }
+
* {
@apply border-border font-body;
}
diff --git a/dashboard/src/utils/index.ts b/dashboard/src/utils/index.ts
index ad6bb6e..d8e6241 100644
--- a/dashboard/src/utils/index.ts
+++ b/dashboard/src/utils/index.ts
@@ -4,3 +4,4 @@ export * from './cn'
export * from './query-client'
export * from './subscription-link'
export * from './is-online'
+export * from './project-info'
diff --git a/dashboard/src/utils/project-info.ts b/dashboard/src/utils/project-info.ts
new file mode 100644
index 0000000..d8ed728
--- /dev/null
+++ b/dashboard/src/utils/project-info.ts
@@ -0,0 +1,8 @@
+
+export const projectInfo = {
+ repo: "khodedawsh/marzneshin",
+ github: "https://github.com/khodedawsh/marzneshin",
+ donationLink: "https://github.com/khodedawsh/marzneshin#donation",
+ website: "marzneshin.com",
+ authors: ["Mardin", "Dawsh"]
+}