Compare commits

...

28 Commits

Author SHA1 Message Date
22225ed042 feat(playground): add Python support in code editor and multi-model editor using path as an identifier 2024-12-17 14:19:27 +08:00
40f273f5a2 feat(playground): add ligatures checkbox component and update translations in PlaygroundPage 2024-12-15 21:54:39 +08:00
f590d77578 feat(playground): add theme selector component and enhance code editor with additional themes and languages 2024-12-15 21:15:19 +08:00
668145a9c7 feat(playground): add code editor component and integrate file retrieval functionality 2024-12-15 12:33:39 +08:00
ac7e34dfc5 feat(playground): enhance file tree structure by adding path property to FileTree 2024-12-15 00:02:31 +08:00
2cdd9adf3e refactor: import actions from specific module
- Changed the import statement for `retrieveTreeStructure` from a default import to a named import from the `@/actions` module.
- This change improves code readability and adheres to best practices for module imports.
2024-12-14 23:23:50 +08:00
20d790c5e0 feat(playground): add session management and folder open icon in PlaygroundSidebar component 2024-12-14 18:49:35 +08:00
08831481b8 feat(playground): integrate Gitea API for file tree retrieval and update PlaygroundSidebar component 2024-12-14 17:52:15 +08:00
a872a33052 feat(playground): refactor PlaygroundSidebar component by removing unused changes section and reorganizing imports 2024-12-13 17:57:39 +08:00
1aba2bdc0f feat(playground): restructure sidebar components and implement Playground layout 2024-12-13 17:49:34 +08:00
9b2177fea3 feat(playground): add PlaygroundPage component with tabbed interface and update sidebar navigation 2024-12-13 16:02:25 +08:00
b6a4c3182e feat(i18n): update AppSidebar component for internationalization support
chore(site): add siteConfig
2024-12-12 21:32:06 +08:00
ae662bff78 feat(i18n): update NavProjects component for internationalization support 2024-12-12 20:59:15 +08:00
8d2df7dc4a feat(i18n): restructure translation files and update NavUser component for internationalization support 2024-12-12 20:51:10 +08:00
cd842b3bdd feat(i18n): refactor authentication to use user locale from cookies and implement language switching 2024-12-12 20:28:31 +08:00
e5b8848b8f feat(i18n): add LanguageSwitcher and integrate with dashboard header 2024-12-12 13:38:02 +08:00
301490c30b feat(i18n): add internationalization support with en-US and zh-CN translations 2024-12-12 10:31:10 +08:00
52a7ee5d0b chore(auth): update NextAuth configuration to include JWT language
chore(eslint): add warning rule for unused variables in ESLint configuration
2024-12-11 20:28:44 +08:00
b69cc453e8 feat(auth): extend NextAuth user and session to include language support
refactor(gitea): make username optional in GiteaProfile interface

style(eslint): add warning rule for empty object type in ESLint configuration
2024-12-11 19:24:37 +08:00
5ffbecefc2 docs: add source attribution for use-meta-color implementation 2024-12-09 20:40:21 +08:00
93798b5a7e feat(theme): add meta color management and integrate Jotai for theme switching 2024-12-09 20:38:08 +08:00
ed547464c1 docs: add source attribution for ModeSwitcher and ThemeProvider implementations 2024-12-09 16:49:00 +08:00
8808b4fb9b feat(theme): add theme switching functionality with ModeSwitcher and ThemeProvider 2024-12-09 16:48:38 +08:00
398c56b38b docs: add source reference and author attribution to ConfirmationDialog and useConfirmationStore 2024-12-09 13:39:27 +08:00
bb2093d14f feat(dialog): add confirmation dialog and state management for user actions 2024-12-09 13:22:10 +08:00
5f427a5032 feat(dashboard): integrate SessionProvider and update NavUser for session data
- Integrated `SessionProvider` from `next-auth/react` to manage user sessions.
- Updated `AppSidebar` to use `SessionProvider` and reflect user data.
- Modified `NavUser` to display user info based on session, with default fallback.
2024-12-09 13:15:59 +08:00
474c71bf96 fix(style): use local image for Gitea OAuth provider logo
- Replaced the default Gitea logo URL with a local image path to ensure the logo is displayed correctly.
- The default URL `https://authjs.dev/img/providers/gitea.svg` is not supported, so a local image is used instead.
2024-12-09 00:18:19 +08:00
c8a9b25e27 feat(dashboard): add shadcn sidebar-08 2024-12-07 22:06:25 +08:00
55 changed files with 3466 additions and 16 deletions

View File

@ -1,3 +1,7 @@
{ {
"extends": ["next/core-web-vitals", "next/typescript"] "extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-empty-object-type": "warn"
}
} }

3
.gitignore vendored
View File

@ -44,3 +44,6 @@ cache
# bun # bun
bun.lockb bun.lockb
# vscode
.vscode

37
messages/en-US.json Normal file
View File

@ -0,0 +1,37 @@
{
"Page": {
"DashboardPage": {
"title": "Welcome to Judge4c!"
},
"PlaygroundPage": {
"Components": {
"playgroundSidebar": {
"title": "Files"
},
"ligaturesCheckbox": {
"label": "Font Ligatures"
}
}
}
},
"Components": {
"NavUser": {
"profile": "Profile",
"notifications": "Notifications",
"settings": "Settings",
"logout": "Log out",
"login": "Log in"
},
"NavProjects": {
"projects": "Projects",
"viewProject": "View Project",
"shareProject": "Share Project",
"deleteProject": "Delete Project",
"more": "More"
},
"NavSecondary": {
"support": "Support",
"feedback": "Feedback"
}
}
}

37
messages/zh-CN.json Normal file
View File

@ -0,0 +1,37 @@
{
"Page": {
"DashboardPage": {
"title": "欢迎使用Judge4c"
},
"PlaygroundPage": {
"Components": {
"playgroundSidebar": {
"title": "文件"
},
"ligaturesCheckbox": {
"label": "连字"
}
}
}
},
"Components": {
"NavUser": {
"profile": "个人资料",
"notifications": "通知",
"settings": "设置",
"logout": "登出",
"login": "登录"
},
"NavProjects": {
"projects": "项目",
"viewProject": "查看项目",
"shareProject": "分享项目",
"deleteProject": "删除项目",
"more": "更多"
},
"NavSecondary": {
"support": "支持",
"feedback": "反馈"
}
}
}

View File

@ -1,7 +1,8 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const nextConfig: NextConfig = { const withNextIntl = createNextIntlPlugin();
/* config options here */
};
export default nextConfig; const nextConfig: NextConfig = {};
export default withNextIntl(nextConfig);

View File

@ -9,25 +9,47 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/fira-code": "^5.1.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cross-fetch": "^4.0.0",
"devicons-react": "^1.4.0",
"gitea-js": "^1.22.0", "gitea-js": "^1.22.0",
"jotai": "^2.10.3",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"next": "15.0.4", "next": "15.0.4",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.25",
"next-intl": "^3.26.1",
"next-themes": "^0.4.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.2"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@shikijs/monaco": "^1.24.2",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "15.0.4" "eslint-config-next": "15.0.4",
"postcss": "^8",
"shiki": "^1.24.2",
"tailwindcss": "^3.4.1",
"typescript": "^5"
} }
} }

18
public/gitea.svg Normal file
View File

@ -0,0 +1,18 @@
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" style="enable-background:new 0 0 640 640;" xml:space="preserve" viewBox="5.67 143.05 628.65 387.55">
<g>
<path id="teabag" style="fill:#FFFFFF"
d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z">
</path>
<g>
<g>
<path style="fill:#609926"
d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z">
</path>
<path style="fill:#609926"
d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

43
src/actions/index.ts Normal file
View File

@ -0,0 +1,43 @@
"use server";
import api from "@/lib/gitea";
import { GitEntry, APIError } from "gitea-js";
export async function retrieveTreeStructure(
owner: string,
repo: string,
sha: string
): Promise<GitEntry[]> {
try {
const response = await api.repos.getTree(owner, repo, sha, {
recursive: true,
});
return response.data.tree || [];
} catch (error) {
if ((error as APIError).message) {
console.error("Gitea API [getTree] Error", error);
} else {
console.error("Unexpected Error", error);
}
throw new Error("Failed to retrieve tree structure");
}
}
export async function retrieveFileContent(
owner: string,
repo: string,
path: string
): Promise<{ encoding: string; content: string }> {
try {
const response = await api.repos.repoGetContents(owner, repo, path);
const { encoding, content } = response.data;
return { encoding: encoding || "", content: content || "" };
} catch (error) {
if ((error as APIError).message) {
console.error("Gitea API [repoGetContents] Error", error);
} else {
console.error("Unexpected Error", error);
}
throw new Error("Failed to retrieve file contents");
}
}

View File

@ -0,0 +1,190 @@
"use client";
import {
BookOpen,
Bot,
Command,
Frame,
LifeBuoy,
Map,
PieChart,
Send,
Settings2,
SquareTerminal,
} from "lucide-react";
import * as React from "react";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { siteConfig } from "@/config/site";
import { useTranslations } from "next-intl";
import { NavMain } from "@/app/dashboard/components/nav-main";
import { NavUser } from "@/app/dashboard/components/nav-user";
import { NavProjects } from "@/app/dashboard/components/nav-projects";
import { NavSecondary } from "@/app/dashboard/components/nav-secondary";
export function DashboardSidebar({
...props
}: React.ComponentProps<typeof Sidebar>) {
const t = useTranslations();
const data = {
user: {
name: "gitea",
email: "about@gitea.com",
avatar: "/gitea.svg",
},
navMain: [
{
title: "Playground",
url: "/playground",
icon: SquareTerminal,
isActive: true,
items: [
{
title: "History",
url: "#",
},
{
title: "Starred",
url: "#",
},
{
title: "Settings",
url: "#",
},
],
},
{
title: "Models",
url: "#",
icon: Bot,
items: [
{
title: "Genesis",
url: "#",
},
{
title: "Explorer",
url: "#",
},
{
title: "Quantum",
url: "#",
},
],
},
{
title: "Documentation",
url: "#",
icon: BookOpen,
items: [
{
title: "Introduction",
url: "#",
},
{
title: "Get Started",
url: "#",
},
{
title: "Tutorials",
url: "#",
},
{
title: "Changelog",
url: "#",
},
],
},
{
title: "Settings",
url: "#",
icon: Settings2,
items: [
{
title: "General",
url: "#",
},
{
title: "Team",
url: "#",
},
{
title: "Billing",
url: "#",
},
{
title: "Limits",
url: "#",
},
],
},
],
navSecondary: [
{
title: t("Components.NavSecondary.support"),
url: siteConfig.url,
icon: LifeBuoy,
},
{
title: t("Components.NavSecondary.feedback"),
url: siteConfig.url,
icon: Send,
},
],
projects: [
{
name: "Design Engineering",
url: "#",
icon: Frame,
},
{
name: "Sales & Marketing",
url: "#",
icon: PieChart,
},
{
name: "Travel",
url: "#",
icon: Map,
},
],
};
return (
<Sidebar variant="inset" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<a href="#">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<Command className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Acme Inc</span>
<span className="truncate text-xs">Enterprise</span>
</div>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavProjects projects={data.projects} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
);
}

View File

@ -0,0 +1,77 @@
"use client";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronRight, type LucideIcon } from "lucide-react";
export function NavMain({
items,
}: {
items: {
title: string;
url: string;
icon: LucideIcon;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
}[];
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible key={item.title} asChild defaultOpen={item.isActive}>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip={item.title}>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
{item.items?.length ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<ChevronRight />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : null}
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
);
}

View File

@ -0,0 +1,90 @@
"use client";
import {
Folder,
MoreHorizontal,
Share,
Trash2,
type LucideIcon,
} from "lucide-react";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTranslations } from "next-intl";
export function NavProjects({
projects,
}: {
projects: {
name: string;
url: string;
icon: LucideIcon;
}[];
}) {
const { isMobile } = useSidebar();
const t = useTranslations("Components.NavProjects");
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>{t("projects")}</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">{t("more")}</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>{t("viewProject")}</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Share className="text-muted-foreground" />
<span>{t("shareProject")}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>{t("deleteProject")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton>
<MoreHorizontal />
<span>{t("more")}</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}

View File

@ -0,0 +1,39 @@
import * as React from "react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { type LucideIcon } from "lucide-react";
export function NavSecondary({
items,
...props
}: {
items: {
title: string;
url: string;
icon: LucideIcon;
}[];
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild size="sm">
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

@ -0,0 +1,159 @@
"use client";
import {
Bell,
LogIn,
LogOut,
Settings,
ChevronsUpDown,
CircleUserRound,
} from "lucide-react";
import { useMemo } from "react";
import {
useSidebar,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import {
DropdownMenu,
DropdownMenuItem,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { useTranslations } from "next-intl";
import useConfirmationStore from "@/store/confirmationStore";
import { signIn, signOut, useSession } from "next-auth/react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
const UserAvatar = ({ src, alt }: { src: string; alt: string }) => (
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={src} alt={alt} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
);
export const NavUser = ({
user,
}: {
user: {
name: string;
email: string;
avatar: string;
};
}) => {
const { isMobile } = useSidebar();
const { data: session } = useSession();
const t = useTranslations("Components.NavUser");
const { openConfirmation } = useConfirmationStore();
const currentUser = useMemo(() => {
if (session?.user) {
return {
name: session.user.name ?? "Unknown",
email: session.user.email ?? "Unknown",
avatar: session.user.image ?? "/gitea.svg",
};
}
return user;
}, [session, user]);
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<UserAvatar src={currentUser.avatar} alt={currentUser.name} />
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{currentUser.name}
</span>
<span className="truncate text-xs">{currentUser.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
{session?.user ? (
<>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<UserAvatar
src={currentUser.avatar}
alt={currentUser.name}
/>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{currentUser.name}
</span>
<span className="truncate text-xs">
{currentUser.email}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<CircleUserRound />
{t("profile")}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Bell />
{t("notifications")}
</DropdownMenuItem>
<DropdownMenuItem>
<Settings />
{t("settings")}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => {
openConfirmation({
title: "Leaving Already?",
description: "Are you sure you want to say goodbye?",
cancelLabel: "Stay",
actionLabel: "Leave",
onCancel: () => {},
onAction: () => {
signOut();
},
});
}}
>
<LogOut />
{t("logout")}
</DropdownMenuItem>
</DropdownMenuGroup>
</>
) : (
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => signIn()}>
<LogIn />
{t("login")}
</DropdownMenuItem>
</DropdownMenuGroup>
)}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
};

View File

@ -0,0 +1,52 @@
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/ui/breadcrumb";
import { SessionProvider } from "next-auth/react";
import { Separator } from "@/components/ui/separator";
import { ModeSwitcher } from "@/components/mode-switcher";
import ConfirmationDialog from "@/dialogs/ConfirmationDialog";
import LanguageSwitcher from "@/components/language-switcher";
import { DashboardSidebar } from "@/app/dashboard/components/dashboard-sidebar";
export default function DashboardLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<SessionProvider>
<SidebarProvider>
<DashboardSidebar />
<SidebarInset>
<header className="flex h-14 shrink-0 items-center gap-2">
<div className="flex flex-1 items-center gap-2 px-3">
<SidebarTrigger />
<ModeSwitcher />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">
Project Management & Task Tracking
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="ml-auto px-3">
<LanguageSwitcher />
</div>
</header>
{children}
</SidebarInset>
<ConfirmationDialog />
</SidebarProvider>
</SessionProvider>
);
}

View File

@ -0,0 +1,17 @@
import { useTranslations } from "next-intl";
export default function DashboardPage() {
const t = useTranslations("Page.DashboardPage");
return (
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50 flex items-center justify-center">
<h1 className="text-2xl font-bold">{t("title")}</h1>
</div>
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
);
}

View File

@ -27,7 +27,15 @@
--chart-3: 197 37% 24%; --chart-3: 197 37% 24%;
--chart-4: 43 74% 66%; --chart-4: 43 74% 66%;
--chart-5: 27 87% 67%; --chart-5: 27 87% 67%;
--radius: 0.5rem --radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
} }
.dark { .dark {
--background: 240 10% 3.9%; --background: 240 10% 3.9%;
@ -53,7 +61,15 @@
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-5: 340 75% 55% --chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
} }
} }
@layer base { @layer base {

View File

@ -2,6 +2,9 @@ import "@/app/globals.css";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages } from "next-intl/server";
import { ThemeProvider } from "@/components/theme-provider";
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
@ -11,17 +14,30 @@ export const metadata: Metadata = {
"A full-stack, open-source online judge platform designed to elevate college programming education", "A full-stack, open-source online judge platform designed to elevate college programming education",
}; };
export default function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const locale = await getLocale();
const messages = await getMessages();
return ( return (
<html lang="en" className="h-full" suppressHydrationWarning> <html lang={locale} className="h-full" suppressHydrationWarning>
<body <body
className={cn("font-sans antialiased flex min-h-full", inter.variable)} className={cn("font-sans antialiased flex min-h-full", inter.variable)}
> >
<main className="w-full">{children}</main> <ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme
>
<NextIntlClientProvider messages={messages}>
<div className="w-full">{children}</div>
</NextIntlClientProvider>
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@ -0,0 +1,90 @@
"use client";
import "@fontsource-variable/fira-code";
import { createHighlighter } from "shiki";
import type { editor } from "monaco-editor";
import { shikiToMonaco } from "@shikijs/monaco";
import { useEffect, useRef, useMemo } from "react";
import { useCodeEditorStore } from "@/store/codeEditorStore";
import MonacoEditor, { type Monaco } from "@monaco-editor/react";
const ADDITIONAL_THEMES = [
"andromeeda",
"aurora-x",
"ayu-dark",
"catppuccin-frappe",
"catppuccin-latte",
"catppuccin-macchiato",
"catppuccin-mocha",
"dark-plus",
"dracula",
"dracula-soft",
"one-dark-pro",
"vitesse-dark",
"vitesse-light",
];
const ADDITIONAL_LANGUAGES = [
"c",
"java",
"python",
] as const satisfies Parameters<typeof createHighlighter>[0]["langs"];
export function CodeEditor() {
const monacoRef = useRef<Monaco | null>(null);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const { lang, path, theme, value, isLigature } = useCodeEditorStore();
useEffect(() => {
if (monacoRef.current && editorRef.current) {
monacoRef.current.editor.setTheme(theme);
}
}, [theme]);
const options = useMemo(
() => ({
minimap: { enabled: false },
fontSize: 14,
fontFamily: "Fira Code Variable, monospace",
tabSize: 4,
showFoldingControls: "always" as const,
fontLigatures: isLigature,
automaticLayout: true,
guides: {
bracketPairs: true,
indentation: true,
},
}),
[isLigature]
);
const handleEditorMount = async (
editor: editor.IStandaloneCodeEditor,
monaco: Monaco
) => {
editorRef.current = editor;
monacoRef.current = monaco;
for (const lang of ADDITIONAL_LANGUAGES) {
monacoRef.current?.languages.register({ id: lang });
}
const highlighter = await createHighlighter({
themes: ADDITIONAL_THEMES,
langs: ADDITIONAL_LANGUAGES,
});
shikiToMonaco(highlighter, monacoRef.current);
};
return (
<MonacoEditor
defaultLanguage={lang}
theme={theme}
path={path}
options={options}
defaultValue={value}
onMount={handleEditorMount}
/>
);
}

View File

@ -0,0 +1,18 @@
"use client";
import { useTranslations } from "next-intl";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { useCodeEditorStore } from "@/store/codeEditorStore";
export default function LigaturesCheckbox() {
const t = useTranslations("Page.PlaygroundPage.Components.ligaturesCheckbox");
const { setIsLigature } = useCodeEditorStore();
return (
<div className="flex items-center gap-2">
<Checkbox onCheckedChange={setIsLigature} />
<Label>{t("label")}</Label>
</div>
);
}

View File

@ -0,0 +1,211 @@
"use client";
import * as React from "react";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarRail,
} from "@/components/ui/sidebar";
import { GitEntry } from "gitea-js";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import * as actions from "@/actions";
import { useTranslations } from "next-intl";
import { useSession } from "next-auth/react";
import { COriginal, JavaOriginal, PythonOriginal } from "devicons-react";
import { useCodeEditorStore } from "@/store/codeEditorStore";
import { ChevronRight, File, Folder, FolderOpen } from "lucide-react";
interface FileTree {
name: string;
path: string;
type: "blob" | "tree";
children?: { [key: string]: FileTree };
}
export function buildFileTree(tree: GitEntry[]): FileTree {
const root: FileTree = { name: "", type: "tree", path: "", children: {} };
tree.forEach((entry: GitEntry) => {
if (!entry.path) return;
const pathParts = entry.path.split("/");
let currentLevel = root;
pathParts.forEach((part: string, index: number) => {
if (!currentLevel.children) {
currentLevel.children = {};
}
const fullPath = pathParts.slice(0, index + 1).join("/");
if (!currentLevel.children[part]) {
currentLevel.children[part] = {
name: part,
path: fullPath,
type: entry.type as "blob" | "tree",
children: {},
};
}
currentLevel = currentLevel.children[part];
});
});
if (root.children) {
const sortedChildren = Object.values(root.children).sort((a, b) => {
if (a.type === "tree" && b.type === "blob") return -1;
if (a.type === "blob" && b.type === "tree") return 1;
return a.name.localeCompare(b.name);
});
root.children = sortedChildren.reduce((acc, child) => {
acc[child.name] = child;
return acc;
}, {} as { [key: string]: FileTree });
}
return root;
}
export function PlaygroundSidebar({
...props
}: React.ComponentProps<typeof Sidebar>) {
const { data: session } = useSession();
const [fileTree, setFileTree] = React.useState<FileTree>({
name: "",
path: "",
type: "tree",
children: {},
});
const username = session?.user?.name ?? "";
const t = useTranslations("Page.PlaygroundPage.Components.playgroundSidebar");
React.useEffect(() => {
async function fetchFileTree() {
if (username) {
const tree: GitEntry[] = await actions.retrieveTreeStructure(
username,
"playground",
"main"
);
const newFileTree = buildFileTree(tree);
setFileTree(newFileTree);
}
}
fetchFileTree();
}, [username]);
return (
<Sidebar {...props}>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>{t("title")}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{fileTree.children &&
Object.values(fileTree.children).map((item, index) => (
<Tree key={index} item={item} />
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarRail />
</Sidebar>
);
}
function Tree({ item }: { item: FileTree }) {
const { data: session } = useSession();
const username = session?.user?.name ?? "";
const { name, path, type, children } = item;
const fileExtension = name.split(".").pop();
let fileIcon = <File />;
if (fileExtension === "c") {
fileIcon = <COriginal />;
} else if (fileExtension === "java") {
fileIcon = <JavaOriginal />;
} else if (fileExtension === "py") {
fileIcon = <PythonOriginal />;
}
const [isOpen, setIsOpen] = React.useState(false);
const { setLang, setPath, setValue } = useCodeEditorStore();
const langMap: { [key: string]: string } = {
c: "c",
java: "java",
py: "python",
};
const handleFileClick = async () => {
if (type === "blob") {
const fileContentsResponse = await actions.retrieveFileContent(
username,
"playground",
path
);
const { encoding, content } = fileContentsResponse;
const decodedContent = Buffer.from(
content,
encoding as BufferEncoding
).toString("utf-8");
const lang = langMap[fileExtension ?? ""] || "plaintext";
setLang(lang);
setPath(path);
setValue(decodedContent);
}
};
if (type === "blob") {
return (
<SidebarMenuButton
className="data-[active=true]:bg-transparent"
onClick={handleFileClick}
>
{fileIcon}
{name}
</SidebarMenuButton>
);
}
return (
<SidebarMenuItem>
<Collapsible
open={isOpen}
onOpenChange={setIsOpen}
className="group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90"
>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<ChevronRight className="transition-transform" />
{isOpen ? <FolderOpen /> : <Folder />}
{name}
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{children &&
Object.values(children).map((subItem, index) => (
<Tree key={index} item={subItem} />
))}
</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>
</SidebarMenuItem>
);
}

View File

@ -0,0 +1,47 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useCodeEditorStore } from "@/store/codeEditorStore";
const themeOptions = [
{ value: "andromeeda", label: "Andromeeda" },
{ value: "aurora-x", label: "Aurora X" },
{ value: "ayu-dark", label: "Ayu Dark" },
{ value: "catppuccin-frappe", label: "Catppuccin Frappé" },
{ value: "catppuccin-latte", label: "Catppuccin Latte" },
{ value: "catppuccin-macchiato", label: "Catppuccin Macchiato" },
{ value: "catppuccin-mocha", label: "Catppuccin Mocha" },
{ value: "dark-plus", label: "Dark Plus" },
{ value: "dracula", label: "Dracula Theme" },
{ value: "dracula-soft", label: "Dracula Theme Soft" },
{ value: "one-dark-pro", label: "One Dark Pro" },
{ value: "vitesse-dark", label: "Vitesse Dark" },
{ value: "vitesse-light", label: "Vitesse Light" },
];
export default function ThemeSelector() {
const { theme, setTheme } = useCodeEditorStore();
return (
<div className="space-y-2">
<Select defaultValue={theme} onValueChange={setTheme}>
<SelectTrigger>
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent>
{themeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}

View File

@ -0,0 +1,39 @@
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { SessionProvider } from "next-auth/react";
import { Separator } from "@/components/ui/separator";
import ThemeSelector from "./components/theme-selector";
import { ModeSwitcher } from "@/components/mode-switcher";
import LanguageSwitcher from "@/components/language-switcher";
import LigaturesCheckbox from "./components/ligatures-checkbox";
import { PlaygroundSidebar } from "@/app/playground/components/playground-sidebar";
export default function PlaygroundLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<SessionProvider>
<SidebarProvider>
<PlaygroundSidebar />
<SidebarInset>
<header className="flex h-14 shrink-0 items-center gap-2">
<div className="flex flex-1 items-center gap-2 px-3">
<SidebarTrigger />
<ModeSwitcher />
<Separator orientation="vertical" className="mr-2 h-4" />
<ThemeSelector />
<LigaturesCheckbox />
</div>
<div className="ml-auto px-3">
<LanguageSwitcher />
</div>
</header>
{children}
</SidebarInset>
</SidebarProvider>
</SessionProvider>
);
}

View File

@ -0,0 +1,9 @@
import { CodeEditor } from "./components/code-editor";
export default function PlaygroundPage() {
return (
<div className="flex flex-1 flex-col">
<CodeEditor />
</div>
);
}

View File

@ -12,4 +12,5 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
}), }),
], ],
secret: process.env.AUTH_SECRET, secret: process.env.AUTH_SECRET,
debug: process.env.NODE_ENV === "development",
}); });

View File

@ -0,0 +1,44 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useLocale } from "next-intl";
import { Locale } from "@/i18n/config";
import { setUserLocale } from "@/services/locale";
const languages = [
{ value: "en-US", label: "English (United States)", flag: "🇺🇸" },
{ value: "zh-CN", label: "简体中文", flag: "🇨🇳" },
];
export default function LanguageSwitcher() {
const locale = useLocale();
function onChange(value: string) {
const locale = value as Locale;
setUserLocale(locale);
}
return (
<div className="space-y-2">
<Select defaultValue={locale} onValueChange={onChange}>
<SelectTrigger className="[&>span]:flex [&>span]:items-center [&>span]:gap-2 [&>span_svg]:shrink-0 [&>span_svg]:text-muted-foreground/80">
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent className="[&_*[role=option]>span>svg]:shrink-0 [&_*[role=option]>span>svg]:text-muted-foreground/80 [&_*[role=option]>span]:end-2 [&_*[role=option]>span]:start-auto [&_*[role=option]>span]:flex [&_*[role=option]>span]:items-center [&_*[role=option]>span]:gap-2 [&_*[role=option]]:pe-8 [&_*[role=option]]:ps-2">
{languages.map((item) => (
<SelectItem key={item.value} value={item.value}>
<span className="text-lg leading-none">{item.flag}</span>{" "}
<span className="truncate">{item.label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}

View File

@ -0,0 +1,37 @@
// This implementation is derived from the solution provided by [shadcn] in shadcn/ui
// https://github.com/shadcn-ui/ui
"use client";
import * as React from "react";
import { useTheme } from "next-themes";
import { Sun, Moon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { META_THEME_COLORS } from "@/config/site";
import { useMetaColor } from "@/hooks/use-meta-color";
export function ModeSwitcher() {
const { setMetaColor } = useMetaColor();
const { setTheme, resolvedTheme } = useTheme();
const toggleTheme = React.useCallback(() => {
setTheme(resolvedTheme === "dark" ? "light" : "dark");
setMetaColor(
resolvedTheme === "dark"
? META_THEME_COLORS.light
: META_THEME_COLORS.dark
);
}, [resolvedTheme, setTheme, setMetaColor]);
return (
<Button
variant="ghost"
className="group/toggle h-8 w-8 px-0"
onClick={toggleTheme}
>
<Sun className="hidden [html.dark_&]:block" />
<Moon className="hidden [html.light_&]:block" />
<span className="sr-only">Toggle theme</span>
</Button>
);
}

View File

@ -0,0 +1,22 @@
// This implementation is derived from the solution provided by [shadcn] in shadcn/ui
// https://github.com/shadcn-ui/ui
"use client";
import * as React from "react";
import { Provider as JotaiProvider } from "jotai";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return (
<JotaiProvider>
<NextThemesProvider {...props}>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
</NextThemesProvider>
</JotaiProvider>
);
}

View File

@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@ -0,0 +1,57 @@
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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@ -0,0 +1,201 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
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 gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
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}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
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>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
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 gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
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>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
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>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
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}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
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}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
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,
}

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover 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",
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)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

