diff --git a/app/(modal)/(routes)/settings/page.tsx b/app/(modal)/(routes)/settings/page.tsx deleted file mode 100644 index 01e14ca..0000000 --- a/app/(modal)/(routes)/settings/page.tsx +++ /dev/null @@ -1,223 +0,0 @@ -"use client"; - -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import axios from "axios"; -import { Button } from "@/components/ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { Info } from "lucide-react"; -import * as React from 'react'; -import { Toggle } from "@/components/ui/toggle"; -import { ApiKey, TwitchConnection, User } from "@prisma/client"; -import { useEffect, useState } from "react"; -import { useSession } from "next-auth/react"; -import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import Link from "next/link"; -import { TTSBadgeFilterModal } from "@/components/modals/tts-badge-filter-modal"; - -const SettingsPage = () => { - const { data: session, status } = useSession(); - const [previousUsername, setPreviousUsername] = useState() - const [userId, setUserId] = useState() - - useEffect(() => { - if (status !== "authenticated" || previousUsername == session.user?.name) { - return - } - - setPreviousUsername(session.user?.name as string) - if (session.user?.name) { - const fetchData = async () => { - var connection: User = (await axios.get("/api/account")).data - setUserId(connection.id) - } - - fetchData().catch(console.error) - } - }, [session]) - - // const [twitchUser, setTwitchUser] = useState(null) - // useEffect(() => { - // const fetchData = async () => { - // var connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data - // setTwitchUser(connection) - // } - - // fetchData().catch(console.error); - // }, []) - - // const OnTwitchConnectionDelete = async () => { - // try { - // await axios.post("/api/settings/connections/twitch/delete") - // setTwitchUser(null) - // } catch (error) { - // console.log("ERROR", error) - // } - // } - - const [apiKeyViewable, setApiKeyViewable] = useState(0) - const [apiKeyChanges, setApiKeyChanges] = useState(0) - const [apiKeys, setApiKeys] = useState([]) - useEffect(() => { - const fetchData = async () => { - try { - const keys = (await axios.get("/api/tokens")).data ?? {}; - setApiKeys(keys) - console.log(keys); - } catch (error) { - console.log("ERROR", error) - } - }; - - fetchData().catch(console.error); - }, [apiKeyChanges]); - - const onApiKeyAdd = async () => { - try { - await axios.post("/api/token", { - label: "Key label" - }); - setApiKeyChanges(apiKeyChanges + 1) - } catch (error) { - console.log("ERROR", error) - } - } - - const onApiKeyDelete = async (id: string) => { - try { - await axios.delete("/api/token/" + id); - setApiKeyChanges(apiKeyChanges - 1) - } catch (error) { - console.log("ERROR", error) - } - } - - useEffect(() => { - const fetchData = async () => { - try { - const keys = (await axios.get("/api/tokens")).data; - setApiKeys(keys) - console.log(keys); - } catch (error) { - console.log("ERROR", error) - } - }; - - fetchData().catch(console.error); - }, [apiKeyViewable]); - - return ( -
-
- Settings -
-
-
-
Connections
-
-
- - - - -
- {/*
*/} - Authorize - {/*
Twitch
-
- -
-
-

{twitchUser?.username}

-
-
-
- -
*/} -
-
-
-
API Keys
- - A list of your secret API keys. - - - Label - Token - View - Action - - - - {apiKeys.map((key, index) => ( - - {key.label} - {(apiKeyViewable & (1 << index)) > 0 ? key.id : "*".repeat(key.id.length)} - - - - - - ))} - - - - - - - -
-
-
-
-
- - TTS Voice -
-
- - - - - - English Voices - - - Brian - Amy - Emma - - - -
-
- -
-
TTS Message Filter
-
- - {/* */} - {/* */} - -
-
-
- ); -} - -export default SettingsPage; \ No newline at end of file diff --git a/app/api/account/authorize/route.ts b/app/api/account/authorize/route.ts index e8f1ff2..b86c84a 100644 --- a/app/api/account/authorize/route.ts +++ b/app/api/account/authorize/route.ts @@ -15,7 +15,7 @@ export async function GET(req: Request) { if (!code || !scope || !state) { return new NextResponse("Bad Request", { status: 400 }); } - console.log("VERIFY") + // Verify state against user id in user table. const user = await db.user.findFirst({ where: { @@ -23,12 +23,10 @@ export async function GET(req: Request) { } }) - console.log("USER", user) if (!user) { return new NextResponse("Bad Request", { status: 400 }); } - console.log("FETCH TOKEN") // Post to https://id.twitch.tv/oauth2/token const token: { access_token:string, expires_in:number, refresh_token:string, token_type:string, scope:string[] } = (await axios.post("https://id.twitch.tv/oauth2/token", { client_id: process.env.TWITCH_BOT_CLIENT_ID, @@ -37,13 +35,12 @@ export async function GET(req: Request) { grant_type: "authorization_code", redirect_uri: "https://hermes.goblincaves.com/api/account/authorize" })).data - console.log("TOKEN", token) // Fetch values from token. const { access_token, expires_in, refresh_token, token_type } = token - console.log("AT", access_token) - console.log("RT", refresh_token) - console.log("TT", token_type) + // console.log("AT", access_token) + // console.log("RT", refresh_token) + // console.log("TT", token_type) if (!access_token || !refresh_token || token_type !== "bearer") { return new NextResponse("Unauthorized", { status: 401 }); diff --git a/app/api/account/reauthorize/route.ts b/app/api/account/reauthorize/route.ts index e2027e4..586f082 100644 --- a/app/api/account/reauthorize/route.ts +++ b/app/api/account/reauthorize/route.ts @@ -12,7 +12,6 @@ export async function GET(req: Request) { } }) - console.log("API USER:", key) if (!key) { return new NextResponse("Forbidden", { status: 403 }); } @@ -35,7 +34,6 @@ export async function GET(req: Request) { if (expires_in > 3600) return new NextResponse("", { status: 201 }); } catch (error) { - console.log("Oudated Twitch token.") } // Post to https://id.twitch.tv/oauth2/token @@ -48,9 +46,9 @@ export async function GET(req: Request) { // Fetch values from token. const { access_token, expires_in, refresh_token, token_type } = token - console.log("AT", access_token) - console.log("RT", refresh_token) - console.log("TT", token_type) + // console.log("AT", access_token) + // console.log("RT", refresh_token) + // console.log("TT", token_type) if (!access_token || !refresh_token || token_type !== "bearer") { return new NextResponse("Unauthorized", { status: 401 }); diff --git a/app/api/account/route.ts b/app/api/account/route.ts index 2b26fba..ac976c9 100644 --- a/app/api/account/route.ts +++ b/app/api/account/route.ts @@ -29,16 +29,10 @@ export async function POST(req: Request) { }); if (exist) { - // const apikey = await db.apiKey.findFirst({ - // where: { - // userId: user.toLowerCase() as string - // } - // }) - return { + return NextResponse.json({ id: exist.id, - username: exist.username, - //key: apikey?.id as string - }; + username: exist.username + }); } const newUser = await db.user.create({ @@ -47,18 +41,9 @@ export async function POST(req: Request) { } }); - // const apikey = await db.apiKey.create({ - // data: { - // id: generateToken(), - // label: "Default", - // userId: user.toLowerCase() as string - // } - // }) - return NextResponse.json({ id: newUser.id, - username: newUser.username, - //key: apikey.id + username: newUser.username }); } catch (error) { console.log("[ACCOUNT]", error); diff --git a/app/api/settings/connections/twitch/route.ts b/app/api/settings/connections/twitch/route.ts index d7f899a..be4d8b8 100644 --- a/app/api/settings/connections/twitch/route.ts +++ b/app/api/settings/connections/twitch/route.ts @@ -37,12 +37,11 @@ export async function POST(req: Request) { try { const { id, secret } = await req.json(); const user = await fetchUserUsingAPI(req) - console.log("userrr:", user) + if (!user) { return new NextResponse("Unauthorized", { status: 401 }); } - console.log(id, secret) let response = null; try { response = await axios.post("https://id.twitch.tv/oauth2/token", { @@ -69,7 +68,7 @@ export async function POST(req: Request) { const connection = await db.twitchConnection.create({ data: { - id, + id: id, secret, userId: user.id as string, broadcasterId, diff --git a/app/(modal)/(routes)/page.tsx b/app/page.tsx similarity index 100% rename from app/(modal)/(routes)/page.tsx rename to app/page.tsx diff --git a/app/settings/api/keys/page.tsx b/app/settings/api/keys/page.tsx new file mode 100644 index 0000000..d858fd7 --- /dev/null +++ b/app/settings/api/keys/page.tsx @@ -0,0 +1,108 @@ +"use client"; + +import axios from "axios"; +import { Button } from "@/components/ui/button"; +import * as React from 'react'; +import { ApiKey, User } from "@prisma/client"; +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; + +const SettingsPage = () => { + const { data: session, status } = useSession(); + + const [apiKeyViewable, setApiKeyViewable] = useState(0) + const [apiKeyChanges, setApiKeyChanges] = useState(0) + const [apiKeys, setApiKeys] = useState([]) + + useEffect(() => { + const fetchData = async () => { + try { + const keys = (await axios.get("/api/tokens")).data ?? {}; + setApiKeys(keys) + console.log(keys); + } catch (error) { + console.log("ERROR", error) + } + }; + + fetchData().catch(console.error); + }, [apiKeyChanges]); + + const onApiKeyAdd = async () => { + try { + await axios.post("/api/token", { + label: "Key label" + }); + setApiKeyChanges(apiKeyChanges + 1) + } catch (error) { + console.log("ERROR", error) + } + } + + const onApiKeyDelete = async (id: string) => { + try { + await axios.delete("/api/token/" + id); + setApiKeyChanges(apiKeyChanges - 1) + } catch (error) { + console.log("ERROR", error) + } + } + + useEffect(() => { + const fetchData = async () => { + try { + const keys = (await axios.get("/api/tokens")).data; + setApiKeys(keys) + console.log(keys); + } catch (error) { + console.log("ERROR", error) + } + }; + + fetchData().catch(console.error); + }, [apiKeyViewable]); + + return ( +
+
+
+
API Keys
+ + A list of your secret API keys. + + + Label + Token + View + Action + + + + {apiKeys.map((key, index) => ( + + {key.label} + {(apiKeyViewable & (1 << index)) > 0 ? key.id : "*".repeat(key.id.length)} + + + + + + ))} + + + + + + + +
+
+
+
+ ); +} + +export default SettingsPage; \ No newline at end of file diff --git a/app/settings/connections/page.tsx b/app/settings/connections/page.tsx new file mode 100644 index 0000000..195d84e --- /dev/null +++ b/app/settings/connections/page.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import axios from "axios"; +import * as React from 'react'; +import { ApiKey, TwitchConnection, User } from "@prisma/client"; +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import { Skeleton } from "@/components/ui/skeleton"; + +const SettingsPage = () => { + const { data: session, status } = useSession(); + const [previousUsername, setPreviousUsername] = useState() + const [userId, setUserId] = useState() + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (status !== "authenticated" || previousUsername == session.user?.name) { + return + } + + setPreviousUsername(session.user?.name as string) + if (session.user?.name) { + const fetchData = async () => { + var connection: User = (await axios.get("/api/account")).data + setUserId(connection.id) + setLoading(false) + } + + fetchData().catch(console.error) + } + }, [session]) + + const [twitchUser, setTwitchUser] = useState(null) + useEffect(() => { + const fetchData = async () => { + var connection: TwitchConnection = (await axios.get("/api/settings/connections/twitch")).data + setTwitchUser(connection) + } + + fetchData().catch(console.error); + }, []) + + const OnTwitchConnectionDelete = async () => { + try { + await axios.post("/api/settings/connections/twitch/delete") + setTwitchUser(null) + } catch (error) { + console.log("ERROR", error) + } + } + + return ( +
+
Connections
+
+
+
+
+
+ + + + +
+
+
Twitch
+
+ Connect your Twitch account! +
+
+

{twitchUser?.broadcasterId}

+
+
+
+
+ +
+ + +
+ + +
+
+
+
+
+
+ ); +} + +export default SettingsPage; \ No newline at end of file diff --git a/app/settings/layout.tsx b/app/settings/layout.tsx new file mode 100644 index 0000000..2fa4a6c --- /dev/null +++ b/app/settings/layout.tsx @@ -0,0 +1,25 @@ +import SettingsNavigation from "@/components/navigation/settings"; +import { cn } from "@/lib/utils"; +import { headers } from 'next/headers'; +import React from "react"; + +const SettingsLayout = async ( + { children } : { children:React.ReactNode } ) => { + const headersList = headers(); + const header_url = headersList.get('x-url') || ""; + console.log("HEADER URL: " + header_url) + + return ( +
+
+ +
+ +
+ {children} +
+
+ ); +} + +export default SettingsLayout; \ No newline at end of file diff --git a/app/settings/page.tsx b/app/settings/page.tsx new file mode 100644 index 0000000..d9169ed --- /dev/null +++ b/app/settings/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +const SettingsPage = () => { + return ( +
+
+ ); +} + +export default SettingsPage; \ No newline at end of file diff --git a/app/settings/tts/filters/page.tsx b/app/settings/tts/filters/page.tsx new file mode 100644 index 0000000..a74a319 --- /dev/null +++ b/app/settings/tts/filters/page.tsx @@ -0,0 +1,250 @@ +"use client"; + +import axios from "axios"; +import * as React from 'react'; +import { Calendar, Check, ChevronsUpDown, MoreHorizontal, Plus, Tags, Trash, User } from "lucide-react" +import { ApiKey, TwitchConnection } from "@prisma/client"; +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button" +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import * as z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { DropdownMenu, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from "@/components/ui/dropdown-menu"; +import { DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { Label } from "@/components/ui/label"; + +const TTSFiltersPage = () => { + const { data: session, status } = useSession(); + const [loading, setLoading] = useState(true) + const [moreOpen, setMoreOpen] = useState(0) + const [tag, setTag] = useState("blacklisted") + const [open, setOpen] = useState(false) + const [userTags, setUserTag] = useState<{ username: string, tag: string }[]>([{ username: "test", tag:"blacklisted" }, { username: "hello world", tag:"moderator" }]) + const router = useRouter(); + + const labels = [ + "blacklisted", + "priority" + ] + + // Username blacklist + const usernameFilteredFormSchema = z.object({ + username: z.string().trim().min(4).max(25).regex(new RegExp("[a-zA-Z0-9][a-zA-Z0-9_]{3, 24}"), "Must be a valid twitch username.") + }); + + + const usernameFilteredForm = useForm({ + resolver: zodResolver(usernameFilteredFormSchema), + defaultValues: { + username: "" + } + }); + + useEffect(() => { + const fetchData = async () => { + try { + // const userFiltersData = (await axios.get("/api/settings/tts/filters/users")).data ?? {}; + // setApiKeys(userFiltersData) + // console.log(userFiltersData); + } catch (error) { + console.log("ERROR", error) + } + }; + + fetchData().catch(console.error); + }, []); + + const onApiKeyAdd = async () => { + ///let usernames = blacklistedUsers.split("\n"); + //console.log(usernames) + // try { + // await axios.post("/api/settings/tts/filters/users", { + // usernames: [] + // }); + // setUserFilters(userFilters.concat(usernames)) + // } catch (error) { + // console.log("ERROR", error) + // } + } + + const onApiKeyDelete = async (username: string) => { + try { + await axios.delete("/api/settings/tts/filters/users/" + username); + //setUserFilters(userFilters.filter((u) => u != username)) + } catch (error) { + console.log("ERROR", error) + } + } + + const isLoading = usernameFilteredForm.formState.isSubmitting; + + const addTwitchUser = (values: z.infer) => { + let response = null; + console.log("TEST") + console.log(values) + + // try { + // response = await axios.post("/api/settings/tts/filter/badges", values); + // } catch (error) { + // console.log("[CONNECTIONS/TWITCH/POST]", error); + // return; + // } + + userTags.push({ username: values.username, tag: tag }) + setUserTag(userTags) + usernameFilteredForm.reset(); + router.refresh(); + //window.location.reload(); + } + + return ( +
+
TTS Filters
+
+
+ {userTags.map((user, index) => ( +
+

+ + {user.tag} + + {user.username} +

+ 0} onOpenChange={() => setMoreOpen((v) => v ^ (1 << index))}> + + + + + Actions + + + + + Apply label + + + + + + No label found. + + {labels.map((label) => ( + { + userTags[index].tag = value + setUserTag(userTags) + setMoreOpen(0) + }} + > + {label} + + ))} + + + + + + + { + userTags.splice(index, 1) + setUserTag(userTags) + }} className="text-red-600"> + + Delete + + + + +
+ ))} +
+ +
+ + ( + + + + + + + )} + /> + + + + + + + Actions + + + + + Apply label + + + + + + No label found. + + {labels.map((label) => ( + { + setTag(value) + setOpen(false) + }} + > + {label} + + ))} + + + + + + + + +
+
+ +
+
+
+ ); +} + +export default TTSFiltersPage; \ No newline at end of file diff --git a/app/settings/tts/voices/page.tsx b/app/settings/tts/voices/page.tsx new file mode 100644 index 0000000..332eb1f --- /dev/null +++ b/app/settings/tts/voices/page.tsx @@ -0,0 +1,110 @@ +"use client"; + +import axios from "axios"; +import * as React from 'react'; +import { Check, ChevronsUpDown } from "lucide-react" +import { ApiKey, TwitchConnection, User } from "@prisma/client"; +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button" +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { Label } from "@/components/ui/label"; + +const TTSFiltersPage = () => { + const { data: session, status } = useSession(); + const [loading, setLoading] = useState(true) + + const [open, setOpen] = useState(false) + const [value, setValue] = useState("") + + const voices = [ + { + value: "brian", + label: "Brian", + gender: "Male", + language: "en" + }, + { + value: "sveltekit", + label: "SvelteKit", + gender: "Male" + }, + { + value: "nuxt.js", + label: "Nuxt.js", + gender: "Male", + language: "en" + }, + { + value: "remix", + label: "Remix", + gender: "Male", + language: "en" + }, + { + value: "astro", + label: "Astro", + gender: "Male", + language: "en" + }, + ] + + return ( +
+
TTS Voices
+
+
+
Default Voice
+ + + + + + + + + No framework found. + + {voices.map((voice) => ( + { + setValue(currentValue === value ? "" : currentValue) + setOpen(false) + }} + > + + {voice.label} + + ))} + + + + +
+
+
+ ); +} + +export default TTSFiltersPage; \ No newline at end of file diff --git a/components/navigation/settings.tsx b/components/navigation/settings.tsx new file mode 100644 index 0000000..2ccee11 --- /dev/null +++ b/components/navigation/settings.tsx @@ -0,0 +1,61 @@ +import Link from "next/link"; +import { Button } from "../ui/button"; +import UserProfile from "./userprofile"; + +const SettingsNavigation = async () => { + return ( +
+
Hermes
+ +
+ +
+ +
+
    +
  • + Settings +
  • +
  • + + + +
  • + +
  • + Text to Speech +
  • +
  • + + + +
  • +
  • + + + +
  • + +
  • + API +
  • +
  • + + + +
  • +
+
+
+ ); +} + +export default SettingsNavigation; \ No newline at end of file diff --git a/components/navigation/userprofile.tsx b/components/navigation/userprofile.tsx new file mode 100644 index 0000000..a9bd09e --- /dev/null +++ b/components/navigation/userprofile.tsx @@ -0,0 +1,45 @@ +"use client" + +import axios from "axios"; +import * as React from 'react'; +import { User } from "@prisma/client"; +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { usePathname } from 'next/navigation' +import { cn } from "@/lib/utils"; + +const UserProfile = () => { + const { data: session, status } = useSession(); + const [previousUsername, setPreviousUsername] = useState() + const [user, setUser] = useState() + const [loading, setLoading] = useState(true) + const pathname = usePathname() + + useEffect(() => { + if (status !== "authenticated" || previousUsername == session.user?.name) { + return + } + + setPreviousUsername(session.user?.name as string) + if (session.user) { + const fetchData = async () => { + var userData: User = (await axios.get("/api/account")).data + setUser(userData) + setLoading(false) + } + + fetchData().catch(console.error) + + // TODO: check cookies if impersonation is in use. + } + }, [session]) + + return ( +
+

Logged in as:

+

{user?.username}

+
+ ); +} + +export default UserProfile; \ No newline at end of file diff --git a/components/ui/command.tsx b/components/ui/command.tsx new file mode 100644 index 0000000..17cc641 --- /dev/null +++ b/components/ui/command.tsx @@ -0,0 +1,155 @@ +"use client" + +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/components/ui/navigation-menu.tsx b/components/ui/navigation-menu.tsx new file mode 100644 index 0000000..1419f56 --- /dev/null +++ b/components/ui/navigation-menu.tsx @@ -0,0 +1,128 @@ +import * as React from "react" +import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" +import { cva } from "class-variance-authority" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const NavigationMenu = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + +)) +NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName + +const NavigationMenuList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName + +const NavigationMenuItem = NavigationMenuPrimitive.Item + +const navigationMenuTriggerStyle = cva( + "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" +) + +const NavigationMenuTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children}{" "} + +)) +NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName + +const NavigationMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName + +const NavigationMenuLink = NavigationMenuPrimitive.Link + +const NavigationMenuViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ +
+)) +NavigationMenuViewport.displayName = + NavigationMenuPrimitive.Viewport.displayName + +const NavigationMenuIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +
+ +)) +NavigationMenuIndicator.displayName = + NavigationMenuPrimitive.Indicator.displayName + +export { + navigationMenuTriggerStyle, + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuContent, + NavigationMenuTrigger, + NavigationMenuLink, + NavigationMenuIndicator, + NavigationMenuViewport, +} diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx new file mode 100644 index 0000000..a0ec48b --- /dev/null +++ b/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx new file mode 100644 index 0000000..01b8b6d --- /dev/null +++ b/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx new file mode 100644 index 0000000..9f9a6dc --- /dev/null +++ b/components/ui/textarea.tsx @@ -0,0 +1,24 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +