Clean up upload form

This commit is contained in:
Fergal Moran
2024-09-10 18:46:33 +01:00
parent b07295ad0d
commit f9e7f3e70d
19 changed files with 492 additions and 8849 deletions

BIN
bun.lockb

Binary file not shown.

8519
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@auth/drizzle-adapter": "^1.4.2", "@auth/drizzle-adapter": "^1.4.2",
"@headlessui/react": "^2.1.5", "@headlessui/react": "^2.1.6",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.1.1",
@@ -47,7 +47,7 @@
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"@t3-oss/env-nextjs": "^0.11.1", "@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^5.55.1", "@tanstack/react-query": "^5.55.4",
"@trpc/client": "^11.0.0-rc.446", "@trpc/client": "^11.0.0-rc.446",
"@trpc/react-query": "^11.0.0-rc.446", "@trpc/react-query": "^11.0.0-rc.446",
"@trpc/server": "^11.0.0-rc.446", "@trpc/server": "^11.0.0-rc.446",
@@ -69,7 +69,7 @@
"loglevel": "^1.9.2", "loglevel": "^1.9.2",
"loglevel-plugin-prefix": "^0.8.4", "loglevel-plugin-prefix": "^0.8.4",
"lucide-react": "^0.439.0", "lucide-react": "^0.439.0",
"next": "^14.2.8", "next": "^14.2.9",
"next-auth": "^4.24.7", "next-auth": "^4.24.7",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"postgres": "^3.4.4", "postgres": "^3.4.4",
@@ -91,21 +91,21 @@
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^9.0.0", "@faker-js/faker": "^9.0.0",
"@types/eslint": "^8.56.12", "@types/eslint": "^9.6.1",
"@types/node": "^22.5.4", "@types/node": "^22.5.4",
"@types/react": "^18.3.5", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.4.0", "@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.4.0", "@typescript-eslint/parser": "^8.5.0",
"drizzle-kit": "^0.24.2", "drizzle-kit": "^0.24.2",
"eslint": "^8.57.0", "eslint": "^9.10.0",
"eslint-config-next": "^14.2.8", "eslint-config-next": "^14.2.9",
"eslint-plugin-drizzle": "^0.2.3", "eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.4.45", "postcss": "^8.4.45",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6", "prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"typescript": "^5.5.4" "typescript": "^5.6.2"
}, },
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.37.0" "initVersion": "7.37.0"

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -8,13 +8,12 @@ interface AuthLayoutProps {
export default function AuthLayout({ children }: AuthLayoutProps) { export default function AuthLayout({ children }: AuthLayoutProps) {
return ( return (
<div className="min-h-screen"> <div className="container flex h-screen w-screen flex-col items-center ">
<div className="container flex h-screen w-screen flex-col items-center justify-center">
<Link <Link
href="/" href="/"
className={cn( className={cn(
buttonVariants({ variant: "ghost" }), buttonVariants({ variant: "ghost" }),
"absolute left-4 top-4 md:left-8 md:top-8", "absolute left-4 top-4 mt-8 md:left-8 md:top-8",
)} )}
> >
<> <>
@@ -22,9 +21,7 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
Back Back
</> </>
</Link> </Link>
<div className="mt-8">{children}</div>
{children}
</div>
</div> </div>
); );
} }

View File

@@ -0,0 +1,5 @@
import UploadPage from "@/components/pages/upload-page";
const Upload = () => <UploadPage />;
export default Upload;

View File

@@ -1,4 +1,4 @@
import { Roboto as font } from "next/font/google"; import { Inknut_Antiqua as font } from "next/font/google";
import "@/styles/globals.css"; import "@/styles/globals.css";
import { type Metadata, type Viewport } from "next"; import { type Metadata, type Viewport } from "next";
@@ -13,6 +13,7 @@ import TopNavbar from "@/components/navbar/top-navbar";
import { dashboardConfig } from "@/config/top-nav.config"; import { dashboardConfig } from "@/config/top-nav.config";
import { siteConfig } from "@/config/site.config"; import { siteConfig } from "@/config/site.config";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { SessionProvider } from "next-auth/react";
export const viewport: Viewport = { export const viewport: Viewport = {
themeColor: [ themeColor: [
@@ -21,8 +22,8 @@ export const viewport: Viewport = {
], ],
}; };
const f = font({ const f = font({
weight: "400",
subsets: ["latin"], subsets: ["latin"],
weight: ["400"],
}); });
export const metadata: Metadata = { export const metadata: Metadata = {

View File

@@ -49,6 +49,7 @@ const SignInForm: React.FC = () => {
logger.debug("signin", "result", result); logger.debug("signin", "result", result);
if (result?.status === 200) { if (result?.status === 200) {
router.push("/"); router.push("/");
window.location.reload();
} }
} catch (error) { } catch (error) {
logger.error("SignInForm", "error", error); logger.error("SignInForm", "error", error);

View File

@@ -26,6 +26,7 @@ import {
type Icon as LucideIcon, type Icon as LucideIcon,
Terminal, Terminal,
LogIn, LogIn,
Upload,
} from "lucide-react"; } from "lucide-react";
export type Icon = typeof LucideIcon; export type Icon = typeof LucideIcon;
@@ -112,4 +113,5 @@ export const Icons = {
), ),
twitter: Twitter, twitter: Twitter,
check: Check, check: Check,
upload: Upload,
}; };

View File

@@ -5,12 +5,12 @@ import React from "react";
import { type NavItem } from "@/types"; import { type NavItem } from "@/types";
import { type Session } from "next-auth"; import { type Session } from "next-auth";
import { Icons } from "../icons"; import { Icons } from "@/components/icons";
import { siteConfig } from "@/config/site.config"; import { siteConfig } from "@/config/site.config";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useSelectedLayoutSegment } from "next/navigation"; import { useSelectedLayoutSegment } from "next/navigation";
import LoginButton from "../widgets/login/login-button"; import LoginButton from "@/components/widgets/login/login-button";
type TopNavbarProps = { type TopNavbarProps = {
items: NavItem[]; items: NavItem[];

View File

@@ -0,0 +1,137 @@
"use client";
import React from "react";
import { Form, FormProvider, useForm } from "react-hook-form";
import { SubmitHandler, Controller } from "react-hook-form";
import { useRouter } from "next/navigation";
import ImageUpload from "@/components/widgets/image-upload";
import TaggedInput from "@/components/widgets/tagged-input";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
type FormValues = {
title: string;
description: string;
tags: string[];
image: string | undefined;
};
const UploadPage: React.FC = () => {
const router = useRouter();
const form = useForm<FormValues>({
defaultValues: {
title: "",
description: "",
tags: [],
image: undefined,
},
});
const _submit: SubmitHandler<FormValues> = async (data) => {
console.log(data);
if (data.image) {
const body = new FormData();
body.append("title", data.title);
body.append("description", data.description);
body.append("tags", data.tags.join("|"));
body.append("file", data.image);
const response = await fetch("api/upload", {
method: "POST",
body,
});
if (response.status === 201) {
await router.replace("/");
}
}
};
return (
<div className="md:grid md:grid-cols-3 md:gap-6">
<div className="md:col-span-1">
<div className="px-4 sm:px-0">
<h3 className="text-lg font-extrabold leading-6">Upload a new gif</h3>
<p className="text-base-content/70 my-3 text-sm">
The more info you can give us the better.
</p>
</div>
</div>
<div className="md:col-span-2">
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(_submit)}>
<div className="shadow sm:overflow-hidden sm:rounded-md">
<div className="space-y-4 px-4">
<FormField
control={form.control}
name="title"
defaultValue={"fergal.moran+opengifame@gmail.com"}
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
{form.formState.errors.title && (
<FormMessage>
{form.formState.errors.title.message}
</FormMessage>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
defaultValue={"fergal.moran+opengifame@gmail.com"}
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} rows={3} />
</FormControl>
{form.formState.errors.description && (
<FormMessage>
{form.formState.errors.description.message}
</FormMessage>
)}
</FormItem>
)}
/>
<Controller
control={form.control}
name="image"
render={({ field: { value, onChange } }) => (
<ImageUpload value={value} onChange={onChange} />
)}
/>
<div className="divider pt-4">optional stuff</div>
<Controller
control={form.control}
name="tags"
render={({ field: { value, onChange } }) => (
<TaggedInput
label="Tags"
value={value}
onChange={onChange}
/>
)}
/>
</div>
<div className="w-full px-4 py-3 text-right">
<button type="submit" className="btn btn-primary w-full">
Upload Gif
</button>
</div>
</div>
</form>
</FormProvider>
</div>
</div>
);
};
export default UploadPage;

View File

@@ -3,6 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import { api } from "@/trpc/react"; import { api } from "@/trpc/react";
import ImageUpload from "./widgets/image-upload";
export function TrendingImages() { export function TrendingImages() {
const [latestPost] = api.post.getLatest.useSuspenseQuery(); const [latestPost] = api.post.getLatest.useSuspenseQuery();
@@ -23,21 +24,13 @@ export function TrendingImages() {
) : ( ) : (
<p>No images yet.</p> <p>No images yet.</p>
)} )}
<form
onSubmit={(e) => { <ImageUpload
e.preventDefault(); value="Farts"
createPost.mutate({ name }); onChange={(e) => {
console.log("trending-images", "uploading", e);
}} }}
className="flex flex-col gap-2 my-4" />
>
<button
type="submit"
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
disabled={createPost.isPending}
>
{createPost.isPending ? "Submitting..." : "Upload an image"}
</button>
</form>
</div> </div>
); );
} }

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

View File

@@ -1,61 +1,59 @@
// /* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
// import React from "react"; import React from "react";
// import { HiOutlineXMark } from "react-icons/hi2"; import { Icons } from "../icons";
// import { ImUpload2 } from "react-icons/im"; interface IImageUploadProps {
// interface IImageUploadProps { value: string | undefined;
// value: string | undefined; onChange: (image: File) => void;
// onChange: (image: File) => void; }
// } const ImageUpload: React.FC<IImageUploadProps> = ({ onChange }) => {
// const ImageUpload: React.FC<IImageUploadProps> = ({ onChange }) => { const [image, setImage] = React.useState<string>();
// const [image, setImage] = React.useState<string>(); const onImageChange = ($event: React.ChangeEvent<HTMLInputElement>) => {
// const onImageChange = ($event: React.ChangeEvent<HTMLInputElement>) => { console.log("ImageUpload", "onImageChange", $event);
// console.log("ImageUpload", "onImageChange", $event); if ($event.target.files?.[0]) {
// if ($event.target.files) { const url = URL.createObjectURL($event.target.files[0]);
// const url = URL.createObjectURL($event.target.files[0]); setImage(url);
// setImage(url); onChange($event.target.files[0]);
// onChange($event.target.files[0]); }
// } };
// }; return (
// return ( <div className="mt-1 flex justify-center rounded-md border-2 border-dashed border-secondary px-6 pb-6 pt-5">
// <div className="mt-1 flex justify-center rounded-md border-2 border-dashed border-secondary px-6 pb-6 pt-5"> {image ? (
// {image ? ( <div>
// <div> <div className="relative">
// <div className="relative"> <img src={image} alt="Preview" />
// <img src={image} alt="Preview" /> <button
// <button type="button"
// type="button" className="absolute right-0 top-0 m-2 rounded-full p-2"
// className="absolute right-0 top-0 m-2 rounded-full p-2" onClick={() => setImage("")}
// onClick={() => setImage("")} >
// > <Icons.close className="h-5 w-5" />
// {""} </button>
// <HiOutlineXMark className="h-5 w-5" /> </div>
// </button> </div>
// </div> ) : (
// </div> <div className="space-y-1 text-center">
// ) : ( <Icons.upload className="text-base-content/70 mx-auto mb-3 h-8 w-8" />
// <div className="space-y-1 text-center"> <div className="text-base-content flex text-sm">
// <ImUpload2 className="text-base-content/70 mx-auto mb-3 h-8 w-8" /> <label
// <div className="text-base-content flex text-sm"> htmlFor="gif-upload"
// <label className="badge badge-primary relative cursor-pointer rounded-md font-medium hover:text-accent"
// htmlFor="gif-upload" >
// className="badge badge-primary relative cursor-pointer rounded-md font-medium hover:text-accent" <span>Upload a file</span>
// > <input
// <span>Upload a file</span> accept="image/gif,video/mp4,video/mov,video/quicktime,video/webm,youtube,vimeo"
// <input id="gif-upload"
// accept="image/gif,video/mp4,video/mov,video/quicktime,video/webm,youtube,vimeo" type="file"
// id="gif-upload" className="sr-only"
// type="file" onChange={onImageChange}
// className="sr-only" />
// onChange={onImageChange} </label>
// /> <p className="pl-1">or drag and drop</p>
// </label> </div>
// <p className="pl-1">or drag and drop</p> <p className="text-xs">PNG, JPG, GIF up to 10MB</p>
// </div> </div>
// <p className="text-xs">PNG, JPG, GIF up to 10MB</p> )}
// </div> </div>
// )} );
// </div> };
// );
// };
// export default ImageUpload; export default ImageUpload;

View File

@@ -2,8 +2,7 @@
import React from "react"; import React from "react";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { RiLoginCircleLine } from "react-icons/ri"; import UserNavDropdown from "@/components/widgets/user-nav-dropdown";
import UserNavDropdown from "../user-nav-dropdown";
import { type Session } from "next-auth"; import { type Session } from "next-auth";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Icons } from "@/components/icons"; import { Icons } from "@/components/icons";

View File

@@ -1,143 +1,143 @@
// import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
// import React, { KeyboardEventHandler } from "react"; import React, { KeyboardEventHandler } from "react";
import { Input } from "@/components/ui/input";
// interface ITaggedInputProps { interface ITaggedInputProps {
// label: string; label: string;
// value: string[]; value: string[];
// onChange: (tags: string[]) => void; onChange: (tags: string[]) => void;
// } }
// const TaggedInput: React.FC<ITaggedInputProps> = ({ const TaggedInput: React.FC<ITaggedInputProps> = ({
// label, label,
// value, value,
// onChange, onChange,
// }) => { }) => {
// const [isSearching, setIsSearching] = React.useState(false); const [isSearching, setIsSearching] = React.useState(false);
// const [searchText, setSearchText] = React.useState<string>(""); const [searchText, setSearchText] = React.useState<string>("");
// const [searchResults, setSearchResults] = React.useState<Array<string>>([]); const [searchResults, setSearchResults] = React.useState<Array<string>>([]);
// const [tags, setTags] = React.useState<Array<string>>(value); const [tags, setTags] = React.useState<Array<string>>(value);
// React.useEffect(() => { React.useEffect(() => {
// logger.debug("TaggedInput", "callingOnChange", tags); logger.debug("TaggedInput", "callingOnChange", tags);
// onChange(tags); onChange(tags);
// }, [tags, onChange]); }, [tags, onChange]);
// const removeTag = (tag: string) => { const removeTag = (tag: string) => {
// setTags(tags.filter((obj) => obj !== tag)); setTags(tags.filter((obj) => obj !== tag));
// }; };
// const searchTags = async (query: string) => { const searchTags = async (query: string) => {
// if (!query) { if (!query) {
// setSearchResults([]); setSearchResults([]);
// return; return;
// } }
// setIsSearching(true); setIsSearching(true);
// const response = await fetch(`api/tags/search?q=${searchText}`); const response = await fetch(`api/tags/search?q=${searchText}`);
// if (response.status === 200) { if (response.status === 200) {
// const results = await response.json(); const results = await response.json();
// setSearchResults(results.map((r: { name: string }) => r.name)); setSearchResults(results.map((r: { name: string }) => r.name));
// } }
// setIsSearching(false); setIsSearching(false);
// }; };
// const handleChange = async ($event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = async ($event: React.ChangeEvent<HTMLInputElement>) => {
// const { value } = $event.target; const { value } = $event.target;
// setSearchText(value); setSearchText(value);
// await searchTags(value); await searchTags(value);
// }; };
// const handleKeyPress = ($event: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyPress = ($event: React.KeyboardEvent<HTMLInputElement>) => {
// if ($event.code === "Enter" || $event.code === "NumpadEnter") { if ($event.code === "Enter" || $event.code === "NumpadEnter") {
// __addTag(searchText); __addTag(searchText);
// } }
// }; };
// const __addTag = (tag: string) => { const __addTag = (tag: string) => {
// setTags([...tags, tag]); setTags([...tags, tag]);
// setSearchResults([]); setSearchResults([]);
// setIsSearching(false); setIsSearching(false);
// setSearchText(""); setSearchText("");
// }; };
// const doResultClick = ($event: any) => const doResultClick = ($event: any) =>
// __addTag($event.target.textContent as string); __addTag($event.target.textContent as string);
// return ( return (
// <> <>
// <label htmlFor="{name}" className="block text-sm font-medium"> <label htmlFor="{name}" className="block text-sm font-medium">
// {label} {label}
// </label> </label>
// <div className="border-accent flex w-full rounded-lg border align-middle text-sm shadow-sm"> <div className="flex w-full rounded-lg align-middle text-sm shadow-sm">
// <div className="flex flex-row space-x-1 pl-2 pt-3"> <div className="flex flex-row space-x-1">
// {tags && {tags &&
// tags.map((tag) => ( tags.map((tag) => (
// <span key={tag} className="badge badge-primary badge-lg py-0.5"> <span key={tag} className="badge badge-primary badge-lg py-0.5">
// {tag} {tag}
// <button <button
// onClick={() => removeTag(tag)} onClick={() => removeTag(tag)}
// type="button" type="button"
// className="ml-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full" className="ml-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full"
// > >
// <span className="sr-only">{tag}</span> <span className="sr-only">{tag}</span>
// <svg <svg
// className="h-2 w-2" className="h-2 w-2"
// stroke="currentColor" stroke="currentColor"
// fill="none" fill="none"
// viewBox="0 0 8 8" viewBox="0 0 8 8"
// > >
// <path <path
// strokeLinecap="round" strokeLinecap="round"
// strokeWidth={1.5} strokeWidth={1.5}
// d="M1 1l6 6m0-6L1 7" d="M1 1l6 6m0-6L1 7"
// /> />
// </svg> </svg>
// </button> </button>
// </span> </span>
// ))} ))}
// </div> </div>
// <input <Input
// value={searchText} value={searchText}
// onKeyDown={handleKeyPress} onKeyDown={handleKeyPress}
// onChange={handleChange} onChange={handleChange}
// placeholder="Start typing and press enter" placeholder="Start typing and press enter"
// className="input w-full focus:outline-none" />
// /> </div>
// </div> {isSearching && (
// {isSearching && ( <div role="status" className="z-50 -mt-3 ml-5">
// <div role="status" className="z-50 -mt-3 ml-5"> <svg
// <svg aria-hidden="true"
// aria-hidden="true" className="mr-2 h-4 w-4"
// className="mr-2 h-4 w-4" viewBox="0 0 100 101"
// viewBox="0 0 100 101" fill="none"
// fill="none" xmlns="http://www.w3.org/2000/svg"
// xmlns="http://www.w3.org/2000/svg" >
// > <path
// <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
// d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"
// fill="currentColor" />
// /> <path
// <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
// d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"
// fill="currentFill" />
// /> </svg>
// </svg> <span className="sr-only">Loading...</span>
// <span className="sr-only">Loading...</span> </div>
// </div> )}
// )} {searchResults && searchResults.length !== 0 && (
// {searchResults && searchResults.length !== 0 && ( <div className={`z-50 mb-4 flex space-y-0`}>
// <div className={`z-50 mb-4 flex space-y-0`}> <aside
// <aside aria-labelledby="menu-heading"
// aria-labelledby="menu-heading" className="absolute z-50 -mt-5 flex w-64 flex-col items-start rounded-md border bg-white text-sm shadow-md"
// className="absolute z-50 -mt-5 flex w-64 flex-col items-start rounded-md border bg-white text-sm shadow-md" >
// > <ul className="flex w-full flex-col">
// <ul className="flex w-full flex-col"> {searchResults.map((result) => (
// {searchResults.map((result) => ( <li
// <li key={result}
// key={result} onClick={doResultClick}
// onClick={doResultClick} className="cursor-pointer space-x-2 px-2 py-1 hover:bg-indigo-500 hover:text-white focus:bg-indigo-500 focus:text-white focus:outline-none"
// className="cursor-pointer space-x-2 px-2 py-1 hover:bg-indigo-500 hover:text-white focus:bg-indigo-500 focus:text-white focus:outline-none" >
// > {result}
// {result} </li>
// </li> ))}
// ))} </ul>
// </ul> </aside>
// </aside> </div>
// </div> )}
// )} </>
// </> );
// ); };
// };
// export default TaggedInput; export default TaggedInput;

View File

@@ -1,5 +1,7 @@
"use client"; "use client";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import Image from "next/image";
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
import { signOut } from "next-auth/react"; import { signOut } from "next-auth/react";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
@@ -9,10 +11,11 @@ interface IUserNavDropdownProps {
session: Session | null; session: Session | null;
} }
const UserNavDropdown: React.FC<IUserNavDropdownProps> = ({ session }) => { const UserNavDropdown: React.FC<IUserNavDropdownProps> = ({ session }) => {
const [profileImage, setProfileImage] = React.useState<string>(); const [profileImage, setProfileImage] = React.useState<string>(
session?.user?.image || "/images/default-profile.jpg",
);
React.useEffect(() => { React.useEffect(() => {
logger.debug("UserNavDropdown", "session", session); logger.debug("UserNavDropdown", "session", session);
setProfileImage(session?.user?.image || "/images/default-profile.jpg"); setProfileImage(session?.user?.image || "/images/default-profile.jpg");
}, [session]); }, [session]);
@@ -22,11 +25,15 @@ const UserNavDropdown: React.FC<IUserNavDropdownProps> = ({ session }) => {
<div> <div>
<Menu.Button className="flex rounded-full bg-gray-800 text-sm text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"> <Menu.Button className="flex rounded-full bg-gray-800 text-sm text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
<span className="sr-only">Open user menu</span> <span className="sr-only">Open user menu</span>
<img {profileImage && (
<Image
width={32}
height={32}
className="h-8 w-8 rounded-full" className="h-8 w-8 rounded-full"
src={profileImage} src={profileImage}
alt="Profile image" alt="Profile image"
/> />
)}
</Menu.Button> </Menu.Button>
</div> </div>
<Transition <Transition

View File

@@ -72,5 +72,7 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
function getBaseUrl() { function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin; if (typeof window !== "undefined") return window.location.origin;
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
if (process.env.NEXT_PUBLIC_SITE_URL)
return `https://${process.env.NEXT_PUBLIC_SITE_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`; return `http://localhost:${process.env.PORT ?? 3000}`;
} }

View File

@@ -1,84 +1,80 @@
import { type Config } from "tailwindcss"; import { type Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme";
export default { export default {
darkMode: ["class"], darkMode: ["class"],
content: ["./src/**/*.tsx"], content: ["./src/**/*.tsx"],
theme: { theme: {
extend: { extend: {
fontFamily: {
sans: ["var(--font-geist-sans)", ...fontFamily.sans]
},
borderRadius: { borderRadius: {
lg: 'var(--radius)', lg: "var(--radius)",
md: 'calc(var(--radius) - 2px)', md: "calc(var(--radius) - 2px)",
sm: 'calc(var(--radius) - 4px)' sm: "calc(var(--radius) - 4px)",
}, },
colors: { colors: {
background: 'hsl(var(--background))', background: "hsl(var(--background))",
foreground: 'hsl(var(--foreground))', foreground: "hsl(var(--foreground))",
card: { card: {
DEFAULT: 'hsl(var(--card))', DEFAULT: "hsl(var(--card))",
foreground: 'hsl(var(--card-foreground))' foreground: "hsl(var(--card-foreground))",
}, },
popover: { popover: {
DEFAULT: 'hsl(var(--popover))', DEFAULT: "hsl(var(--popover))",
foreground: 'hsl(var(--popover-foreground))' foreground: "hsl(var(--popover-foreground))",
}, },
primary: { primary: {
DEFAULT: 'hsl(var(--primary))', DEFAULT: "hsl(var(--primary))",
foreground: 'hsl(var(--primary-foreground))' foreground: "hsl(var(--primary-foreground))",
}, },
secondary: { secondary: {
DEFAULT: 'hsl(var(--secondary))', DEFAULT: "hsl(var(--secondary))",
foreground: 'hsl(var(--secondary-foreground))' foreground: "hsl(var(--secondary-foreground))",
}, },
muted: { muted: {
DEFAULT: 'hsl(var(--muted))', DEFAULT: "hsl(var(--muted))",
foreground: 'hsl(var(--muted-foreground))' foreground: "hsl(var(--muted-foreground))",
}, },
accent: { accent: {
DEFAULT: 'hsl(var(--accent))', DEFAULT: "hsl(var(--accent))",
foreground: 'hsl(var(--accent-foreground))' foreground: "hsl(var(--accent-foreground))",
}, },
destructive: { destructive: {
DEFAULT: 'hsl(var(--destructive))', DEFAULT: "hsl(var(--destructive))",
foreground: 'hsl(var(--destructive-foreground))' foreground: "hsl(var(--destructive-foreground))",
}, },
border: 'hsl(var(--border))', border: "hsl(var(--border))",
input: 'hsl(var(--input))', input: "hsl(var(--input))",
ring: 'hsl(var(--ring))', ring: "hsl(var(--ring))",
chart: { chart: {
'1': 'hsl(var(--chart-1))', "1": "hsl(var(--chart-1))",
'2': 'hsl(var(--chart-2))', "2": "hsl(var(--chart-2))",
'3': 'hsl(var(--chart-3))', "3": "hsl(var(--chart-3))",
'4': 'hsl(var(--chart-4))', "4": "hsl(var(--chart-4))",
'5': 'hsl(var(--chart-5))' "5": "hsl(var(--chart-5))",
} },
}, },
keyframes: { keyframes: {
'accordion-down': { "accordion-down": {
from: { from: {
height: '0' height: "0",
}, },
to: { to: {
height: 'var(--radix-accordion-content-height)' height: "var(--radix-accordion-content-height)",
}
}, },
'accordion-up': { },
"accordion-up": {
from: { from: {
height: 'var(--radix-accordion-content-height)' height: "var(--radix-accordion-content-height)",
}, },
to: { to: {
height: '0' height: "0",
} },
} },
}, },
animation: { animation: {
'accordion-down': 'accordion-down 0.2s ease-out', "accordion-down": "accordion-down 0.2s ease-out",
'accordion-up': 'accordion-up 0.2s ease-out' "accordion-up": "accordion-up 0.2s ease-out",
} },
} },
}, },
plugins: [require("tailwindcss-animate")], plugins: [require("tailwindcss-animate")],
} satisfies Config; } satisfies Config;