140
src/components/ui/sheet.tsx Normal file
View File

@ -0,0 +1,140 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@ -0,0 +1,763 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { PanelLeft } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar:state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContext = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContext | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
)
SidebarProvider.displayName = "SidebarProvider"
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}
>(
(
{
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className
)}
ref={ref}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
ref={ref}
className="group peer hidden md:block text-sidebar-foreground"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)}
/>
<div
className={cn(
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
)
}
)
Sidebar.displayName = "Sidebar"
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
})
SidebarTrigger.displayName = "SidebarTrigger"
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
})
SidebarRail.displayName = "SidebarRail"
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"main">
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
)}
{...props}
/>
)
})
SidebarInset.displayName = "SidebarInset"
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className
)}
{...props}
/>
)
})
SidebarInput.displayName = "SidebarInput"
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarHeader.displayName = "SidebarHeader"
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarFooter.displayName = "SidebarFooter"
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
})
SidebarSeparator.displayName = "SidebarSeparator"
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
})
SidebarContent.displayName = "SidebarContent"
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
})
SidebarGroup.displayName = "SidebarGroup"
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div"
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
})
SidebarGroupLabel.displayName = "SidebarGroupLabel"
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarGroupAction.displayName = "SidebarGroupAction"
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
))
SidebarGroupContent.displayName = "SidebarGroupContent"
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
))
SidebarMenu.displayName = "SidebarMenu"
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
))
SidebarMenuItem.displayName = "SidebarMenuItem"
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
)
SidebarMenuButton.displayName = "SidebarMenuButton"
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className
)}
{...props}
/>
)
})
SidebarMenuAction.displayName = "SidebarMenuAction"
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuBadge.displayName = "SidebarMenuBadge"
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 flex-1 max-w-[--skeleton-width]"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
})
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuSub.displayName = "SidebarMenuSub"
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />)
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View File

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

