Spaces:
Running
Running
new header
Browse files- assets/pro.svg +10 -0
- components/editor/commits.tsx +3 -7
- components/editor/header.tsx +2 -19
- components/editor/project-settings.tsx +120 -0
- components/ui/dropdown-menu.tsx +26 -26
- components/user-menu/index.tsx +69 -54
- lib/auth.ts +3 -3
- next-auth.d.ts +4 -2
assets/pro.svg
ADDED
|
|
components/editor/commits.tsx
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
| 12 |
import { Commit } from "@/lib/type";
|
| 13 |
import { cn, humanizeNumber } from "@/lib/utils";
|
| 14 |
|
|
|
|
|
|
|
| 15 |
export const Commits = function ({ commits }: { commits?: Commit[] }) {
|
| 16 |
const searchParams = useSearchParams();
|
| 17 |
const commitParam = searchParams.get("commit");
|
|
@@ -65,14 +67,8 @@ export const Commits = function ({ commits }: { commits?: Commit[] }) {
|
|
| 65 |
<Popover open={open} onOpenChange={setOpen}>
|
| 66 |
<form>
|
| 67 |
<PopoverTrigger asChild>
|
| 68 |
-
<Button variant={open ? "default" : "
|
| 69 |
<HistoryIcon className="size-3.5" />
|
| 70 |
-
<span>
|
| 71 |
-
History <span className="max-lg:hidden">version</span>
|
| 72 |
-
</span>
|
| 73 |
-
<span className="py-0.5 px-1 text-[10px] flex items-center justify-center rounded font-semibold bg-accent text-primary font-mono">
|
| 74 |
-
{humanizeNumber(commits ? commits.length : 0)}
|
| 75 |
-
</span>
|
| 76 |
</Button>
|
| 77 |
</PopoverTrigger>
|
| 78 |
<PopoverContent
|
|
|
|
| 12 |
import { Commit } from "@/lib/type";
|
| 13 |
import { cn, humanizeNumber } from "@/lib/utils";
|
| 14 |
|
| 15 |
+
// todo: when there is a commit query, highlight the selected commit in the list.
|
| 16 |
+
|
| 17 |
export const Commits = function ({ commits }: { commits?: Commit[] }) {
|
| 18 |
const searchParams = useSearchParams();
|
| 19 |
const commitParam = searchParams.get("commit");
|
|
|
|
| 67 |
<Popover open={open} onOpenChange={setOpen}>
|
| 68 |
<form>
|
| 69 |
<PopoverTrigger asChild>
|
| 70 |
+
<Button variant={open ? "default" : "ghost"} size="icon-xs">
|
| 71 |
<HistoryIcon className="size-3.5" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
</Button>
|
| 73 |
</PopoverTrigger>
|
| 74 |
<PopoverContent
|
components/editor/header.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import { UserMenu } from "@/components/user-menu";
|
|
| 22 |
import { useProject } from "@/components/projects/useProject";
|
| 23 |
import { cn } from "@/lib/utils";
|
| 24 |
import { Commits } from "./commits";
|
|
|
|
| 25 |
|
| 26 |
export function AppEditorHeader({
|
| 27 |
currentActivity,
|
|
@@ -65,25 +66,7 @@ export function AppEditorHeader({
|
|
| 65 |
mobileTab !== "left-sidebar" ? "max-lg:hidden" : "max-lg:w-full!"
|
| 66 |
)}
|
| 67 |
>
|
| 68 |
-
<
|
| 69 |
-
<Image
|
| 70 |
-
src="/logo.svg"
|
| 71 |
-
alt="DeepSite"
|
| 72 |
-
width={100}
|
| 73 |
-
height={100}
|
| 74 |
-
className="size-8"
|
| 75 |
-
/>
|
| 76 |
-
<div className="flex flex-col -space-y-1 items-start">
|
| 77 |
-
<p className="text-sm font-bold text-primary">
|
| 78 |
-
{project?.cardData?.title ?? "New DeepSite website"}{" "}
|
| 79 |
-
{project?.cardData?.emoji}
|
| 80 |
-
</p>
|
| 81 |
-
<p className="text-xs text-muted-foreground">
|
| 82 |
-
Live preview of your app
|
| 83 |
-
</p>
|
| 84 |
-
</div>
|
| 85 |
-
{/* <ChevronDown /> */}
|
| 86 |
-
</Button>
|
| 87 |
<div className="flex items-center justify-end gap-2 max-lg:hidden">
|
| 88 |
{(project?.commits?.length ?? 0) > 0 && (
|
| 89 |
<Commits commits={project?.commits} />
|
|
|
|
| 22 |
import { useProject } from "@/components/projects/useProject";
|
| 23 |
import { cn } from "@/lib/utils";
|
| 24 |
import { Commits } from "./commits";
|
| 25 |
+
import { ProjectSettings } from "./project-settings";
|
| 26 |
|
| 27 |
export function AppEditorHeader({
|
| 28 |
currentActivity,
|
|
|
|
| 66 |
mobileTab !== "left-sidebar" ? "max-lg:hidden" : "max-lg:w-full!"
|
| 67 |
)}
|
| 68 |
>
|
| 69 |
+
{project && <ProjectSettings project={project} />}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
<div className="flex items-center justify-end gap-2 max-lg:hidden">
|
| 71 |
{(project?.commits?.length ?? 0) > 0 && (
|
| 72 |
<Commits commits={project?.commits} />
|
components/editor/project-settings.tsx
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Image from "next/image";
|
| 2 |
+
import Link from "next/link";
|
| 3 |
+
import { useTheme } from "next-themes";
|
| 4 |
+
import { Sun, Moon, Settings, Contrast, Check } from "lucide-react";
|
| 5 |
+
import { useSession } from "next-auth/react";
|
| 6 |
+
import { ChevronDown, ChevronLeft, Edit } from "lucide-react";
|
| 7 |
+
|
| 8 |
+
import {
|
| 9 |
+
DropdownMenu,
|
| 10 |
+
DropdownMenuContent,
|
| 11 |
+
DropdownMenuItem,
|
| 12 |
+
DropdownMenuLabel,
|
| 13 |
+
DropdownMenuPortal,
|
| 14 |
+
DropdownMenuSeparator,
|
| 15 |
+
DropdownMenuSub,
|
| 16 |
+
DropdownMenuSubContent,
|
| 17 |
+
DropdownMenuSubTrigger,
|
| 18 |
+
DropdownMenuTrigger,
|
| 19 |
+
} from "@/components/ui/dropdown-menu";
|
| 20 |
+
import { Button } from "@/components/ui/button";
|
| 21 |
+
import { ProjectWithCommits } from "@/actions/projects";
|
| 22 |
+
import { cn } from "@/lib/utils";
|
| 23 |
+
import ProIcon from "@/assets/pro.svg";
|
| 24 |
+
import { useParams } from "next/navigation";
|
| 25 |
+
|
| 26 |
+
export const ProjectSettings = ({
|
| 27 |
+
project,
|
| 28 |
+
}: {
|
| 29 |
+
project: ProjectWithCommits;
|
| 30 |
+
}) => {
|
| 31 |
+
const { repoId, owner } = useParams();
|
| 32 |
+
const { theme, setTheme } = useTheme();
|
| 33 |
+
const { data: session } = useSession();
|
| 34 |
+
return (
|
| 35 |
+
// <Popover open={open} onOpenChange={setOpen}>
|
| 36 |
+
// <PopoverTrigger asChild>
|
| 37 |
+
<DropdownMenu>
|
| 38 |
+
<DropdownMenuTrigger asChild>
|
| 39 |
+
<Button variant="ghost" className="pl-2.5! pr-3! py-1.5! h-auto!">
|
| 40 |
+
<Image
|
| 41 |
+
src="/logo.svg"
|
| 42 |
+
alt="DeepSite"
|
| 43 |
+
width={100}
|
| 44 |
+
height={100}
|
| 45 |
+
className="size-8"
|
| 46 |
+
/>
|
| 47 |
+
<div className="flex flex-col -space-y-1 items-start">
|
| 48 |
+
<p className="text-sm font-bold text-primary">
|
| 49 |
+
{project?.cardData?.title ?? "New DeepSite website"}{" "}
|
| 50 |
+
{project?.cardData?.emoji}
|
| 51 |
+
</p>
|
| 52 |
+
<p className="text-xs text-muted-foreground">
|
| 53 |
+
Live preview of your app
|
| 54 |
+
</p>
|
| 55 |
+
</div>
|
| 56 |
+
<ChevronDown />
|
| 57 |
+
</Button>
|
| 58 |
+
</DropdownMenuTrigger>
|
| 59 |
+
<DropdownMenuContent className="w-64" align="start">
|
| 60 |
+
<DropdownMenuItem>
|
| 61 |
+
<Link href="/" className="flex items-center gap-1.5">
|
| 62 |
+
<ChevronLeft className="size-3.5" />
|
| 63 |
+
Go to Projects
|
| 64 |
+
</Link>
|
| 65 |
+
</DropdownMenuItem>
|
| 66 |
+
<DropdownMenuSeparator />
|
| 67 |
+
{!session?.user?.isPro && (
|
| 68 |
+
<>
|
| 69 |
+
<DropdownMenuItem>
|
| 70 |
+
<Link
|
| 71 |
+
href="https://huggingface.co/pro"
|
| 72 |
+
className="flex items-center gap-1.5 bg-linear-to-r from-pink-500 via-green-500 to-amber-500 text-transparent bg-clip-text font-semibold"
|
| 73 |
+
target="_blank"
|
| 74 |
+
>
|
| 75 |
+
<Image alt="Pro" src={ProIcon} className="size-3.5" />
|
| 76 |
+
Subscribe to Pro
|
| 77 |
+
</Link>
|
| 78 |
+
</DropdownMenuItem>
|
| 79 |
+
<DropdownMenuSeparator />
|
| 80 |
+
</>
|
| 81 |
+
)}
|
| 82 |
+
<DropdownMenuItem>
|
| 83 |
+
<Edit className="size-3.5" />
|
| 84 |
+
Rename the project
|
| 85 |
+
</DropdownMenuItem>
|
| 86 |
+
<DropdownMenuItem>
|
| 87 |
+
<Link
|
| 88 |
+
href={`https://huggingface.co/${owner}/${repoId}/settings`}
|
| 89 |
+
className="flex items-center gap-1.5"
|
| 90 |
+
>
|
| 91 |
+
<Settings className="size-3.5" />
|
| 92 |
+
Project settings
|
| 93 |
+
</Link>
|
| 94 |
+
</DropdownMenuItem>
|
| 95 |
+
<DropdownMenuSub>
|
| 96 |
+
<DropdownMenuSubTrigger className="flex items-center justify-start gap-1.5">
|
| 97 |
+
<Contrast className="size-3.5" />
|
| 98 |
+
Appearance
|
| 99 |
+
</DropdownMenuSubTrigger>
|
| 100 |
+
<DropdownMenuPortal>
|
| 101 |
+
<DropdownMenuSubContent>
|
| 102 |
+
<DropdownMenuItem onClick={() => setTheme("light")}>
|
| 103 |
+
Light
|
| 104 |
+
{theme === "light" && <Check />}
|
| 105 |
+
</DropdownMenuItem>
|
| 106 |
+
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
| 107 |
+
Dark
|
| 108 |
+
{theme === "dark" && <Check />}
|
| 109 |
+
</DropdownMenuItem>
|
| 110 |
+
<DropdownMenuItem onClick={() => setTheme("system")}>
|
| 111 |
+
System
|
| 112 |
+
{theme === "system" && <Check />}
|
| 113 |
+
</DropdownMenuItem>
|
| 114 |
+
</DropdownMenuSubContent>
|
| 115 |
+
</DropdownMenuPortal>
|
| 116 |
+
</DropdownMenuSub>
|
| 117 |
+
</DropdownMenuContent>
|
| 118 |
+
</DropdownMenu>
|
| 119 |
+
);
|
| 120 |
+
};
|
components/ui/dropdown-menu.tsx
CHANGED
|
@@ -1,15 +1,15 @@
|
|
| 1 |
-
"use client"
|
| 2 |
|
| 3 |
-
import * as React from "react"
|
| 4 |
-
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
| 5 |
-
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
| 6 |
|
| 7 |
-
import { cn } from "@/lib/utils"
|
| 8 |
|
| 9 |
function DropdownMenu({
|
| 10 |
...props
|
| 11 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
| 12 |
-
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props}
|
| 13 |
}
|
| 14 |
|
| 15 |
function DropdownMenuPortal({
|
|
@@ -17,7 +17,7 @@ function DropdownMenuPortal({
|
|
| 17 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
| 18 |
return (
|
| 19 |
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
| 20 |
-
)
|
| 21 |
}
|
| 22 |
|
| 23 |
function DropdownMenuTrigger({
|
|
@@ -28,7 +28,7 @@ function DropdownMenuTrigger({
|
|
| 28 |
data-slot="dropdown-menu-trigger"
|
| 29 |
{...props}
|
| 30 |
/>
|
| 31 |
-
)
|
| 32 |
}
|
| 33 |
|
| 34 |
function DropdownMenuContent({
|
|
@@ -48,7 +48,7 @@ function DropdownMenuContent({
|
|
| 48 |
{...props}
|
| 49 |
/>
|
| 50 |
</DropdownMenuPrimitive.Portal>
|
| 51 |
-
)
|
| 52 |
}
|
| 53 |
|
| 54 |
function DropdownMenuGroup({
|
|
@@ -56,7 +56,7 @@ function DropdownMenuGroup({
|
|
| 56 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
| 57 |
return (
|
| 58 |
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
| 59 |
-
)
|
| 60 |
}
|
| 61 |
|
| 62 |
function DropdownMenuItem({
|
|
@@ -65,8 +65,8 @@ function DropdownMenuItem({
|
|
| 65 |
variant = "default",
|
| 66 |
...props
|
| 67 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
| 68 |
-
inset?: boolean
|
| 69 |
-
variant?: "default" | "destructive"
|
| 70 |
}) {
|
| 71 |
return (
|
| 72 |
<DropdownMenuPrimitive.Item
|
|
@@ -74,12 +74,12 @@ function DropdownMenuItem({
|
|
| 74 |
data-inset={inset}
|
| 75 |
data-variant={variant}
|
| 76 |
className={cn(
|
| 77 |
-
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex
|
| 78 |
className
|
| 79 |
)}
|
| 80 |
{...props}
|
| 81 |
/>
|
| 82 |
-
)
|
| 83 |
}
|
| 84 |
|
| 85 |
function DropdownMenuCheckboxItem({
|
|
@@ -105,7 +105,7 @@ function DropdownMenuCheckboxItem({
|
|
| 105 |
</span>
|
| 106 |
{children}
|
| 107 |
</DropdownMenuPrimitive.CheckboxItem>
|
| 108 |
-
)
|
| 109 |
}
|
| 110 |
|
| 111 |
function DropdownMenuRadioGroup({
|
|
@@ -116,7 +116,7 @@ function DropdownMenuRadioGroup({
|
|
| 116 |
data-slot="dropdown-menu-radio-group"
|
| 117 |
{...props}
|
| 118 |
/>
|
| 119 |
-
)
|
| 120 |
}
|
| 121 |
|
| 122 |
function DropdownMenuRadioItem({
|
|
@@ -140,7 +140,7 @@ function DropdownMenuRadioItem({
|
|
| 140 |
</span>
|
| 141 |
{children}
|
| 142 |
</DropdownMenuPrimitive.RadioItem>
|
| 143 |
-
)
|
| 144 |
}
|
| 145 |
|
| 146 |
function DropdownMenuLabel({
|
|
@@ -148,7 +148,7 @@ function DropdownMenuLabel({
|
|
| 148 |
inset,
|
| 149 |
...props
|
| 150 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
| 151 |
-
inset?: boolean
|
| 152 |
}) {
|
| 153 |
return (
|
| 154 |
<DropdownMenuPrimitive.Label
|
|
@@ -160,7 +160,7 @@ function DropdownMenuLabel({
|
|
| 160 |
)}
|
| 161 |
{...props}
|
| 162 |
/>
|
| 163 |
-
)
|
| 164 |
}
|
| 165 |
|
| 166 |
function DropdownMenuSeparator({
|
|
@@ -173,7 +173,7 @@ function DropdownMenuSeparator({
|
|
| 173 |
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
| 174 |
{...props}
|
| 175 |
/>
|
| 176 |
-
)
|
| 177 |
}
|
| 178 |
|
| 179 |
function DropdownMenuShortcut({
|
|
@@ -189,13 +189,13 @@ function DropdownMenuShortcut({
|
|
| 189 |
)}
|
| 190 |
{...props}
|
| 191 |
/>
|
| 192 |
-
)
|
| 193 |
}
|
| 194 |
|
| 195 |
function DropdownMenuSub({
|
| 196 |
...props
|
| 197 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
| 198 |
-
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props}
|
| 199 |
}
|
| 200 |
|
| 201 |
function DropdownMenuSubTrigger({
|
|
@@ -204,7 +204,7 @@ function DropdownMenuSubTrigger({
|
|
| 204 |
children,
|
| 205 |
...props
|
| 206 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
| 207 |
-
inset?: boolean
|
| 208 |
}) {
|
| 209 |
return (
|
| 210 |
<DropdownMenuPrimitive.SubTrigger
|
|
@@ -219,7 +219,7 @@ function DropdownMenuSubTrigger({
|
|
| 219 |
{children}
|
| 220 |
<ChevronRightIcon className="ml-auto size-4" />
|
| 221 |
</DropdownMenuPrimitive.SubTrigger>
|
| 222 |
-
)
|
| 223 |
}
|
| 224 |
|
| 225 |
function DropdownMenuSubContent({
|
|
@@ -235,7 +235,7 @@ function DropdownMenuSubContent({
|
|
| 235 |
)}
|
| 236 |
{...props}
|
| 237 |
/>
|
| 238 |
-
)
|
| 239 |
}
|
| 240 |
|
| 241 |
export {
|
|
@@ -254,4 +254,4 @@ export {
|
|
| 254 |
DropdownMenuSub,
|
| 255 |
DropdownMenuSubTrigger,
|
| 256 |
DropdownMenuSubContent,
|
| 257 |
-
}
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
|
| 3 |
+
import * as React from "react";
|
| 4 |
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
| 5 |
+
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
| 6 |
|
| 7 |
+
import { cn } from "@/lib/utils";
|
| 8 |
|
| 9 |
function DropdownMenu({
|
| 10 |
...props
|
| 11 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
| 12 |
+
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
| 13 |
}
|
| 14 |
|
| 15 |
function DropdownMenuPortal({
|
|
|
|
| 17 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
| 18 |
return (
|
| 19 |
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
| 20 |
+
);
|
| 21 |
}
|
| 22 |
|
| 23 |
function DropdownMenuTrigger({
|
|
|
|
| 28 |
data-slot="dropdown-menu-trigger"
|
| 29 |
{...props}
|
| 30 |
/>
|
| 31 |
+
);
|
| 32 |
}
|
| 33 |
|
| 34 |
function DropdownMenuContent({
|
|
|
|
| 48 |
{...props}
|
| 49 |
/>
|
| 50 |
</DropdownMenuPrimitive.Portal>
|
| 51 |
+
);
|
| 52 |
}
|
| 53 |
|
| 54 |
function DropdownMenuGroup({
|
|
|
|
| 56 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
| 57 |
return (
|
| 58 |
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
| 59 |
+
);
|
| 60 |
}
|
| 61 |
|
| 62 |
function DropdownMenuItem({
|
|
|
|
| 65 |
variant = "default",
|
| 66 |
...props
|
| 67 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
| 68 |
+
inset?: boolean;
|
| 69 |
+
variant?: "default" | "destructive";
|
| 70 |
}) {
|
| 71 |
return (
|
| 72 |
<DropdownMenuPrimitive.Item
|
|
|
|
| 74 |
data-inset={inset}
|
| 75 |
data-variant={variant}
|
| 76 |
className={cn(
|
| 77 |
+
"focus:bg-accent cursor-pointer! focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 78 |
className
|
| 79 |
)}
|
| 80 |
{...props}
|
| 81 |
/>
|
| 82 |
+
);
|
| 83 |
}
|
| 84 |
|
| 85 |
function DropdownMenuCheckboxItem({
|
|
|
|
| 105 |
</span>
|
| 106 |
{children}
|
| 107 |
</DropdownMenuPrimitive.CheckboxItem>
|
| 108 |
+
);
|
| 109 |
}
|
| 110 |
|
| 111 |
function DropdownMenuRadioGroup({
|
|
|
|
| 116 |
data-slot="dropdown-menu-radio-group"
|
| 117 |
{...props}
|
| 118 |
/>
|
| 119 |
+
);
|
| 120 |
}
|
| 121 |
|
| 122 |
function DropdownMenuRadioItem({
|
|
|
|
| 140 |
</span>
|
| 141 |
{children}
|
| 142 |
</DropdownMenuPrimitive.RadioItem>
|
| 143 |
+
);
|
| 144 |
}
|
| 145 |
|
| 146 |
function DropdownMenuLabel({
|
|
|
|
| 148 |
inset,
|
| 149 |
...props
|
| 150 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
| 151 |
+
inset?: boolean;
|
| 152 |
}) {
|
| 153 |
return (
|
| 154 |
<DropdownMenuPrimitive.Label
|
|
|
|
| 160 |
)}
|
| 161 |
{...props}
|
| 162 |
/>
|
| 163 |
+
);
|
| 164 |
}
|
| 165 |
|
| 166 |
function DropdownMenuSeparator({
|
|
|
|
| 173 |
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
| 174 |
{...props}
|
| 175 |
/>
|
| 176 |
+
);
|
| 177 |
}
|
| 178 |
|
| 179 |
function DropdownMenuShortcut({
|
|
|
|
| 189 |
)}
|
| 190 |
{...props}
|
| 191 |
/>
|
| 192 |
+
);
|
| 193 |
}
|
| 194 |
|
| 195 |
function DropdownMenuSub({
|
| 196 |
...props
|
| 197 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
| 198 |
+
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
| 199 |
}
|
| 200 |
|
| 201 |
function DropdownMenuSubTrigger({
|
|
|
|
| 204 |
children,
|
| 205 |
...props
|
| 206 |
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
| 207 |
+
inset?: boolean;
|
| 208 |
}) {
|
| 209 |
return (
|
| 210 |
<DropdownMenuPrimitive.SubTrigger
|
|
|
|
| 219 |
{children}
|
| 220 |
<ChevronRightIcon className="ml-auto size-4" />
|
| 221 |
</DropdownMenuPrimitive.SubTrigger>
|
| 222 |
+
);
|
| 223 |
}
|
| 224 |
|
| 225 |
function DropdownMenuSubContent({
|
|
|
|
| 235 |
)}
|
| 236 |
{...props}
|
| 237 |
/>
|
| 238 |
+
);
|
| 239 |
}
|
| 240 |
|
| 241 |
export {
|
|
|
|
| 254 |
DropdownMenuSub,
|
| 255 |
DropdownMenuSubTrigger,
|
| 256 |
DropdownMenuSubContent,
|
| 257 |
+
};
|
components/user-menu/index.tsx
CHANGED
|
@@ -1,6 +1,13 @@
|
|
| 1 |
import Link from "next/link";
|
| 2 |
import { useSession, signIn, signOut } from "next-auth/react";
|
| 3 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import { useTheme } from "next-themes";
|
| 5 |
import { useEffect } from "react";
|
| 6 |
import { FaDiscord } from "react-icons/fa6";
|
|
@@ -11,15 +18,20 @@ import {
|
|
| 11 |
DropdownMenuContent,
|
| 12 |
DropdownMenuItem,
|
| 13 |
DropdownMenuLabel,
|
|
|
|
| 14 |
DropdownMenuSeparator,
|
|
|
|
|
|
|
|
|
|
| 15 |
DropdownMenuTrigger,
|
| 16 |
} from "@/components/ui/dropdown-menu";
|
| 17 |
import { Button } from "@/components/ui/button";
|
| 18 |
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
| 19 |
import { useProjects } from "@/components/projects/useProjects";
|
| 20 |
import { ProjectCard } from "@/components/projects/project-card";
|
| 21 |
-
import {
|
| 22 |
import HFLogo from "@/assets/hf-logo.svg";
|
|
|
|
| 23 |
|
| 24 |
export function UserMenu() {
|
| 25 |
const { data: session, status } = useSession();
|
|
@@ -107,6 +119,21 @@ export function UserMenu() {
|
|
| 107 |
<DropdownMenuContent className="w-64" align="start">
|
| 108 |
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
| 109 |
<DropdownMenuSeparator />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
<DropdownMenuItem>
|
| 111 |
<Link
|
| 112 |
href={`https://huggingface.co/${session.user.username}`}
|
|
@@ -123,6 +150,28 @@ export function UserMenu() {
|
|
| 123 |
Settings
|
| 124 |
</Link>
|
| 125 |
</DropdownMenuItem>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
<>
|
| 127 |
<DropdownMenuSeparator />
|
| 128 |
<DropdownMenuLabel>Projects</DropdownMenuLabel>
|
|
@@ -170,61 +219,27 @@ export function UserMenu() {
|
|
| 170 |
</div>
|
| 171 |
)}
|
| 172 |
</>
|
| 173 |
-
<>
|
| 174 |
-
<DropdownMenuSeparator />
|
| 175 |
-
<DropdownMenuLabel>Social</DropdownMenuLabel>
|
| 176 |
-
<DropdownMenuItem>
|
| 177 |
-
<Link
|
| 178 |
-
href={DISCORD_URL}
|
| 179 |
-
target="_blank"
|
| 180 |
-
className="flex items-center justify-start gap-2"
|
| 181 |
-
>
|
| 182 |
-
<FaDiscord className="size-4 text-indigo-500" />
|
| 183 |
-
Discord
|
| 184 |
-
</Link>
|
| 185 |
-
</DropdownMenuItem>
|
| 186 |
-
<DropdownMenuItem>
|
| 187 |
-
<Link
|
| 188 |
-
href="https://huggingface.co/enzostvs"
|
| 189 |
-
target="_blank"
|
| 190 |
-
className="flex items-center justify-start gap-2"
|
| 191 |
-
>
|
| 192 |
-
<Image src={HFLogo} alt="HF" className="size-4" />
|
| 193 |
-
Hugging Face
|
| 194 |
-
</Link>
|
| 195 |
-
</DropdownMenuItem>
|
| 196 |
-
</>
|
| 197 |
<DropdownMenuSeparator />
|
| 198 |
-
<
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
onClick={() => setTheme("light")}
|
| 204 |
-
className={cn(
|
| 205 |
-
"flex-1",
|
| 206 |
-
theme === "light" && "border-amber-300! bg-amber-500/10!"
|
| 207 |
-
)}
|
| 208 |
>
|
| 209 |
-
<
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
className=
|
| 218 |
-
"flex-1",
|
| 219 |
-
theme === "dark" && "border-indigo-500/50! bg-indigo-500/20!"
|
| 220 |
-
)}
|
| 221 |
>
|
| 222 |
-
<
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
</div>
|
| 227 |
-
<DropdownMenuSeparator />
|
| 228 |
<DropdownMenuItem onClick={handleSignOut}>
|
| 229 |
<LogOut className="size-4" />
|
| 230 |
Sign out
|
|
|
|
| 1 |
import Link from "next/link";
|
| 2 |
import { useSession, signIn, signOut } from "next-auth/react";
|
| 3 |
+
import {
|
| 4 |
+
ArrowRight,
|
| 5 |
+
Check,
|
| 6 |
+
Contrast,
|
| 7 |
+
Folder,
|
| 8 |
+
LogOut,
|
| 9 |
+
Plus,
|
| 10 |
+
} from "lucide-react";
|
| 11 |
import { useTheme } from "next-themes";
|
| 12 |
import { useEffect } from "react";
|
| 13 |
import { FaDiscord } from "react-icons/fa6";
|
|
|
|
| 18 |
DropdownMenuContent,
|
| 19 |
DropdownMenuItem,
|
| 20 |
DropdownMenuLabel,
|
| 21 |
+
DropdownMenuPortal,
|
| 22 |
DropdownMenuSeparator,
|
| 23 |
+
DropdownMenuSub,
|
| 24 |
+
DropdownMenuSubContent,
|
| 25 |
+
DropdownMenuSubTrigger,
|
| 26 |
DropdownMenuTrigger,
|
| 27 |
} from "@/components/ui/dropdown-menu";
|
| 28 |
import { Button } from "@/components/ui/button";
|
| 29 |
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
| 30 |
import { useProjects } from "@/components/projects/useProjects";
|
| 31 |
import { ProjectCard } from "@/components/projects/project-card";
|
| 32 |
+
import { DISCORD_URL } from "@/lib/utils";
|
| 33 |
import HFLogo from "@/assets/hf-logo.svg";
|
| 34 |
+
import ProIcon from "@/assets/pro.svg";
|
| 35 |
|
| 36 |
export function UserMenu() {
|
| 37 |
const { data: session, status } = useSession();
|
|
|
|
| 119 |
<DropdownMenuContent className="w-64" align="start">
|
| 120 |
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
| 121 |
<DropdownMenuSeparator />
|
| 122 |
+
{!session?.user?.isPro && (
|
| 123 |
+
<>
|
| 124 |
+
<DropdownMenuItem>
|
| 125 |
+
<Link
|
| 126 |
+
href="https://huggingface.co/pro"
|
| 127 |
+
className="flex items-center gap-1.5 bg-linear-to-r from-pink-500 via-green-500 to-amber-500 text-transparent bg-clip-text font-semibold"
|
| 128 |
+
target="_blank"
|
| 129 |
+
>
|
| 130 |
+
<Image alt="Pro" src={ProIcon} className="size-3.5" />
|
| 131 |
+
Subscribe to Pro
|
| 132 |
+
</Link>
|
| 133 |
+
</DropdownMenuItem>
|
| 134 |
+
<DropdownMenuSeparator />
|
| 135 |
+
</>
|
| 136 |
+
)}
|
| 137 |
<DropdownMenuItem>
|
| 138 |
<Link
|
| 139 |
href={`https://huggingface.co/${session.user.username}`}
|
|
|
|
| 150 |
Settings
|
| 151 |
</Link>
|
| 152 |
</DropdownMenuItem>
|
| 153 |
+
<DropdownMenuSub>
|
| 154 |
+
<DropdownMenuSubTrigger className="flex items-center justify-start gap-1.5">
|
| 155 |
+
<Contrast className="size-3.5" />
|
| 156 |
+
Appearance
|
| 157 |
+
</DropdownMenuSubTrigger>
|
| 158 |
+
<DropdownMenuPortal>
|
| 159 |
+
<DropdownMenuSubContent>
|
| 160 |
+
<DropdownMenuItem onClick={() => setTheme("light")}>
|
| 161 |
+
Light
|
| 162 |
+
{theme === "light" && <Check />}
|
| 163 |
+
</DropdownMenuItem>
|
| 164 |
+
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
| 165 |
+
Dark
|
| 166 |
+
{theme === "dark" && <Check />}
|
| 167 |
+
</DropdownMenuItem>
|
| 168 |
+
<DropdownMenuItem onClick={() => setTheme("system")}>
|
| 169 |
+
System
|
| 170 |
+
{theme === "system" && <Check />}
|
| 171 |
+
</DropdownMenuItem>
|
| 172 |
+
</DropdownMenuSubContent>
|
| 173 |
+
</DropdownMenuPortal>
|
| 174 |
+
</DropdownMenuSub>
|
| 175 |
<>
|
| 176 |
<DropdownMenuSeparator />
|
| 177 |
<DropdownMenuLabel>Projects</DropdownMenuLabel>
|
|
|
|
| 219 |
</div>
|
| 220 |
)}
|
| 221 |
</>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
<DropdownMenuSeparator />
|
| 223 |
+
<DropdownMenuItem>
|
| 224 |
+
<Link
|
| 225 |
+
href={DISCORD_URL}
|
| 226 |
+
target="_blank"
|
| 227 |
+
className="flex items-center justify-start gap-2"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
>
|
| 229 |
+
<FaDiscord className="size-4" />
|
| 230 |
+
Discord
|
| 231 |
+
</Link>
|
| 232 |
+
</DropdownMenuItem>
|
| 233 |
+
<DropdownMenuItem>
|
| 234 |
+
<Link
|
| 235 |
+
href="https://huggingface.co/enzostvs"
|
| 236 |
+
target="_blank"
|
| 237 |
+
className="flex items-center justify-start gap-2"
|
|
|
|
|
|
|
|
|
|
| 238 |
>
|
| 239 |
+
<Image src={HFLogo} alt="HF" className="size-4 grayscale" />
|
| 240 |
+
Hugging Face
|
| 241 |
+
</Link>
|
| 242 |
+
</DropdownMenuItem>
|
|
|
|
|
|
|
| 243 |
<DropdownMenuItem onClick={handleSignOut}>
|
| 244 |
<LogOut className="size-4" />
|
| 245 |
Sign out
|
lib/auth.ts
CHANGED
|
@@ -24,28 +24,28 @@ export const authConfig = {
|
|
| 24 |
id: profile.sub,
|
| 25 |
name: profile.name || profile.preferred_username,
|
| 26 |
username: profile.preferred_username,
|
| 27 |
-
email: profile.email,
|
| 28 |
image: profile.picture,
|
|
|
|
| 29 |
};
|
| 30 |
},
|
| 31 |
},
|
| 32 |
],
|
| 33 |
callbacks: {
|
| 34 |
async jwt({ token, account, user }) {
|
| 35 |
-
// Persist the OAuth access_token and user info to the token right after signin
|
| 36 |
if (account) {
|
| 37 |
token.accessToken = account.access_token;
|
| 38 |
}
|
| 39 |
if (user) {
|
| 40 |
token.username = user.username;
|
|
|
|
| 41 |
}
|
| 42 |
return token;
|
| 43 |
},
|
| 44 |
async session({ session, token }) {
|
| 45 |
-
// Send properties to the client, like an access_token from a provider
|
| 46 |
session.accessToken = token.accessToken as string;
|
| 47 |
if (session.user) {
|
| 48 |
session.user.username = token.username as string;
|
|
|
|
| 49 |
}
|
| 50 |
return session;
|
| 51 |
},
|
|
|
|
| 24 |
id: profile.sub,
|
| 25 |
name: profile.name || profile.preferred_username,
|
| 26 |
username: profile.preferred_username,
|
|
|
|
| 27 |
image: profile.picture,
|
| 28 |
+
isPro: profile.isPro || false,
|
| 29 |
};
|
| 30 |
},
|
| 31 |
},
|
| 32 |
],
|
| 33 |
callbacks: {
|
| 34 |
async jwt({ token, account, user }) {
|
|
|
|
| 35 |
if (account) {
|
| 36 |
token.accessToken = account.access_token;
|
| 37 |
}
|
| 38 |
if (user) {
|
| 39 |
token.username = user.username;
|
| 40 |
+
token.isPro = user.isPro;
|
| 41 |
}
|
| 42 |
return token;
|
| 43 |
},
|
| 44 |
async session({ session, token }) {
|
|
|
|
| 45 |
session.accessToken = token.accessToken as string;
|
| 46 |
if (session.user) {
|
| 47 |
session.user.username = token.username as string;
|
| 48 |
+
session.user.isPro = token.isPro as boolean;
|
| 49 |
}
|
| 50 |
return session;
|
| 51 |
},
|
next-auth.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ import NextAuth, { DefaultSession } from "next-auth";
|
|
| 3 |
declare module "next-auth" {
|
| 4 |
interface Session {
|
| 5 |
accessToken?: string;
|
|
|
|
| 6 |
}
|
| 7 |
-
|
| 8 |
interface User {
|
| 9 |
username?: string;
|
|
|
|
| 10 |
}
|
| 11 |
}
|
| 12 |
|
|
@@ -14,6 +16,6 @@ declare module "next-auth/jwt" {
|
|
| 14 |
interface JWT {
|
| 15 |
accessToken?: string;
|
| 16 |
username?: string;
|
|
|
|
| 17 |
}
|
| 18 |
}
|
| 19 |
-
|
|
|
|
| 3 |
declare module "next-auth" {
|
| 4 |
interface Session {
|
| 5 |
accessToken?: string;
|
| 6 |
+
isPro?: boolean;
|
| 7 |
}
|
| 8 |
+
|
| 9 |
interface User {
|
| 10 |
username?: string;
|
| 11 |
+
isPro?: boolean;
|
| 12 |
}
|
| 13 |
}
|
| 14 |
|
|
|
|
| 16 |
interface JWT {
|
| 17 |
accessToken?: string;
|
| 18 |
username?: string;
|
| 19 |
+
isPro?: boolean;
|
| 20 |
}
|
| 21 |
}
|
|
|