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

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -8,23 +8,20 @@ interface AuthLayoutProps {
export default function AuthLayout({ children }: AuthLayoutProps) {
return (
<div className="min-h-screen">
<div className="container flex h-screen w-screen flex-col items-center justify-center">
<Link
href="/"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute left-4 top-4 md:left-8 md:top-8",
)}
>
<>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
Back
</>
</Link>
{children}
</div>
<div className="container flex h-screen w-screen flex-col items-center ">
<Link
href="/"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute left-4 top-4 mt-8 md:left-8 md:top-8",
)}
>
<>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
Back
</>
</Link>
<div className="mt-8">{children}</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 { 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 { siteConfig } from "@/config/site.config";
import { getServerSession } from "next-auth";
import { SessionProvider } from "next-auth/react";
export const viewport: Viewport = {
themeColor: [
@@ -21,8 +22,8 @@ export const viewport: Viewport = {
],
};
const f = font({
weight: "400",
subsets: ["latin"],
weight: ["400"],
});
export const metadata: Metadata = {

View File

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

View File

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

View File

@@ -5,12 +5,12 @@ import React from "react";
import { type NavItem } from "@/types";
import { type Session } from "next-auth";
import { Icons } from "../icons";
import { Icons } from "@/components/icons";
import { siteConfig } from "@/config/site.config";
import { cn } from "@/lib/utils";
import { useSelectedLayoutSegment } from "next/navigation";
import LoginButton from "../widgets/login/login-button";
import LoginButton from "@/components/widgets/login/login-button";
type TopNavbarProps = {
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 { api } from "@/trpc/react";
import ImageUpload from "./widgets/image-upload";
export function TrendingImages() {
const [latestPost] = api.post.getLatest.useSuspenseQuery();
@@ -23,21 +24,13 @@ export function TrendingImages() {
) : (
<p>No images yet.</p>
)}
<form
onSubmit={(e) => {
e.preventDefault();
createPost.mutate({ name });
<ImageUpload
value="Farts"
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>
);
}

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

View File

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

View File

@@ -1,143 +1,143 @@
// import { logger } from "@/lib/logger";
// import React, { KeyboardEventHandler } from "react";
import { logger } from "@/lib/logger";
import React, { KeyboardEventHandler } from "react";
import { Input } from "@/components/ui/input";
// interface ITaggedInputProps {
// label: string;
// value: string[];
// onChange: (tags: string[]) => void;
// }
// const TaggedInput: React.FC<ITaggedInputProps> = ({
// label,
// value,
// onChange,
// }) => {
// const [isSearching, setIsSearching] = React.useState(false);
// const [searchText, setSearchText] = React.useState<string>("");
// const [searchResults, setSearchResults] = React.useState<Array<string>>([]);
// const [tags, setTags] = React.useState<Array<string>>(value);
interface ITaggedInputProps {
label: string;
value: string[];
onChange: (tags: string[]) => void;
}
const TaggedInput: React.FC<ITaggedInputProps> = ({
label,
value,
onChange,
}) => {
const [isSearching, setIsSearching] = React.useState(false);
const [searchText, setSearchText] = React.useState<string>("");
const [searchResults, setSearchResults] = React.useState<Array<string>>([]);
const [tags, setTags] = React.useState<Array<string>>(value);
// React.useEffect(() => {
// logger.debug("TaggedInput", "callingOnChange", tags);
// onChange(tags);
// }, [tags, onChange]);
// const removeTag = (tag: string) => {
// setTags(tags.filter((obj) => obj !== tag));
// };
// const searchTags = async (query: string) => {
// if (!query) {
// setSearchResults([]);
// return;
// }
// setIsSearching(true);
// const response = await fetch(`api/tags/search?q=${searchText}`);
// if (response.status === 200) {
// const results = await response.json();
// setSearchResults(results.map((r: { name: string }) => r.name));
// }
// setIsSearching(false);
// };
// const handleChange = async ($event: React.ChangeEvent<HTMLInputElement>) => {
// const { value } = $event.target;
// setSearchText(value);
// await searchTags(value);
// };
// const handleKeyPress = ($event: React.KeyboardEvent<HTMLInputElement>) => {
// if ($event.code === "Enter" || $event.code === "NumpadEnter") {
// __addTag(searchText);
// }
// };
// const __addTag = (tag: string) => {
// setTags([...tags, tag]);
// setSearchResults([]);
// setIsSearching(false);
// setSearchText("");
// };
// const doResultClick = ($event: any) =>
// __addTag($event.target.textContent as string);
// return (
// <>
// <label htmlFor="{name}" className="block text-sm font-medium">
// {label}
// </label>
// <div className="border-accent flex w-full rounded-lg border align-middle text-sm shadow-sm">
// <div className="flex flex-row space-x-1 pl-2 pt-3">
// {tags &&
// tags.map((tag) => (
// <span key={tag} className="badge badge-primary badge-lg py-0.5">
// {tag}
// <button
// onClick={() => removeTag(tag)}
// type="button"
// 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>
// <svg
// className="h-2 w-2"
// stroke="currentColor"
// fill="none"
// viewBox="0 0 8 8"
// >
// <path
// strokeLinecap="round"
// strokeWidth={1.5}
// d="M1 1l6 6m0-6L1 7"
// />
// </svg>
// </button>
// </span>
// ))}
// </div>
// <input
// value={searchText}
// onKeyDown={handleKeyPress}
// onChange={handleChange}
// placeholder="Start typing and press enter"
// className="input w-full focus:outline-none"
// />
// </div>
// {isSearching && (
// <div role="status" className="z-50 -mt-3 ml-5">
// <svg
// aria-hidden="true"
// className="mr-2 h-4 w-4"
// viewBox="0 0 100 101"
// fill="none"
// xmlns="http://www.w3.org/2000/svg"
// >
// <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"
// fill="currentColor"
// />
// <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"
// fill="currentFill"
// />
// </svg>
// <span className="sr-only">Loading...</span>
// </div>
// )}
// {searchResults && searchResults.length !== 0 && (
// <div className={`z-50 mb-4 flex space-y-0`}>
// <aside
// 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"
// >
// <ul className="flex w-full flex-col">
// {searchResults.map((result) => (
// <li
// key={result}
// 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"
// >
// {result}
// </li>
// ))}
// </ul>
// </aside>
// </div>
// )}
// </>
// );
// };
React.useEffect(() => {
logger.debug("TaggedInput", "callingOnChange", tags);
onChange(tags);
}, [tags, onChange]);
const removeTag = (tag: string) => {
setTags(tags.filter((obj) => obj !== tag));
};
const searchTags = async (query: string) => {
if (!query) {
setSearchResults([]);
return;
}
setIsSearching(true);
const response = await fetch(`api/tags/search?q=${searchText}`);
if (response.status === 200) {
const results = await response.json();
setSearchResults(results.map((r: { name: string }) => r.name));
}
setIsSearching(false);
};
const handleChange = async ($event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = $event.target;
setSearchText(value);
await searchTags(value);
};
const handleKeyPress = ($event: React.KeyboardEvent<HTMLInputElement>) => {
if ($event.code === "Enter" || $event.code === "NumpadEnter") {
__addTag(searchText);
}
};
const __addTag = (tag: string) => {
setTags([...tags, tag]);
setSearchResults([]);
setIsSearching(false);
setSearchText("");
};
const doResultClick = ($event: any) =>
__addTag($event.target.textContent as string);
return (
<>
<label htmlFor="{name}" className="block text-sm font-medium">
{label}
</label>
<div className="flex w-full rounded-lg align-middle text-sm shadow-sm">
<div className="flex flex-row space-x-1">
{tags &&
tags.map((tag) => (
<span key={tag} className="badge badge-primary badge-lg py-0.5">
{tag}
<button
onClick={() => removeTag(tag)}
type="button"
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>
<svg
className="h-2 w-2"
stroke="currentColor"
fill="none"
viewBox="0 0 8 8"
>
<path
strokeLinecap="round"
strokeWidth={1.5}
d="M1 1l6 6m0-6L1 7"
/>
</svg>
</button>
</span>
))}
</div>
<Input
value={searchText}
onKeyDown={handleKeyPress}
onChange={handleChange}
placeholder="Start typing and press enter"
/>
</div>
{isSearching && (
<div role="status" className="z-50 -mt-3 ml-5">
<svg
aria-hidden="true"
className="mr-2 h-4 w-4"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<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"
fill="currentColor"
/>
<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"
fill="currentFill"
/>
</svg>
<span className="sr-only">Loading...</span>
</div>
)}
{searchResults && searchResults.length !== 0 && (
<div className={`z-50 mb-4 flex space-y-0`}>
<aside
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"
>
<ul className="flex w-full flex-col">
{searchResults.map((result) => (
<li
key={result}
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"
>
{result}
</li>
))}
</ul>
</aside>
</div>
)}
</>
);
};
// export default TaggedInput;
export default TaggedInput;

View File

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

View File

@@ -72,5 +72,7 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
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}`;
}

View File

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