8
src/config/site.ts Normal file
View File

@ -0,0 +1,8 @@
export const siteConfig = {
url: "https://github.com/NGC2207/judge4c",
};
export const META_THEME_COLORS = {
light: "#ffffff",
dark: "#09090b",
};

View File

@ -0,0 +1,51 @@
// This implementation is derived from the solution provided by [baydisng13] in the discussion thread:
// https://github.com/shadcn-ui/ui/discussions/3875
"use client";
import React from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import useConfirmationStore from "@/store/confirmationStore";
const ConfirmationDialog = () => {
const {
open,
title,
description,
cancelLabel,
actionLabel,
onCancel,
onAction,
closeConfirmation,
} = useConfirmationStore();
return (
<AlertDialog open={open} onOpenChange={closeConfirmation}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>
{cancelLabel}
</AlertDialogCancel>
<AlertDialogAction onClick={onAction}>
{actionLabel}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
export default ConfirmationDialog;

View File

@ -0,0 +1,27 @@
// This implementation is derived from the solution provided by [shadcn] in shadcn/ui
// https://github.com/shadcn-ui/ui
import * as React from "react";
import { useTheme } from "next-themes";
import { META_THEME_COLORS } from "@/config/site";
export function useMetaColor() {
const { resolvedTheme } = useTheme();
const metaColor = React.useMemo(() => {
return resolvedTheme !== "dark"
? META_THEME_COLORS.light
: META_THEME_COLORS.dark;
}, [resolvedTheme]);
const setMetaColor = React.useCallback((color: string) => {
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", color);
}, []);
return {
metaColor,
setMetaColor,
};
}

21
src/hooks/use-mobile.tsx Normal file
View File

@ -0,0 +1,21 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

5
src/i18n/config.ts Normal file
View File

@ -0,0 +1,5 @@
export type Locale = (typeof locales)[number];
export const locales = ["en-US", "zh-CN"] as const;
export const defaultLocale: Locale = "en-US";

11
src/i18n/request.ts Normal file
View File

@ -0,0 +1,11 @@
import { getUserLocale } from "@/services/locale";
import { getRequestConfig } from "next-intl/server";
export default getRequestConfig(async () => {
const locale = await getUserLocale();
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default,
};
});

13
src/lib/gitea.ts Normal file
View File

@ -0,0 +1,13 @@
import fetch from "cross-fetch";
import { giteaApi } from "gitea-js";
if (!process.env.GITEA_URL) {
throw new Error("GITEA_URL environment variable is not defined");
}
const api = giteaApi(process.env.GITEA_URL, {
token: process.env.GITEA_TOKEN,
customFetch: fetch,
});
export default api;

View File

@ -2,7 +2,7 @@ import { Email as GiteaEmail, User } from "gitea-js";
import { OAuthConfig, OAuthUserConfig } from "next-auth/providers"; import { OAuthConfig, OAuthUserConfig } from "next-auth/providers";
export interface GiteaProfile extends User { export interface GiteaProfile extends User {
username: string; username?: string;
} }
export default function Gitea( export default function Gitea(
@ -65,8 +65,13 @@ export default function Gitea(
name: profile.username ?? profile.login, name: profile.username ?? profile.login,
email: profile.email, email: profile.email,
image: profile.avatar_url, image: profile.avatar_url,
language: profile.language,
}; };
}, },
options: config, options: config,
style: {
logo: "/gitea.svg",
brandColor: "#609926",
},
}; };
} }

14
src/services/locale.ts Normal file
View File

@ -0,0 +1,14 @@
"use server";
import { cookies } from "next/headers";
import { defaultLocale, Locale } from "@/i18n/config";
const LOBE_LOCALE_COOKIE = "JUDGE4C_LOCALE";
export async function getUserLocale() {
return (await cookies()).get(LOBE_LOCALE_COOKIE)?.value || defaultLocale;
}
export async function setUserLocale(locale: Locale) {
(await cookies()).set(LOBE_LOCALE_COOKIE, locale);
}

View File

@ -0,0 +1,27 @@
import { create } from "zustand";
interface CodeEditorState {
lang: string;
path: string;
theme: string;
value: string;
isLigature: boolean;
setLang: (lang: string) => void;
setPath: (path: string) => void;
setTheme: (theme: string) => void;
setValue: (value: string) => void;
setIsLigature: (isLigature: boolean) => void;
}
export const useCodeEditorStore = create<CodeEditorState>((set) => ({
lang: "c",
path: "",
theme: "one-dark-pro",
value: "",
isLigature: false,
setLang: (lang) => set({ lang }),
setPath: (path) => set({ path }),
setTheme: (theme) => set({ theme }),
setValue: (value) => set({ value }),
setIsLigature: (isLigature) => set({ isLigature }),
}));

View File

@ -0,0 +1,71 @@
// This implementation is derived from the solution provided by [baydisng13] in the discussion thread:
// https://github.com/shadcn-ui/ui/discussions/3875
import { create } from "zustand";
interface ConfirmationState {
open: boolean;
title: string | null;
description: string | null;
cancelLabel: string | null;
actionLabel: string | null;
onCancel: () => void;
onAction: () => void;
}
interface ConfirmationActions {
openConfirmation: (data: {
title: string;
description: string;
cancelLabel: string;
actionLabel: string;
onCancel: () => void;
onAction: () => void;
}) => void;
closeConfirmation: () => void;
}
const useConfirmationStore = create<ConfirmationState & ConfirmationActions>(
(set) => ({
open: false,
title: null,
description: null,
cancelLabel: null,
actionLabel: null,
onCancel: () => {},
onAction: () => {},
openConfirmation: (data) =>
set(() => ({
open: true,
title: data.title,
description: data.description,
cancelLabel: data.cancelLabel,
actionLabel: data.actionLabel,
onCancel: data.onCancel,
onAction: data.onAction,
})),
closeConfirmation: () => {
// Close the confirmation dialog by setting the 'open' state to false.
set((state) => ({
...state,
open: false,
}));
// After a short delay (100ms), reset the dialog's state to its initial values.
// This delay ensures that the dialog is fully closed before resetting,
// preventing any visual flickering or UI inconsistencies.
setTimeout(() => {
set((state) => ({
...state,
title: null,
description: null,
cancelLabel: null,
actionLabel: null,
onCancel: () => {},
onAction: () => {},
}));
}, 100);
},
})
);
export default useConfirmationStore;

View File

@ -53,6 +53,16 @@ export default {
"4": "hsl(var(--chart-4))", "4": "hsl(var(--chart-4))",
"5": "hsl(var(--chart-5))", "5": "hsl(var(--chart-5))",
}, },
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: "var(--radius)",