This commit is contained in:
Fergal Moran
2024-09-04 15:53:51 +01:00
parent 467c53078c
commit 3c0fbf638c
82 changed files with 1107 additions and 5204 deletions

5
.env
View File

@@ -1,5 +1,3 @@
<<<<<<< Updated upstream
=======
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.
@@ -10,10 +8,9 @@ DATABASE_URL="postgresql://postgres:hackme@localhost:5432/opengifame"
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_SECRET="tAOVgxpY1U0BsnPCr6Gf8WVkmRMkp06ztUfwMhBKMQ4="
NEXTAUTH_URL="http://localhost:3000"
# Next Auth Discord Provider
DISCORD_CLIENT_ID=""
DISCORD_CLIENT_SECRET=""
>>>>>>> Stashed changes

View File

@@ -1,3 +0,0 @@
{
"workbench.colorTheme": "SynthWave '84"
}

View File

@@ -1,140 +0,0 @@
'use client';
import React, { FormEventHandler } from 'react';
import { SocialLogin } from '@components';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { signIn } from 'next-auth/react';
const SignInPage = () => {
const router = useRouter();
const [userInfo, setUserInfo] = React.useState({
email: '',
password: '',
});
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
//TODO: validation
e.preventDefault();
const res = await signIn('credentials', {
email: userInfo.email,
password: userInfo.password,
redirect: false,
});
if (res?.status === 200) {
router.replace('/');
}
console.log('signin', 'handleSubmit', res);
};
return (
<div className="flex flex-col justify-center min-h-full py-1 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-2 text-3xl font-extrabold text-center ">
Sign in to your account
</h2>
<p className="mt-2 text-sm text-center ">
Or{' '}
<Link
href="/auth/signup"
className="font-medium "
>
create a new account
</Link>
</p>
</div>
<div className="mt-2 sm:mx-auto sm:w-full sm:max-w-md">
<div className="px-4 py-8 shadow sm:rounded-lg sm:px-10">
<form
className="space-y-6"
onSubmit={handleSubmit}
method="post"
>
<div>
<label
htmlFor="email"
className="block text-sm font-medium "
>
Email address
</label>
<div className="mt-1">
<input
id="email"
type="email"
autoComplete="email"
required
value={userInfo.email}
onChange={({ target }) =>
setUserInfo({ ...userInfo, email: target.value })
}
className="w-full input input-bordered"
/>
</div>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium "
>
Password
</label>
<div className="mt-1">
<input
id="password"
type="password"
autoComplete="current-password"
required
value={userInfo.password}
onChange={({ target }) =>
setUserInfo({ ...userInfo, password: target.value })
}
className="w-full input input-bordered"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="w-4 h-4"
/>
<label
htmlFor="remember-me"
className="block ml-2 text-sm text-accent"
>
Remember me
</label>
</div>
<div className="text-sm">
<a
href="#"
className="font-medium text-info hover:text-primary/50"
>
Forgot your password?
</a>
</div>
</div>
<div>
<button
type="submit"
className="w-full btn btn-primary"
>
Sign in
</button>
</div>
</form>
<div className="mt-6">
<SocialLogin />
</div>
</div>
</div>
</div>
);
};
export default SignInPage;

View File

@@ -1,141 +0,0 @@
'use client';
import React, { FormEventHandler } from 'react';
import { useRouter } from 'next/router';
import { logger } from '@lib/logger';
import { SocialLogin } from '@components';
const SignUpPage = () => {
const router = useRouter();
const [userInfo, setUserInfo] = React.useState({
email: '',
password: '',
repeatPassword: '',
});
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
//TODO: validation
e.preventDefault();
try {
const res = await fetch(`/api/user/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: userInfo.email,
password: userInfo.password,
}),
});
logger.debug(`res`, res);
router.push(
`signin${
router.query.callbackUrl
? `?callbackUrl=${router.query.callbackUrl}`
: ''
}`
);
} catch (error) {
console.error(error);
}
};
return (
<div className="flex flex-col justify-center min-h-full py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-2 text-3xl font-extrabold text-center ">
Create new account
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="px-4 py-8 shadow sm:rounded-lg sm:px-10">
<form
className="space-y-6"
onSubmit={handleSubmit}
method="post"
>
<div>
<label
htmlFor="email"
className="block text-sm font-medium "
>
{' '}
Email address{' '}
</label>
<div className="mt-1">
<input
id="email"
type="email"
autoComplete="email"
required
value={userInfo.email}
onChange={({ target }) =>
setUserInfo({ ...userInfo, email: target.value })
}
className="w-full input input-bordered"
/>
</div>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium"
>
{' '}
Password{' '}
</label>
<div className="mt-1">
<input
id="password"
type="password"
autoComplete="current-password"
required
value={userInfo.password}
onChange={({ target }) =>
setUserInfo({ ...userInfo, password: target.value })
}
className="w-full input input-bordered"
/>
</div>
</div>
<div>
<label
htmlFor="repeat-password"
className="block text-sm font-medium"
>
Repeat password
</label>
<div className="mt-1">
<input
id="repeat-password"
type="password"
autoComplete="current-password"
required
value={userInfo.repeatPassword}
onChange={({ target }) =>
setUserInfo({ ...userInfo, repeatPassword: target.value })
}
className="w-full input input-bordered"
/>
</div>
</div>
<div>
<button
type="submit"
className="w-full btn btn-primary"
>
Create account
</button>
</div>
</form>
<div className="mt-6">
<SocialLogin />
</div>
</div>
</div>
</div>
);
};
export default SignUpPage;

View File

@@ -1,60 +0,0 @@
import React from 'react';
import {Gif} from '@models';
import {GifContainer, SharingComponent} from '@components';
import client from '@lib/prismadb';
import {mapGif} from '@lib/mapping/gif';
import {notFound, useRouter} from 'next/navigation';
interface IGifPageProps {
params: {
slug: string;
};
}
const getGif = async (slug: string): Promise<Gif> => {
const res = await fetch(`${process.env.API_URL}/api/gifs/${slug}`);
if (res.status === 200) {
return await res.json();
}
notFound();
};
const GifPage = async ({params}: IGifPageProps) => {
const {slug} = params;
const gif = await getGif(slug as string);
return (
<div className="relative overflow-hidden">
<div className="relative pb-16 sm:pb-24 lg:pb-32">
<main className="px-4 mx-auto max-w-7xl sm:px-6">
<div className="lg:grid lg:grid-cols-12 lg:gap-8">
<div className="sm:text-center md:max-w-2xl lg:col-span-6 lg:text-left">
<h1>
<span className="block mt-1 text-4xl font-extrabold tracking-tight sm:text-5xl xl:text-6xl">
<span className="block text-accent">{gif.title}</span>
</span>
</h1>
<p className="mt-3 text-base sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
{gif.description}
</p>
<SharingComponent gif={gif}/>
{/* <div className="mt-5">
<AddCommentComponent />
</div> */}
</div>
<div
className="relative mt-12 sm:max-w-lg sm:mx-auto lg:mt-0 lg:max-w-none lg:mx-0 lg:col-span-6 lg:flex lg:items-center">
<div className="relative w-full mx-auto rounded-lg shadow-lg lg:max-w-md">
<span className="sr-only">The gif</span>
<GifContainer
gif={gif}
isLink={false}
/>
</div>
</div>
</div>
</main>
</div>
</div>
);
};
export default GifPage;

View File

@@ -1,54 +0,0 @@
'use client';
import Navbar from '@components/layout/Navbar';
import { SessionProvider } from 'next-auth/react';
import '@styles/globals.css';
export default function SiteLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<head>
<title>Open Gifame</title>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link
rel="manifest"
href="/site.webmanifest"
/>
<link
rel="mask-icon"
href="/safari-pinned-tab.svg"
color="#5bbad5"
/>
<meta
name="msapplication-TileColor"
content="#da532c"
/>
</head>
<body>
<SessionProvider>
<Navbar />
<div className="m-4">{children}</div>
</SessionProvider>
</body>
</html>
);
}

View File

@@ -1,31 +0,0 @@
import React from 'react';
import { Gif } from '@models';
import { GifContainer } from '@components';
import { logger } from '@lib/logger';
async function getGifs() {
const gifs = await fetch(`${process.env.API_URL}/api/gifs`, {
cache: 'no-store',
});
return await gifs.json();
}
const HomePage = async () => {
const gifs = await getGifs();
return (
<div className="grid grid-cols-1 xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2">
{gifs.map((gif: Gif) => {
return (
<div
key={gif.id}
className="m-2"
>
<GifContainer gif={gif} />
</div>
);
})}
</div>
);
};
export default HomePage;

View File

@@ -1,145 +0,0 @@
'use client';
import React from 'react';
import { useForm } from 'react-hook-form';
import { SubmitHandler, Controller } from 'react-hook-form';
import { useRouter } from 'next/navigation';
import { ImageUpload, TaggedInput } from '@components';
type FormValues = {
title: string;
description: string;
terms: string[];
image: string | undefined;
};
const UploadPage = () => {
const router = useRouter();
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
defaultValues: {
title: 'argle',
description: 'Argle bargle Foo Ferra',
terms: ['Niles', 'Frasier'],
image: undefined,
},
});
const onSubmit: 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('terms', data.terms.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="my-3 text-sm text-base-content/70">
The more info you can give us the better.
</p>
</div>
</div>
<div className="md:col-span-2">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="shadow sm:rounded-md sm:overflow-hidden">
<div className="px-4 space-y-4">
<div className="col-span-3 sm:col-span-2">
<label
htmlFor="title"
className="block text-sm font-medium"
>
Title
</label>
<div className="flex mt-1 rounded-md shadow-sm">
<input
{...register('title', { required: 'Title is required' })}
type="text"
name="title"
className="flex-1 block w-full rounded-md input input-bordered rounded-r-md sm:text-sm"
placeholder="Title for this gif"
/>
</div>
<p className="mt-2 text-sm text-red-600">
{errors.title?.message}
</p>
</div>
<div>
<label
htmlFor="description"
className="block text-sm font-medium"
>
Description
</label>
<div className="mt-1">
<textarea
{...register('description', {
required: 'Description is required',
})}
name="description"
rows={3}
className="block w-full mt-1 border rounded-md shadow-sm textarea textarea-bordered sm:text-sm"
placeholder="Description for this gif"
/>
</div>
<p className="mt-2 text-sm text-red-600">
{errors.description?.message}
</p>
</div>
<div>
<label className="block text-sm font-medium">The gif</label>
<Controller
control={control}
name="image"
render={({ field: { value, onChange } }) => (
<ImageUpload
value={value}
onChange={onChange}
/>
)}
/>
</div>
<div className="pt-4 divider">optional stuff</div>
<Controller
control={control}
name="terms"
render={({ field: { value, onChange } }) => (
<TaggedInput
label="Search terms"
value={value}
onChange={onChange}
/>
)}
/>
</div>
<div className="w-full px-4 py-3 text-right ">
<button
type="submit"
className="w-full btn btn-primary"
>
Upload Gif
</button>
</div>
</div>
</form>
</div>
</div>
);
};
export default UploadPage;

View File

@@ -1,33 +0,0 @@
import React from 'react';
import {GifContainer} from '@components';
import {Gif} from "@models";
import {notFound} from "@node_modules/next/navigation";
interface IShareGifPageProps {
params: {
slug: string;
};
}
const getGif = async (slug: string): Promise<Gif> => {
const res = await fetch(`${process.env.API_URL}/api/gifs/${slug}`);
if (res.status === 200) {
return await res.json();
}
notFound();
};
const ShareGifPage = async ({params}: IShareGifPageProps) => {
const {slug} = params;
const gif = await getGif(slug as string);
return (
<div className="p-2 w-96">
<GifContainer
gif={gif}
isLink={true}
showDetails={false}
/>
</div>
);
};
export default ShareGifPage;

View File

@@ -1,7 +0,0 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,216 +0,0 @@
'use client';
import React, { Fragment } from 'react';
import {
HiFire,
HiHeart,
HiHandThumbUp,
HiXCircle,
HiPaperClip,
} from 'react-icons/hi2';
import {
HiEmojiHappy,
HiOutlineEmojiHappy,
HiOutlineEmojiSad,
} from 'react-icons/hi';
import { Listbox, Transition } from '@headlessui/react';
import { useSession } from 'next-auth/react';
const moods = [
{
name: 'Excited',
value: 'excited',
icon: HiFire,
iconColor: 'text-white',
bgColor: 'bg-red-500',
},
{
name: 'Loved',
value: 'loved',
icon: HiHeart,
iconColor: 'text-white',
bgColor: 'bg-pink-400',
},
{
name: 'Happy',
value: 'happy',
icon: HiOutlineEmojiHappy,
iconColor: 'text-white',
bgColor: 'bg-green-400',
},
{
name: 'Sad',
value: 'sad',
icon: HiOutlineEmojiSad,
iconColor: 'text-white',
bgColor: 'bg-yellow-400',
},
{
name: 'Thumbsy',
value: 'thumbsy',
icon: HiHandThumbUp,
iconColor: 'text-white',
bgColor: 'bg-blue-500',
},
{
name: 'I feel nothing',
value: null,
icon: HiXCircle,
iconColor: 'text-gray-400',
bgColor: 'bg-transparent',
},
];
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ');
}
const AddCommentComponent = () => {
const { data: session } = useSession();
const [selected, setSelected] = React.useState(moods[5]);
return session ? (
<div className="flex items-start space-x-4">
<div className="flex-shrink-0">
<img
className="inline-block w-10 h-10 rounded-full"
src={session?.user?.image as string}
alt="User"
/>
</div>
<div className="flex-1 min-w-0">
<form action="#">
<div className="border-b border-gray-200 focus-within:border-indigo-600">
<label
htmlFor="comment"
className="sr-only"
>
Add your comment
</label>
<textarea
rows={3}
name="comment"
id="comment"
className="block w-full p-0 pb-2 border-0 border-b border-transparent resize-none focus:ring-0 focus:border-indigo-600 sm:text-sm"
placeholder="Add your comment..."
defaultValue={''}
/>
</div>
<div className="flex justify-between pt-2">
<div className="flex items-center space-x-5">
<div className="flow-root">
<button
type="button"
className="inline-flex items-center justify-center w-10 h-10 -m-2 text-gray-400 rounded-full hover:text-gray-500"
>
<HiPaperClip
className="w-6 h-6"
aria-hidden="true"
/>
<span className="sr-only">Attach a file</span>
</button>
</div>
<div className="flow-root">
<Listbox
value={selected}
onChange={setSelected}
>
{({ open }) => (
<>
<Listbox.Label className="sr-only">
Your mood
</Listbox.Label>
<div className="relative">
<Listbox.Button className="relative inline-flex items-center justify-center w-10 h-10 -m-2 text-gray-400 rounded-full hover:text-gray-500">
<span className="flex items-center justify-center">
{selected.value === null ? (
<span>
<HiOutlineEmojiHappy
className="flex-shrink-0 w-6 h-6"
aria-hidden="true"
/>
<span className="sr-only">Add your mood</span>
</span>
) : (
<span>
<div
className={classNames(
selected.bgColor,
'w-8 h-8 rounded-full flex items-center justify-center'
)}
>
<selected.icon
className="flex-shrink-0 w-5 h-5 text-white"
aria-hidden="true"
/>
</div>
<span className="sr-only">{selected.name}</span>
</span>
)}
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 py-3 -ml-6 text-base bg-white rounded-lg shadow w-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:ml-auto sm:w-64 sm:text-sm">
{moods.map((mood) => (
<Listbox.Option
key={mood.value}
className={({ active }) =>
classNames(
active ? 'bg-gray-100' : 'bg-white',
'cursor-default select-none relative py-2 px-3'
)
}
value={mood}
>
<div className="flex items-center">
<div
className={classNames(
mood.bgColor,
'w-8 h-8 rounded-full flex items-center justify-center'
)}
>
<mood.icon
className={classNames(
mood.iconColor,
'flex-shrink-0 h-5 w-5'
)}
aria-hidden="true"
/>
</div>
<span className="block ml-3 font-medium truncate">
{mood.name}
</span>
</div>
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
</div>
</div>
<div className="flex-shrink-0">
<button
type="submit"
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Post
</button>
</div>
</div>
</form>
</div>
</div>
) : (
<div></div>
);
};
export default AddCommentComponent;

View File

@@ -1,23 +0,0 @@
import GifContainer from './widgets/GifContainer';
import LoginButton from './widgets/login/LoginButton';
import SocialLogin from './widgets/login/SocialLogin';
import TaggedInput from './widgets/TaggedInput';
import ImageUpload from './widgets/ImageUpload';
import SharingComponent from './sharing/SharingComponent';
import AddCommentComponent from './comments/AddCommentComponent';
import UserNavDropdown from './widgets/UserNavDropdown';
import CopyTextInput from './widgets/CopyTextInput';
import Loading from './widgets/Loading';
export {
GifContainer,
LoginButton,
SocialLogin,
TaggedInput,
ImageUpload,
SharingComponent,
AddCommentComponent,
UserNavDropdown,
CopyTextInput,
Loading,
};

View File

@@ -1,207 +0,0 @@
'use client';
/* eslint-disable @next/next/no-img-element */
import React from 'react';
import Image from "next/image";
import { useSession } from 'next-auth/react';
import { Disclosure } from '@headlessui/react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import LoginButton from '@components/widgets/login/LoginButton';
import { FaBars, FaSearch, FaTimes } from 'react-icons/fa';
const Navbar = () => {
const { data: session } = useSession();
const router = useRouter();
const pathname = usePathname();
return (
<Disclosure
as="nav"
className="bg-base-300"
>
{({ open }) => (
<>
<div className="px-2 mx-auto max-w-7xl sm:px-4 lg:px-8">
<div className="relative flex items-center justify-between h-16">
<div className="flex items-center px-2 lg:px-0">
<div className="flex-shrink-0">
<div className="block w-auto h-8 lg:hidden">
<Link href="/">
<Image
width={32}
height={32}
src="/img/icon.svg"
alt="Workflow"
style={{
maxWidth: "100%",
height: "auto"
}} />
</Link>
</div>
<div className="hidden w-auto h-8 lg:block">
<Link href="/">
<Image
width={32}
height={32}
src="/img/icon.svg"
alt="Workflow"
style={{
maxWidth: "100%",
height: "auto"
}} />
</Link>
</div>
</div>
<div className="hidden lg:block lg:ml-6">
<div className="flex space-x-4">
<Link
href="/upload"
className={
pathname === '/upload'
? 'px-3 py-2 text-sm font-medium bg-accent rounded-md'
: 'px-3 py-2 text-sm font-medium rounded-md hover:bg-accent/20'
}
>
Upload
</Link>
<Link
href="/request"
className={
pathname === '/request'
? 'px-3 py-2 text-sm font-medium bg-accent rounded-md'
: 'px-3 py-2 text-sm font-medium rounded-md hover:bg-accent/20 '
}
>
Request
</Link>
</div>
</div>
</div>
<div className="flex justify-center flex-1 px-2 lg:ml-6 lg:justify-end">
<div className="w-full max-w-lg lg:max-w-xs">
<label
htmlFor="search"
className="sr-only"
>
Search
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<FaSearch
className="w-5 h-5 text-gray-400"
aria-hidden="true"
/>
</div>
<input
id="search"
name="search"
className="block w-full py-2 pl-10 pr-3 leading-5 text-gray-300 placeholder-gray-400 border border-transparent rounded-md bg-base-100 focus:outline-none focus:bg-white focus:border-white focus:ring-white focus:text-gray-900 sm:text-sm"
placeholder="Search"
type="search"
/>
</div>
</div>
</div>
<div className="flex lg:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center p-2 text-gray-400 rounded-md hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<span className="sr-only">Open main menu</span>
{open ? (
<FaTimes
className="block w-6 h-6"
aria-hidden="true"
/>
) : (
<FaBars
className="block w-6 h-6"
aria-hidden="true"
/>
)}
</Disclosure.Button>
</div>
<div className="hidden lg:block lg:ml-4">
<LoginButton session={session} />
</div>
</div>
</div>
<Disclosure.Panel className="lg:hidden">
<div className="px-2 pt-2 pb-3 space-y-1">
<Disclosure.Button
as="a"
href="#"
className="block px-3 py-2 text-base font-medium text-white bg-gray-900 rounded-md"
>
Dashboard
</Disclosure.Button>
<Disclosure.Button
as="a"
href="#"
className="block px-3 py-2 text-base font-medium text-gray-300 rounded-md hover:bg-gray-700 hover:text-white"
>
Team
</Disclosure.Button>
<Disclosure.Button
as="a"
href="#"
className="block px-3 py-2 text-base font-medium text-gray-300 rounded-md hover:bg-gray-700 hover:text-white"
>
Projects
</Disclosure.Button>
<Disclosure.Button
as="a"
href="#"
className="block px-3 py-2 text-base font-medium text-gray-300 rounded-md hover:bg-gray-700 hover:text-white"
>
Calendar
</Disclosure.Button>
</div>
<div className="pt-4 pb-3 border-t border-gray-700">
<div className="flex items-center px-5">
<div className="flex-shrink-0">
<img
className="w-10 h-10 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</div>
<div className="ml-3">
<div className="text-base font-medium text-white">
Tom Cook
</div>
<div className="text-sm font-medium text-gray-400">
tom@example.com
</div>
</div>
</div>
<div className="px-2 mt-3 space-y-1">
<Disclosure.Button
as="a"
href="#"
className="block px-3 py-2 text-base font-medium text-gray-400 rounded-md hover:text-white hover:bg-gray-700"
>
Your Profile
</Disclosure.Button>
<Disclosure.Button
as="a"
href="#"
className="block px-3 py-2 text-base font-medium text-gray-400 rounded-md hover:text-white hover:bg-gray-700"
>
Settings
</Disclosure.Button>
<Disclosure.Button
as="a"
href="#"
className="block px-3 py-2 text-base font-medium text-gray-400 rounded-md hover:text-white hover:bg-gray-700"
>
Sign out
</Disclosure.Button>
</div>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default Navbar;

View File

@@ -1,20 +0,0 @@
import React from 'react';
import Navbar from './Navbar';
interface IPageLayoutProps {
children: React.ReactNode;
}
const PageLayout: React.FC<IPageLayoutProps> = ({ children }) => {
return (
<div
className="h-full min-h-full"
>
<Navbar />
<div className="px-10 py-10">
<main>{children}</main>
</div>
</div>
);
};
export default PageLayout;

View File

@@ -1,3 +0,0 @@
import PageLayout from './PageLayout';
export { PageLayout };

View File

@@ -1,68 +0,0 @@
'use client';
import { Gif } from '@models';
import React from 'react';
import { ImEmbed2 } from 'react-icons/im';
import {
AiOutlineFacebook,
AiOutlineReddit,
AiOutlineTwitter,
} from 'react-icons/ai';
import { Disclosure, Menu, Popover, Transition } from '@headlessui/react';
import SharingEmbedComponent from './SharingEmbedComponent';
interface ISharingComponentProps {
gif: Gif;
}
const SharingComponent: React.FC<ISharingComponentProps> = ({ gif }) => {
return (
<div className="flex gap-6 mt-6">
<a
className="p-1 -m-1 group"
aria-label="Share to Reddit"
target="_blank"
rel="noreferrer"
href={`https://www.reddit.com/submit?url=https%3A%2F%2Fgiphy.com%2Fclips%2Fanimation-studio-turbine-RhpNunPCbmB1dPTiGK`}
>
<AiOutlineReddit className="w-6 h-6 transition fill-zinc-500 group-hover:fill-zinc-600 dark:fill-zinc-400 dark:group-hover:fill-zinc-300" />
</a>
<a
className="p-1 -m-1 group"
aria-label="Share to Twitter"
target="_blank"
rel="noreferrer"
href={`http://twitter.com/share?url=https%3A%2F%2Fgiphy.com%2Fclips%2Fanimation-studio-turbine-RhpNunPCbmB1dPTiGK?tc=1&via=giphy`}
>
<AiOutlineTwitter className="w-6 h-6 transition fill-zinc-500 group-hover:fill-zinc-600 dark:fill-zinc-400 dark:group-hover:fill-zinc-300" />
</a>
<a
className="p-1 -m-1 group"
aria-label="Share to Facebook"
target="_blank"
rel="noreferrer"
href={`http://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fgiphy.com%2Fclips%2Fanimation-studio-turbine-RhpNunPCbmB1dPTiGK`}
>
<AiOutlineFacebook className="w-6 h-6 transition fill-zinc-500 group-hover:fill-zinc-600 dark:fill-zinc-400 dark:group-hover:fill-zinc-300" />
</a>
<Popover className="relative">
{({ open, close }) => (
<>
<Popover.Button className="flex flex-row justify-between">
<ImEmbed2 className="w-6 h-6 transition fill-zinc-500 group-hover:fill-zinc-600 dark:fill-zinc-400 dark:group-hover:fill-zinc-300" />
</Popover.Button>
<Popover.Panel className="absolute z-50">
<SharingEmbedComponent
onClose={() => close()}
gif={gif}
/>
</Popover.Panel>
</>
)}
</Popover>
</div>
);
};
export default SharingComponent;

View File

@@ -1,40 +0,0 @@
import React from 'react';
import { MdArrowBackIosNew } from 'react-icons/md';
import { CopyTextInput } from '@components/index';
import { Gif } from '@models';
interface ISharingEmbedComponent {
gif: Gif;
onClose: () => void;
}
const SharingEmbedComponent: React.FC<ISharingEmbedComponent> = ({
gif,
onClose,
}) => {
return (
<div className="p-4 shadow-xl card w-96 bg-secondary">
<h1 className="flex flex-row justify-between pb-2 align-middle border-b-2 border-warning">
<button onClick={() => onClose()}>
{''}
<MdArrowBackIosNew className="h-6" />
</button>
<span>Embed Gif</span>
</h1>
<div className="mt-2">
<div className="p-2">
<CopyTextInput
label="Fixed frame"
text={gif.fixedEmbedCode}
/>
</div>
<div className="p-2">
<CopyTextInput
label="Responsive frame"
text={gif.responsiveEmbedCode}
/>
</div>
</div>
</div>
);
};
export default SharingEmbedComponent;

View File

@@ -1,69 +0,0 @@
'use client';
import { Transition } from '@headlessui/react';
import React from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { HiOutlineClipboardCopy } from 'react-icons/hi';
interface ICopyTextInput {
label: string;
text: string;
}
const CopyTextInput: React.FC<ICopyTextInput> = ({ label, text }) => {
const [showResult, setShowResult] = React.useState(false);
const _onCopy = () => {
setShowResult(true);
setTimeout(() => {
setShowResult(false);
}, 2000);
};
return (
<React.Fragment>
<div>
<label
htmlFor="text-to-copy"
className="block text-sm font-medium label"
>
{label}
</label>
<div className="flex mt-1 rounded-md shadow-sm ">
<div className="relative flex items-stretch flex-grow indicator focus-within:z-10">
<Transition
show={showResult}
enter="transition-opacity duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-500"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<span className="w-3/4 indicator-item indicator-center badge badge-accent">
Copied successfully
</span>
</Transition>
<input
id="text-to-copy"
readOnly={true}
value={text}
className="block w-full rounded-none input input-md rounded-l-md sm:text-sm"
placeholder="John Doe"
/>
</div>
<CopyToClipboard
text={text}
onCopy={_onCopy}
>
<button
type="button"
className="relative inline-flex items-center px-4 py-2 -ml-px space-x-2 text-sm font-medium btn rounded-r-md"
>
<HiOutlineClipboardCopy className="w-6 h-6 text-accent" />
</button>
</CopyToClipboard>
</div>
</div>
</React.Fragment>
);
};
export default CopyTextInput;

View File

@@ -1,100 +0,0 @@
'use client';
import React from 'react';
import Image from 'next/image';
import { TbThumbUp, TbThumbDown } from 'react-icons/tb';
import { Gif } from 'models';
import Link from 'next/link';
interface IGifContainerProps {
gif: Gif;
isLink?: boolean;
showDetails?: boolean;
}
const GifContainer: React.FC<IGifContainerProps> = ({
gif,
isLink = true,
showDetails = true,
}) => {
const [upVotes, setUpVotes] = React.useState<number>(gif.upVotes);
const [downVotes, setDownVotes] = React.useState<number>(gif.downVotes);
const _doot = async (id: string, isUp: boolean) => {
const response = await fetch(`api/votes?gifId=${id}&isUp=${isUp ? 1 : 0}`, {
method: 'POST',
});
if (response.status === 200) {
const result = (await response.json()) as Gif;
setUpVotes(result.upVotes);
setDownVotes(result.downVotes);
}
};
return (
<>
<div className="group relative h-[17.5rem] transform overflow-hidden rounded-4xl">
<div className="absolute inset-0">
{isLink ? (
<Link
href={`gifs/${gif.slug}`}
title={gif.title}
>
<Image
alt={gif.title}
className="absolute inset-0 transition duration-300 group-hover:scale-110"
src={gif.fileName}
fill
sizes="100vw"
style={{
objectFit: 'fill',
}}
/>
</Link>
) : (
<Image
alt={gif.title}
className="absolute inset-0 transition duration-300 group-hover:scale-110"
src={gif.fileName}
fill
sizes="100vw"
style={{
objectFit: 'fill',
}}
/>
)}
</div>
</div>
{showDetails && (
<div className="flex flex-row p-2">
<div className="flex-1 space-x-2 text-base">
{gif.searchTerms?.map((t) => (
<div
key={t}
className="mr-0.5 badge badge-info badge-md badge-outline"
>
{`#${t}`}
</div>
))}
</div>
<div className="flex items-center justify-center space-x-1">
<div className="flex transition duration-75 ease-in-out delay-150 hover:text-orange-700 hover:cursor-pointer">
<span onClick={() => _doot(gif.id, true)}>
<TbThumbUp className="w-5" />
</span>
<span className="text-xs">{upVotes}</span>
</div>
<div className="flex transition duration-75 ease-in-out delay-150 hover:text-orange-700 hover:cursor-pointer">
<span
onClick={() => _doot(gif.id, false)}
className="pl-2 "
>
<TbThumbDown className="w-5" />
</span>
<span className="text-xs">{downVotes}</span>
</div>
</div>
</div>
)}
</>
);
};
export default GifContainer;

View File

@@ -1,75 +0,0 @@
/* eslint-disable @next/next/no-img-element */
import React from 'react';
import { HiOutlineXMark } from 'react-icons/hi2';
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="flex justify-center px-6 pt-5 pb-6 mt-1 border-2 border-dashed rounded-md border-secondary">
{image ? (
<div>
<div className="relative">
<img
src={image}
alt="Preview"
/>
<button
type="button"
className="absolute top-0 right-0 p-2 m-2 rounded-full "
onClick={() => setImage('')}
>
<HiOutlineXMark className="w-5 h-5" />
</button>
</div>
</div>
) : (
<div className="space-y-1 text-center">
<svg
className="w-12 h-12 mx-auto text-base-content/70"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<div className="flex text-sm text-base-content">
<label
htmlFor="gif-upload"
className="relative font-medium rounded-md cursor-pointer hover:text-accent badge badge-primary"
>
<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;

View File

@@ -1,92 +0,0 @@
import React from 'react';
const Loading = () => {
return (
<div
aria-label="Loading..."
role="status"
className="flex items-center space-x-2"
>
<svg
className="w-6 h-6 animate-spin stroke-accent"
viewBox="0 0 256 256"
>
<line
x1="128"
y1="32"
x2="128"
y2="64"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={24}
/>
<line
x1="195.9"
y1="60.1"
x2="173.3"
y2="82.7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={24}
/>
<line
x1="224"
y1="128"
x2="192"
y2="128"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={24}
/>
<line
x1="195.9"
y1="195.9"
x2="173.3"
y2="173.3"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={24}
/>
<line
x1="128"
y1="224"
x2="128"
y2="192"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={24}
/>
<line
x1="60.1"
y1="195.9"
x2="82.7"
y2="173.3"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={24}
/>
<line
x1="32"
y1="128"
x2="64"
y2="128"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={24}
/>
<line
x1="60.1"
y1="60.1"
x2="82.7"
y2="82.7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={24}
/>
</svg>
<span className="text-xs font-medium text-accent">Loading...</span>
</div>
);
};
export default Loading;

View File

@@ -1,151 +0,0 @@
import { logger } from '@lib/logger';
import React, { KeyboardEventHandler } from 'react';
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);
return (
<>
<label
htmlFor="{name}"
className="block text-sm font-medium"
>
{label}
</label>
<div className="flex w-full text-sm align-middle border rounded-lg shadow-sm border-accent ">
<div className="flex flex-row pt-3 pl-2 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="flex-shrink-0 ml-0.5 h-4 w-4 rounded-full inline-flex items-center justify-center "
>
<span className="sr-only">{tag}</span>
<svg
className="w-2 h-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="w-full input focus:outline-none"
/>
</div>
{isSearching && (
<div
role="status"
className="z-50 ml-5 -mt-3"
>
<svg
aria-hidden="true"
className="w-4 h-4 mr-2 "
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 flex flex-col items-start w-64 -mt-5 text-sm bg-white border rounded-md shadow-md"
>
<ul className="flex flex-col w-full">
{searchResults.map((result) => (
<li
key={result}
onClick={doResultClick}
className="px-2 py-1 space-x-2 cursor-pointer 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;

View File

@@ -1,58 +0,0 @@
'use client';
import { logger } from '@lib/logger';
import React, { Fragment } from 'react';
import { Menu, Transition } from '@headlessui/react';
import { signOut } from 'next-auth/react';
interface IUserNavDropdownProps {
session: any;
}
const UserNavDropdown: React.FC<IUserNavDropdownProps> = ({ session }) => {
React.useEffect(() => {
logger.debug('UserNavDropdown', 'session', session);
}, [session]);
return (
<div className="flex items-center">
<Menu
as="div"
className="relative flex-shrink-0 ml-4"
>
<div>
<Menu.Button className="flex text-sm text-white bg-gray-800 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
<span className="sr-only">Open user menu</span>
<img
className="w-8 h-8 rounded-full"
src={session?.user?.image as string}
alt="Profile image"
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-50 w-48 py-1 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item>
{({ active }) => (
<button
onClick={() => signOut()}
className="block px-4 py-2 text-sm text-gray-700"
>
Logout
</button>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>
);
};
export default UserNavDropdown;

View File

@@ -1,26 +0,0 @@
'use client';
import React from 'react';
import { signIn } from 'next-auth/react';
import { RiLoginCircleLine } from 'react-icons/ri';
import { UserNavDropdown } from '@components';
interface ILoginButtonProps {
session: any;
}
const LoginButton: React.FC<ILoginButtonProps> = ({ session }) => {
return session ? (
<UserNavDropdown session={session} />
) : (
<button
onClick={() => signIn()}
className="normal-case btn btn-ghost drawer-button"
>
<RiLoginCircleLine className="inline-block w-6 h-6 fill-current md:mr-1" />
Login
</button>
);
};
export default LoginButton;

View File

@@ -1,92 +0,0 @@
'use client';
import React from 'react';
import {
ClientSafeProvider,
getProviders,
LiteralUnion,
signIn,
useSession,
} from 'next-auth/react';
import { logger } from '@lib/logger';
import { BuiltInProviderType } from 'next-auth/providers';
import {
FaFacebook,
FaGithub,
FaGithubAlt,
FaGoogle,
FaTwitter,
} from 'react-icons/fa';
import { useRouter } from 'next/navigation';
const SocialLogin = () => {
const router = useRouter();
const [providers, setproviders] = React.useState<Record<
LiteralUnion<BuiltInProviderType, string>,
ClientSafeProvider
> | null>();
const { data: session, status } = useSession();
React.useEffect(() => {
const setTheProviders = async () => {
const setupProviders = await getProviders();
setproviders(setupProviders);
};
setTheProviders();
}, []);
const handleProviderAuth = async (provider: string) => {
logger.debug('signin', 'handleProviderAuth', provider);
const res = await signIn(provider, {
callbackUrl: `${process.env.API_URL}`,
});
logger.debug('signin', 'handleProviderAuth_res', res);
if (res?.ok) {
router.push('/');
}
};
return (
<>
<div className="divider">or continue with</div>
<div className="flex flex-grow w-full gap-3 mt-6">
{providers?.facebook && (
<button
onClick={() => handleProviderAuth(providers.facebook.id)}
className="justify-center flex-1 w-full px-2 py-1 btn btn-outline "
>
<span className="sr-only">Sign in with Facebook</span>
<FaFacebook className="w-5 h-5" />
</button>
)}
{providers?.google && (
<button
onClick={() => handleProviderAuth(providers.google.id)}
className="justify-center flex-1 w-full px-2 py-1 btn btn-outline "
>
<span className="sr-only">Sign in with Google</span>
<FaGoogle className="w-5 h-5" />
</button>
)}
{providers?.github && (
<button
onClick={() => handleProviderAuth(providers.github.id)}
className="justify-center flex-1 w-full px-2 py-1 btn btn-outline "
>
<span className="sr-only">Sign in with GitHub</span>
<FaGithub className="w-5 h-5" />
</button>
)}
{providers?.twitter && (
<button
onClick={() => handleProviderAuth(providers.twitter.id)}
className="justify-center flex-1 w-full px-2 py-1 btn btn-outline "
>
<span className="sr-only">Sign in with Twitter</span>
<FaTwitter className="w-5 h-5" />
</button>
)}
</div>
</>
);
};
export default SocialLogin;

View File

@@ -8,5 +8,5 @@ export default {
dbCredentials: {
url: env.DATABASE_URL,
},
tablesFilter: ["opengifame_*"],
tablesFilter: ["*"],
} satisfies Config;

View File

@@ -0,0 +1,67 @@
CREATE TABLE IF NOT EXISTS "account" (
"user_id" varchar(255) NOT NULL,
"type" varchar(255) NOT NULL,
"provider" varchar(255) NOT NULL,
"provider_account_id" varchar(255) NOT NULL,
"refresh_token" text,
"access_token" text,
"expires_at" integer,
"token_type" varchar(255),
"scope" varchar(255),
"id_token" text,
"session_state" varchar(255),
CONSTRAINT "account_provider_provider_account_id_pk" PRIMARY KEY("provider","provider_account_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "post" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(256),
"created_by" varchar(255) NOT NULL,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updated_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "session" (
"session_token" varchar(255) PRIMARY KEY NOT NULL,
"user_id" varchar(255) NOT NULL,
"expires" timestamp with time zone NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "user" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255),
"email" varchar(255) NOT NULL,
"email_verified" timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
"image" varchar(255),
"password" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "verification_token" (
"identifier" varchar(255) NOT NULL,
"token" varchar(255) NOT NULL,
"expires" timestamp with time zone NOT NULL,
CONSTRAINT "verification_token_identifier_token_pk" PRIMARY KEY("identifier","token")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "post" ADD CONSTRAINT "post_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "account_user_id_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "created_by_idx" ON "post" USING btree ("created_by");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "name_idx" ON "post" USING btree ("name");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "session_user_id_idx" ON "session" USING btree ("user_id");

View File

@@ -1,66 +0,0 @@
CREATE TABLE IF NOT EXISTS "opengifame_account" (
"user_id" varchar(255) NOT NULL,
"type" varchar(255) NOT NULL,
"provider" varchar(255) NOT NULL,
"provider_account_id" varchar(255) NOT NULL,
"refresh_token" text,
"access_token" text,
"expires_at" integer,
"token_type" varchar(255),
"scope" varchar(255),
"id_token" text,
"session_state" varchar(255),
CONSTRAINT "opengifame_account_provider_provider_account_id_pk" PRIMARY KEY("provider","provider_account_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "opengifame_post" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(256),
"created_by" varchar(255) NOT NULL,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updated_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "opengifame_session" (
"session_token" varchar(255) PRIMARY KEY NOT NULL,
"user_id" varchar(255) NOT NULL,
"expires" timestamp with time zone NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "opengifame_user" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255),
"email" varchar(255) NOT NULL,
"email_verified" timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
"image" varchar(255)
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "opengifame_verification_token" (
"identifier" varchar(255) NOT NULL,
"token" varchar(255) NOT NULL,
"expires" timestamp with time zone NOT NULL,
CONSTRAINT "opengifame_verification_token_identifier_token_pk" PRIMARY KEY("identifier","token")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "opengifame_account" ADD CONSTRAINT "opengifame_account_user_id_opengifame_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."opengifame_user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "opengifame_post" ADD CONSTRAINT "opengifame_post_created_by_opengifame_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."opengifame_user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "opengifame_session" ADD CONSTRAINT "opengifame_session_user_id_opengifame_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."opengifame_user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "account_user_id_idx" ON "opengifame_account" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "created_by_idx" ON "opengifame_post" USING btree ("created_by");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "name_idx" ON "opengifame_post" USING btree ("name");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "session_user_id_idx" ON "opengifame_session" USING btree ("user_id");

View File

@@ -1,11 +1,11 @@
{
"id": "eba24bbe-db75-4d3a-a69e-fc8fc26a5173",
"id": "3cde7a7b-e714-4d18-a93a-9e07c9e48f5a",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.opengifame_account": {
"name": "opengifame_account",
"public.account": {
"name": "account",
"schema": "",
"columns": {
"user_id": {
@@ -93,10 +93,10 @@
}
},
"foreignKeys": {
"opengifame_account_user_id_opengifame_user_id_fk": {
"name": "opengifame_account_user_id_opengifame_user_id_fk",
"tableFrom": "opengifame_account",
"tableTo": "opengifame_user",
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
@@ -108,8 +108,8 @@
}
},
"compositePrimaryKeys": {
"opengifame_account_provider_provider_account_id_pk": {
"name": "opengifame_account_provider_provider_account_id_pk",
"account_provider_provider_account_id_pk": {
"name": "account_provider_provider_account_id_pk",
"columns": [
"provider",
"provider_account_id"
@@ -118,8 +118,8 @@
},
"uniqueConstraints": {}
},
"public.opengifame_post": {
"name": "opengifame_post",
"public.post": {
"name": "post",
"schema": "",
"columns": {
"id": {
@@ -187,10 +187,10 @@
}
},
"foreignKeys": {
"opengifame_post_created_by_opengifame_user_id_fk": {
"name": "opengifame_post_created_by_opengifame_user_id_fk",
"tableFrom": "opengifame_post",
"tableTo": "opengifame_user",
"post_created_by_user_id_fk": {
"name": "post_created_by_user_id_fk",
"tableFrom": "post",
"tableTo": "user",
"columnsFrom": [
"created_by"
],
@@ -204,8 +204,8 @@
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.opengifame_session": {
"name": "opengifame_session",
"public.session": {
"name": "session",
"schema": "",
"columns": {
"session_token": {
@@ -245,10 +245,10 @@
}
},
"foreignKeys": {
"opengifame_session_user_id_opengifame_user_id_fk": {
"name": "opengifame_session_user_id_opengifame_user_id_fk",
"tableFrom": "opengifame_session",
"tableTo": "opengifame_user",
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
@@ -262,8 +262,8 @@
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.opengifame_user": {
"name": "opengifame_user",
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
@@ -296,6 +296,12 @@
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
@@ -303,8 +309,8 @@
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.opengifame_verification_token": {
"name": "opengifame_verification_token",
"public.verification_token": {
"name": "verification_token",
"schema": "",
"columns": {
"identifier": {
@@ -329,8 +335,8 @@
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"opengifame_verification_token_identifier_token_pk": {
"name": "opengifame_verification_token_identifier_token_pk",
"verification_token_identifier_token_pk": {
"name": "verification_token_identifier_token_pk",
"columns": [
"identifier",
"token"

View File

@@ -5,8 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1725317790880,
"tag": "0000_sleepy_whizzer",
"when": 1725454391060,
"tag": "0000_dazzling_cerebro",
"breakpoints": true
}
]

View File

@@ -1,45 +0,0 @@
import { logger } from './logger';
import client from './prismadb';
export const titleToSlug = async (title: string): Promise<string> => {
const slug = _getSlug(title);
let candidate = slug;
let i = 1;
while (
(await client.gif.findUnique({
where: {
slug: candidate,
},
})) !== null
) {
candidate = `${slug}-${i++}`;
}
return candidate;
};
const _getSlug = (title: string): string => {
let slug;
// convert to lower case
slug = title.toLowerCase();
// remove special characters
slug = slug.replace(
/\`|\~|\!|\@|\#|\||\$|\%|\^|\&|\*|\(|\)|\+|\=|\,|\.|\/|\?|\>|\<|\'|\"|\:|\;|_/gi,
''
);
// The /gi modifier is used to do a case insensitive search of all occurrences of a regular expression in a string
// replace spaces with dash symbols
slug = slug.replace(/ /gi, '-');
// remove consecutive dash symbols
slug = slug.replace(/\-\-\-\-\-/gi, '-');
slug = slug.replace(/\-\-\-\-/gi, '-');
slug = slug.replace(/\-\-\-/gi, '-');
slug = slug.replace(/\-\-/gi, '-');
// remove the unwanted dash symbols at the beginning and the end of the slug
slug = '@' + slug + '@';
slug = slug.replace(/\@\-|\-\@|\@/gi, '');
return slug;
};

2777
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,21 +15,31 @@
},
"dependencies": {
"@auth/drizzle-adapter": "^1.1.0",
"@headlessui/react": "^2.1.4",
"@hookform/resolvers": "^3.9.0",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.50.0",
"@trpc/client": "^11.0.0-rc.446",
"@trpc/react-query": "^11.0.0-rc.446",
"@trpc/server": "^11.0.0-rc.446",
"@types/bcrypt": "^5.0.2",
"@types/loglevel": "^1.6.3",
"@types/react-copy-to-clipboard": "^5.0.7",
"bcrypt": "^5.1.1",
"chalk": "^5.3.0",
"drizzle-orm": "^0.33.0",
"geist": "^1.3.0",
"lodash": "^4.17.21",
"loglevel": "^1.9.1",
"loglevel-plugin-prefix": "^0.8.4",
"next": "^14.2.4",
"next-auth": "^4.24.7",
"postgres": "^3.4.4",
"react": "^18.3.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-icons": "^5.3.0",
"server-only": "^0.0.1",
"superjson": "^2.2.1",
"zod": "^3.23.3"

View File

@@ -1,26 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { mapGif } from '@lib/mapping/gif';
import client from '@lib/prismadb';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { slug } = req.query;
const gif = await client.gif.findUnique({
where: {
slug: slug as string,
},
include: {
_count: {
select: {
upVotes: true,
downVotes: true,
},
},
},
});
if (!gif) {
return {
notFound: true,
};
}
return res.status(200).json(mapGif(gif));
};
export default handler;

View File

@@ -1,32 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { mapGif } from '@lib/mapping/gif';
import client from '@lib/prismadb';
import { logger } from '@lib/logger';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== 'GET') {
res.setHeader('Allow', 'GET');
return res.status(405).json({
data: null,
error: 'Method Not Allowed',
});
}
const results = await client.gif.findMany({
take: 12,
include: {
_count: {
select: {
upVotes: true,
downVotes: true,
},
},
},
orderBy: {
upVotes: {
_count: 'desc',
},
},
});
const gifs = results.map((gif) => mapGif(gif));
return res.status(200).json(gifs);
};
export default handler;

View File

@@ -1,95 +0,0 @@
import {NextApiRequest, NextApiResponse} from 'next';
import formidable, {File} from 'formidable';
import {promises as fs} from 'fs';
import mime from 'mime-types';
import {logger} from '@lib/logger';
import {getSession} from 'next-auth/react';
import {titleToSlug} from '@lib/slug';
type ProcessedFiles = Array<[string, File]>;
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({req});
if (!session?.user?.id) {
return res.status(401).json({status: 'denied', message: 'Access denied'});
}
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST');
return res.status(405).json({
data: null,
error: 'Method Not Allowed',
});
} else {
var formData: { [key: string]: string } = {};
const files = await new Promise<ProcessedFiles | undefined>(
(resolve, reject) => {
const form = new formidable.IncomingForm();
const files: ProcessedFiles = [];
form.on('file', (field, file) => {
files.push([field, file]);
});
form.on('field', (name, value) => {
logger.debug('Got field value: ', name, value);
formData[name] = value;
});
form.on('end', () => resolve(files));
form.on('error', (err) => reject(err));
form.parse(req, () => {
});
}
).catch((e) => {
logger.error('index', 'Error parsing form', e);
return res.status(500).json({
status: 'fail',
message: 'Upload error',
});
});
if (files?.length) {
const newGif = await prisma?.gif.create({
data: {
userId: session.user.id as string,
title: formData.title,
description: formData.description,
searchTerms: formData.terms.split('|'),
slug: await titleToSlug(formData.title),
},
});
if (!newGif?.id) {
return res.status(401).json({
status: 'error',
message: 'Unable to save gif',
});
}
logger.debug('FormData: ', formData);
for (const file of files) {
const tempPath = file[1].filepath;
await fs.copyFile(
tempPath,
`./public/uploads/${newGif.id}.${mime.extension(
file[1].mimetype as string
)}`
);
await fs.unlink(tempPath);
}
return res.status(201).json({
status: 'ok',
message: 'Gif created successfully',
});
}
return res.status(400).json({
status: 'error',
message: 'No files in request',
});
}
};
// const saveFile = async (file) => {
// const data = fs.readFileSync(file.path);
// fs.writeFileSync(`./public/uploads/${file.name}`, data);
// await fs.unlinkSync(file.path);
// };
export const config = {
api: {
bodyParser: false,
},
};
export default handler;

View File

@@ -1,132 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { mapGif } from '@lib/mapping/gif';
import client from '@lib/prismadb';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const {
query: { gifId, isUp },
} = req;
const browserId = req.cookies.bid;
if (!gifId || !browserId) {
return res.status(400);
}
if (isUp === '1') {
return _processUpvote(res, gifId as string, browserId);
} else {
return _processDownvote(res, gifId as string, browserId);
}
}
const _processDownvote = async (
res: NextApiResponse,
gifId: string,
browserId: string
) => {
//check for existing downvote
const exists = await client.downVotes.count({
where: {
gifId: gifId,
browserId: browserId,
},
});
if (exists !== 0) {
return res.status(403).json({
message: 'You have already downvoted on this gif',
});
}
//delete any upvotes
try {
await client.upVotes.delete({
where: {
browserId_gifId: {
gifId: gifId,
browserId: browserId,
},
},
});
} catch {}
const result = await client.downVotes.create({
data: {
browserId: browserId as string,
gifId: gifId as string,
},
});
const response = await client.gif.findUnique({
where: {
id: gifId,
},
include: {
_count: {
select: {
upVotes: true,
downVotes: true,
},
},
},
});
if (response !== null) {
return res.status(200).json(mapGif(response));
}
return res.status(404);
};
const _processUpvote = async (
res: NextApiResponse,
gifId: string,
browserId: string
) => {
//check for existing upvote
const exists = await client.upVotes.count({
where: {
gifId: gifId,
browserId: browserId,
},
});
if (exists !== 0) {
return res.status(403).json({
message: 'You have already upvoted this gif',
});
}
//delete any downvotes
try {
await client.downVotes.delete({
where: {
browserId_gifId: {
gifId: gifId,
browserId: browserId,
},
},
});
} catch {}
const result = await client.upVotes.create({
data: {
browserId: browserId as string,
gifId: gifId as string,
},
});
const response = await client.gif.findUnique({
where: {
id: gifId,
},
include: {
_count: {
select: {
upVotes: true,
downVotes: true,
},
},
},
});
if (response !== null) {
return res.status(200).json(mapGif(response));
}
return res.status(404);
};

View File

@@ -1,12 +0,0 @@
-- CreateTable
CREATE TABLE "Gif" (
"id" TEXT NOT NULL,
"title" VARCHAR(100) NOT NULL,
"description" VARCHAR(2000) NOT NULL,
"searchTerms" VARCHAR(2000) NOT NULL,
"upVotes" INTEGER NOT NULL DEFAULT 0,
"downVotes" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Gif_pkey" PRIMARY KEY ("id")
);

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `fileName` to the `Gif` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Gif" ADD COLUMN "fileName" VARCHAR(100) NOT NULL;

View File

@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "Gif" ALTER COLUMN "description" DROP NOT NULL,
ALTER COLUMN "searchTerms" DROP NOT NULL;

View File

@@ -1,24 +0,0 @@
/*
Warnings:
- You are about to drop the column `downVotes` on the `Gif` table. All the data in the column will be lost.
- You are about to drop the column `upVotes` on the `Gif` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Gif" DROP COLUMN "downVotes",
DROP COLUMN "upVotes";
-- CreateTable
CREATE TABLE "Votes" (
"id" TEXT NOT NULL,
"isUp" BOOLEAN NOT NULL,
"browserId" VARCHAR(1000) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"gifId" TEXT NOT NULL,
CONSTRAINT "Votes_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Votes" ADD CONSTRAINT "Votes_gifId_fkey" FOREIGN KEY ("gifId") REFERENCES "Gif"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,39 +0,0 @@
/*
Warnings:
- You are about to drop the `Votes` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Votes" DROP CONSTRAINT "Votes_gifId_fkey";
-- DropTable
DROP TABLE "Votes";
-- CreateTable
CREATE TABLE "UpVotes" (
"id" TEXT NOT NULL,
"isUp" BOOLEAN NOT NULL,
"browserId" VARCHAR(1000) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"gifId" TEXT NOT NULL,
CONSTRAINT "UpVotes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DownVotes" (
"id" TEXT NOT NULL,
"isUp" BOOLEAN NOT NULL,
"browserId" VARCHAR(1000) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"gifId" TEXT NOT NULL,
CONSTRAINT "DownVotes_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "UpVotes" ADD CONSTRAINT "UpVotes_gifId_fkey" FOREIGN KEY ("gifId") REFERENCES "Gif"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DownVotes" ADD CONSTRAINT "DownVotes_gifId_fkey" FOREIGN KEY ("gifId") REFERENCES "Gif"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,12 +0,0 @@
/*
Warnings:
- You are about to drop the column `isUp` on the `DownVotes` table. All the data in the column will be lost.
- You are about to drop the column `isUp` on the `UpVotes` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "DownVotes" DROP COLUMN "isUp";
-- AlterTable
ALTER TABLE "UpVotes" DROP COLUMN "isUp";

View File

@@ -1,12 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[browserId,gifId]` on the table `DownVotes` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[browserId,gifId]` on the table `UpVotes` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "DownVotes_browserId_gifId_key" ON "DownVotes"("browserId", "gifId");
-- CreateIndex
CREATE UNIQUE INDEX "UpVotes_browserId_gifId_key" ON "UpVotes"("browserId", "gifId");

View File

@@ -1,66 +0,0 @@
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT;

View File

@@ -1,14 +0,0 @@
/*
Warnings:
- You are about to drop the column `emailVerified` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `image` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `name` on the `User` table. All the data in the column will be lost.
- Made the column `email` on table `User` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "emailVerified",
DROP COLUMN "image",
DROP COLUMN "name",
ALTER COLUMN "email" SET NOT NULL;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- Made the column `password` on table `User` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "password" SET NOT NULL;

View File

@@ -1,21 +0,0 @@
-- CreateTable
CREATE TABLE "Season" (
"id" TEXT NOT NULL,
"number" INTEGER NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "Season_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Episode" (
"id" TEXT NOT NULL,
"number" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"seasonId" TEXT,
CONSTRAINT "Episode_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Episode" ADD CONSTRAINT "Episode_seasonId_fkey" FOREIGN KEY ("seasonId") REFERENCES "Season"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,16 +0,0 @@
/*
Warnings:
- You are about to drop the column `name` on the `Season` table. All the data in the column will be lost.
- A unique constraint covering the columns `[number,seasonId]` on the table `Episode` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[number]` on the table `Season` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Season" DROP COLUMN "name";
-- CreateIndex
CREATE UNIQUE INDEX "Episode_number_seasonId_key" ON "Episode"("number", "seasonId");
-- CreateIndex
CREATE UNIQUE INDEX "Season_number_key" ON "Season"("number");

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `airDate` to the `Episode` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Episode" ADD COLUMN "airDate" TIMESTAMP(3) NOT NULL;

View File

@@ -1,14 +0,0 @@
/*
Warnings:
- Made the column `seasonId` on table `Episode` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE "Episode" DROP CONSTRAINT "Episode_seasonId_fkey";
-- AlterTable
ALTER TABLE "Episode" ALTER COLUMN "seasonId" SET NOT NULL;
-- AddForeignKey
ALTER TABLE "Episode" ADD CONSTRAINT "Episode_seasonId_fkey" FOREIGN KEY ("seasonId") REFERENCES "Season"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,17 +0,0 @@
-- AlterTable
ALTER TABLE "Gif" ADD COLUMN "userId" TEXT;
-- CreateTable
CREATE TABLE "Tags" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "Tags_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Gif" ADD CONSTRAINT "Gif_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Tags" ADD CONSTRAINT "Tags_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[name]` on the table `Tags` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Tags_name_key" ON "Tags"("name");

View File

@@ -1,14 +0,0 @@
/*
Warnings:
- Made the column `userId` on table `Gif` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE "Gif" DROP CONSTRAINT "Gif_userId_fkey";
-- AlterTable
ALTER TABLE "Gif" ALTER COLUMN "userId" SET NOT NULL;
-- AddForeignKey
ALTER TABLE "Gif" ADD CONSTRAINT "Gif_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Gif" ADD COLUMN "publicVisible" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -1,8 +0,0 @@
/*
Warnings:
- You are about to drop the column `fileName` on the `Gif` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Gif" DROP COLUMN "fileName";

View File

@@ -1,9 +0,0 @@
/*
Warnings:
- The `searchTerms` column on the `Gif` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- AlterTable
ALTER TABLE "Gif" DROP COLUMN "searchTerms",
ADD COLUMN "searchTerms" VARCHAR(2000)[];

View File

@@ -1,11 +0,0 @@
-- CreateEnum
CREATE TYPE "SettingType" AS ENUM ('STRING', 'BOOL', 'NUMBER');
-- CreateTable
CREATE TABLE "Setting" (
"key" VARCHAR NOT NULL,
"value" VARCHAR NOT NULL,
"type" "SettingType" NOT NULL DEFAULT 'STRING',
CONSTRAINT "Setting_pkey" PRIMARY KEY ("key")
);

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "emailVerified" TIMESTAMP(3),
ADD COLUMN "image" TEXT,
ADD COLUMN "name" TEXT,
ALTER COLUMN "email" DROP NOT NULL;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL;

View File

@@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "oauth_token" TEXT,
ADD COLUMN "oauth_token_secret" TEXT;

View File

@@ -1,12 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[slug]` on the table `Gif` will be added. If there are existing duplicate values, this will fail.
- Added the required column `slug` to the `Gif` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Gif" ADD COLUMN "slug" VARCHAR(50) NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Gif_slug_key" ON "Gif"("slug");

View File

@@ -1,140 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
binaryTargets = ["native"]
previewFeatures = ["filteredRelationCount"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
oauth_token String? @db.Text
oauth_token_secret String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
password String?
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
Tags Tags[]
Gif Gif[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
enum SettingType {
STRING
BOOL
NUMBER
}
model Setting {
key String @id @db.VarChar
value String @db.VarChar
type SettingType @default(STRING)
}
model Gif {
id String @id @default(cuid())
slug String @db.VarChar(50)
title String @db.VarChar(100)
description String? @db.VarChar(2000)
searchTerms String[] @db.VarChar(2000)
publicVisible Boolean @default(false)
upVotes UpVotes[]
downVotes DownVotes[]
createdAt DateTime @default(now())
//TODO: remove nullable
user User @relation(fields: [userId], references: [id])
userId String
@@unique([slug])
}
model UpVotes {
id String @id @default(cuid())
browserId String @db.VarChar(1000)
createdAt DateTime @default(now())
gif Gif @relation(fields: [gifId], references: [id])
gifId String
@@unique([browserId, gifId])
}
model DownVotes {
id String @id @default(cuid())
browserId String @db.VarChar(1000)
createdAt DateTime @default(now())
gif Gif @relation(fields: [gifId], references: [id])
gifId String
@@unique([browserId, gifId])
}
model Season {
id String @id @default(cuid())
number Int @unique
episodes Episode[]
}
model Episode {
id String @id @default(cuid())
number Int
name String
airDate DateTime
Season Season @relation(fields: [seasonId], references: [id])
seasonId String
@@unique([number, seasonId])
}
model Tags {
id String @id @default(cuid())
name String @unique()
user User @relation(fields: [userId], references: [id])
userId String
}

View File

@@ -1,101 +0,0 @@
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcrypt';
import { titleToSlug } from '@lib/slug';
const prisma = new PrismaClient();
type Episode = {
season: number;
episode: number;
name: string;
air_date: string;
};
const episodes: Episode[] = [];
const tags = [
'Frasier',
'Niles',
'Eddie',
'Daphne',
'Roz',
'Bulldog',
'Noel',
'Kenny',
'Wine',
'Sherry',
];
async function main() {
//add admin user
const adminUser = process.env.ADMIN_USER as string;
const adminPassword = await bcrypt.hash(
process.env.ADMIN_PASSWORD as string,
0
);
console.log('seed', 'creating user', adminUser);
const user = await prisma.user.upsert({
where: {
email: adminUser,
},
update: {
password: adminPassword,
},
create: {
email: adminUser,
password: adminPassword,
},
});
for (const tag of tags) {
const newTag = await prisma.tags.upsert({
where: {
name: tag,
},
update: {},
create: {
name: tag,
userId: user.id,
},
});
}
//add seasons and episodes
for (const e of episodes) {
const season = await prisma.season.upsert({
where: { number: e.season },
update: {},
create: {
number: e.season,
},
});
const episode = await prisma.episode.upsert({
where: {
number_seasonId: {
number: e.episode,
seasonId: season.id,
},
},
update: {},
create: {
number: e.episode,
name: e.name,
airDate: new Date(Date.parse(e.air_date)),
seasonId: season.id,
},
});
console.log('seed', episode);
}
const gifs = await prisma.gif.findMany();
for (const gif of gifs) {
if (!gif.slug) {
gif.slug = await titleToSlug(gif.title);
}
}
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e), await prisma.$disconnect();
process.exit(1);
});

View File

@@ -1,5 +1,124 @@
export const SignInPage = () => {
return <div>Sign me in bitch!</div>;
"use client";
import SocialLogin from "@/components/widgets/login/SocialLogin";
import Link from "next/link";
import React from "react";
import { logger } from "@/lib/logger";
import { signIn } from "next-auth/react";
const SignInPage = () => {
const [userInfo, setUserInfo] = React.useState({
email: "fergal.moran+opengifame@gmail.com",
password: "secret",
});
return (
<div className="flex min-h-full flex-col justify-center py-1 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-2 text-center text-3xl font-extrabold">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm">
Or
<Link href="/auth/signup" className="font-medium">
create a new account
</Link>
</p>
</div>
<div className="mt-2 sm:mx-auto sm:w-full sm:max-w-md">
<div className="px-4 py-8 shadow sm:rounded-lg sm:px-10">
<form
className="space-y-6"
onSubmit={async (e) => {
e.preventDefault();
logger.debug("signin", "using", userInfo);
const result = await signIn("credentials", {
redirect: false,
email: userInfo.email,
password: userInfo.password,
});
logger.debug("signin", "result", result);
}}
method="post"
>
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email address
</label>
<div className="mt-1">
<input
id="email"
type="email"
autoComplete="email"
required
value={userInfo.email}
onChange={({ target }) =>
setUserInfo({ ...userInfo, email: target.value })
}
className="input input-bordered w-full"
/>
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<div className="mt-1">
<input
id="password"
type="password"
autoComplete="current-password"
required
value={userInfo.password}
onChange={({ target }) =>
setUserInfo({ ...userInfo, password: target.value })
}
className="input input-bordered w-full"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4"
/>
<label
htmlFor="remember-me"
className="text-accent ml-2 block text-sm"
>
Remember me
</label>
</div>
<div className="text-sm">
<a
href="#"
className="text-info hover:text-primary/50 font-medium"
>
Forgot your password?
</a>
</div>
</div>
<div>
<button type="submit" className="btn btn-primary w-full">
Sign in
</button>
</div>
</form>
<div className="mt-6">
<SocialLogin />
</div>
</div>
</div>
</div>
);
};
export default SignInPage;

View File

@@ -0,0 +1,125 @@
"use client";
import React from 'react';
import RegistrationForm from '@/components/RegistrationForm';
const RegisterPage: React.FC = () => {
return (
<div>
<h1>Register</h1>
<RegistrationForm />
</div>
);
};
export default RegisterPage;
// const SignUpPage = () => {
// const router = useRouter();
//
// const signup = api.auth.create.useMutation({
// onSuccess: () => router.push("/auth/signin"),
// onError: (error) => {
// logger.error("signup", "error", error);
// },
// });
// const [userInfo, setUserInfo] = React.useState({
// email: "fergal.moran+opengifame@gmail.com",
// password: "secret",
// repeatPassword: "secret",
// });
//
// return (
// <div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
// <div className="sm:mx-auto sm:w-full sm:max-w-md">
// <h2 className="mt-2 text-center text-3xl font-extrabold">
// Create new account
// </h2>
// </div>
//
// <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
// <div className="px-4 py-8 shadow sm:rounded-lg sm:px-10">
// <form
// className="space-y-6"
// onSubmit={(e) => {
// e.preventDefault();
// signup.mutate(userInfo);
// }}
// method="post"
// >
// <div>
// <label htmlFor="email" className="block text-sm font-medium">
// Email address
// </label>
// <div className="mt-1">
// <input
// id="email"
// type="email"
// autoComplete="email"
// required
// value={userInfo.email}
// onChange={({ target }) =>
// setUserInfo({ ...userInfo, email: target.value })
// }
// className="input input-bordered w-full"
// />
// </div>
// </div>
//
// <div>
// <label htmlFor="password" className="block text-sm font-medium">
// Password
// </label>
// <div className="mt-1">
// <input
// id="password"
// type="password"
// autoComplete="current-password"
// required
// value={userInfo.password}
// onChange={({ target }) =>
// setUserInfo({ ...userInfo, password: target.value })
// }
// className="input input-bordered w-full"
// />
// </div>
// </div>
// <div>
// <label
// htmlFor="repeat-password"
// className="block text-sm font-medium"
// >
// Repeat password
// </label>
// <div className="mt-1">
// <input
// id="repeat-password"
// type="password"
// autoComplete="current-password"
// required
// value={userInfo.repeatPassword}
// onChange={({ target }) =>
// setUserInfo({ ...userInfo, repeatPassword: target.value })
// }
// className="input input-bordered w-full"
// />
// </div>
// </div>
//
// <div>
// <button type="submit" className="btn btn-primary w-full">
// Create account
// </button>
// </div>
// </form>
//
// <div className="mt-6">
// <SocialLogin />
// </div>
// </div>
// </div>
// </div>
// );
// };
//
// export default SignUpPage;

View File

@@ -0,0 +1,9 @@
"use client";
import React from "react";
import { SessionProvider } from "next-auth/react";
export default function SiteLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return <SessionProvider>{children}</SessionProvider>;
}

View File

@@ -5,46 +5,6 @@ import { useState } from "react";
import { api } from "@/trpc/react";
export function TrendingImages() {
const [latestPost] = api.post.getLatest.useSuspenseQuery();
const utils = api.useUtils();
const [name, setName] = useState("");
const createPost = api.post.create.useMutation({
onSuccess: async () => {
await utils.post.invalidate();
setName("");
},
});
return (
<div className="w-full max-w-xs">
{latestPost ? (
<p className="truncate">Your most recent post: {latestPost.name}</p>
) : (
<p>You have no posts yet.</p>
)}
<form
onSubmit={(e) => {
e.preventDefault();
createPost.mutate({ name });
}}
className="flex flex-col gap-2"
>
<input
type="text"
placeholder="Title"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-full px-4 py-2 text-black"
/>
<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..." : "Submit"}
</button>
</form>
</div>
);
return <div className="w-full max-w-xs">Trending Images</div>;
}

View File

@@ -4,7 +4,7 @@ import { api, HydrateClient } from "@/trpc/server";
export default async function Home() {
const session = await getServerAuthSession();
void api.post.getLatest.prefetch();
// void api.post.getLatest.prefetch();
return (
<HydrateClient>

View File

@@ -0,0 +1,73 @@
// src/components/RegistrationForm.tsx
import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
Input,
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
} from "shadcn-ui";
const registrationSchema = z.object({
email: z.string().email({ message: "Invalid email address" }),
password: z
.string()
.min(5, { message: "Password must be at least 5 characters long" }),
});
type RegistrationFormValues = z.infer<typeof registrationSchema>;
const RegistrationForm: React.FC = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<RegistrationFormValues>({
resolver: zodResolver(registrationSchema),
});
const createUser = trpc.auth.create.useMutation();
const onSubmit = async (data: RegistrationFormValues) => {
try {
await createUser.mutateAsync(data);
alert("User registered successfully");
} catch (error) {
alert("Failed to register user");
}
};
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<FormField>
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...register("email")} />
</FormControl>
{errors.email && <FormMessage>{errors.email.message}</FormMessage>}
</FormItem>
</FormField>
<FormField>
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...register("password")} />
</FormControl>
{errors.password && (
<FormMessage>{errors.password.message}</FormMessage>
)}
</FormItem>
</FormField>
<Button type="submit">Register</Button>
</Form>
);
};
export default RegistrationForm;

View File

@@ -2,7 +2,6 @@
import React from 'react';
import Image from 'next/image';
import { TbThumbUp, TbThumbDown } from 'react-icons/tb';
import { Gif } from 'models';
import Link from 'next/link';
interface IGifContainerProps {

View File

@@ -1,5 +1,5 @@
import { logger } from '@lib/logger';
import React, { KeyboardEventHandler } from 'react';
import { logger } from "@/lib/logger";
import React, { KeyboardEventHandler } from "react";
interface ITaggedInputProps {
label: string;
@@ -12,12 +12,12 @@ const TaggedInput: React.FC<ITaggedInputProps> = ({
onChange,
}) => {
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 [tags, setTags] = React.useState<Array<string>>(value);
React.useEffect(() => {
logger.debug('TaggedInput', 'callingOnChange', tags);
logger.debug("TaggedInput", "callingOnChange", tags);
onChange(tags);
}, [tags, onChange]);
const removeTag = (tag: string) => {
@@ -42,7 +42,7 @@ const TaggedInput: React.FC<ITaggedInputProps> = ({
await searchTags(value);
};
const handleKeyPress = ($event: React.KeyboardEvent<HTMLInputElement>) => {
if ($event.code === 'Enter' || $event.code === 'NumpadEnter') {
if ($event.code === "Enter" || $event.code === "NumpadEnter") {
__addTag(searchText);
}
};
@@ -50,34 +50,29 @@ const TaggedInput: React.FC<ITaggedInputProps> = ({
setTags([...tags, tag]);
setSearchResults([]);
setIsSearching(false);
setSearchText('');
setSearchText("");
};
const doResultClick = ($event: any) => __addTag($event.target.textContent);
const doResultClick = ($event: any) =>
__addTag($event.target.textContent as string);
return (
<>
<label
htmlFor="{name}"
className="block text-sm font-medium"
>
<label htmlFor="{name}" className="block text-sm font-medium">
{label}
</label>
<div className="flex w-full text-sm align-middle border rounded-lg shadow-sm border-accent ">
<div className="flex flex-row pt-3 pl-2 space-x-1">
<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"
>
<span key={tag} className="badge badge-primary badge-lg py-0.5">
{tag}
<button
onClick={() => removeTag(tag)}
type="button"
className="flex-shrink-0 ml-0.5 h-4 w-4 rounded-full inline-flex items-center justify-center "
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="w-2 h-2"
className="h-2 w-2"
stroke="currentColor"
fill="none"
viewBox="0 0 8 8"
@@ -97,17 +92,14 @@ const TaggedInput: React.FC<ITaggedInputProps> = ({
onKeyDown={handleKeyPress}
onChange={handleChange}
placeholder="Start typing and press enter"
className="w-full input focus:outline-none"
className="input w-full focus:outline-none"
/>
</div>
{isSearching && (
<div
role="status"
className="z-50 ml-5 -mt-3"
>
<div role="status" className="z-50 -mt-3 ml-5">
<svg
aria-hidden="true"
className="w-4 h-4 mr-2 "
className="mr-2 h-4 w-4"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@@ -128,14 +120,14 @@ const TaggedInput: React.FC<ITaggedInputProps> = ({
<div className={`z-50 mb-4 flex space-y-0`}>
<aside
aria-labelledby="menu-heading"
className="absolute z-50 flex flex-col items-start w-64 -mt-5 text-sm bg-white border rounded-md 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 flex-col w-full">
<ul className="flex w-full flex-col">
{searchResults.map((result) => (
<li
key={result}
onClick={doResultClick}
className="px-2 py-1 space-x-2 cursor-pointer 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}
</li>

View File

@@ -1,9 +1,9 @@
'use client';
"use client";
import React from 'react';
import { signIn } from 'next-auth/react';
import { RiLoginCircleLine } from 'react-icons/ri';
import { UserNavDropdown } from '@components';
import React from "react";
import { signIn } from "next-auth/react";
import { RiLoginCircleLine } from "react-icons/ri";
import UserNavDropdown from "../UserNavDropdown";
interface ILoginButtonProps {
session: any;
@@ -15,9 +15,9 @@ const LoginButton: React.FC<ILoginButtonProps> = ({ session }) => {
) : (
<button
onClick={() => signIn()}
className="normal-case btn btn-ghost drawer-button"
className="btn btn-ghost drawer-button normal-case"
>
<RiLoginCircleLine className="inline-block w-6 h-6 fill-current md:mr-1" />
<RiLoginCircleLine className="inline-block h-6 w-6 fill-current md:mr-1" />
Login
</button>
);

View File

@@ -1,23 +1,17 @@
'use client';
"use client";
import React from 'react';
import React from "react";
import {
ClientSafeProvider,
type ClientSafeProvider,
type LiteralUnion,
getProviders,
LiteralUnion,
signIn,
useSession,
} from 'next-auth/react';
import { logger } from '@lib/logger';
import { BuiltInProviderType } from 'next-auth/providers';
import {
FaFacebook,
FaGithub,
FaGithubAlt,
FaGoogle,
FaTwitter,
} from 'react-icons/fa';
import { useRouter } from 'next/navigation';
} from "next-auth/react";
import { logger } from "@/lib/logger";
import { FaFacebook, FaGithub, FaGoogle, FaTwitter } from "react-icons/fa";
import { useRouter } from "next/navigation";
import type { BuiltInProviderType } from "next-auth/providers/index";
const SocialLogin = () => {
const router = useRouter();
@@ -32,56 +26,58 @@ const SocialLogin = () => {
const setupProviders = await getProviders();
setproviders(setupProviders);
};
setTheProviders();
setTheProviders().catch((error) => {
logger.error("SocialLogin", "Error setting up providers", error);
});
}, []);
const handleProviderAuth = async (provider: string) => {
logger.debug('signin', 'handleProviderAuth', provider);
logger.debug("signin", "handleProviderAuth", provider);
const res = await signIn(provider, {
callbackUrl: `${process.env.API_URL}`,
});
logger.debug('signin', 'handleProviderAuth_res', res);
logger.debug("signin", "handleProviderAuth_res", res);
if (res?.ok) {
router.push('/');
router.push("/");
}
};
return (
<>
<div className="divider">or continue with</div>
<div className="flex flex-grow w-full gap-3 mt-6">
<div className="mt-6 flex w-full flex-grow gap-3">
{providers?.facebook && (
<button
onClick={() => handleProviderAuth(providers.facebook.id)}
className="justify-center flex-1 w-full px-2 py-1 btn btn-outline "
className="btn btn-outline w-full flex-1 justify-center px-2 py-1"
>
<span className="sr-only">Sign in with Facebook</span>
<FaFacebook className="w-5 h-5" />
<FaFacebook className="h-5 w-5" />
</button>
)}
{providers?.google && (
<button
onClick={() => handleProviderAuth(providers.google.id)}
className="justify-center flex-1 w-full px-2 py-1 btn btn-outline "
className="btn btn-outline w-full flex-1 justify-center px-2 py-1"
>
<span className="sr-only">Sign in with Google</span>
<FaGoogle className="w-5 h-5" />
<FaGoogle className="h-5 w-5" />
</button>
)}
{providers?.github && (
<button
onClick={() => handleProviderAuth(providers.github.id)}
className="justify-center flex-1 w-full px-2 py-1 btn btn-outline "
className="btn btn-outline w-full flex-1 justify-center px-2 py-1"
>
<span className="sr-only">Sign in with GitHub</span>
<FaGithub className="w-5 h-5" />
<FaGithub className="h-5 w-5" />
</button>
)}
{providers?.twitter && (
<button
onClick={() => handleProviderAuth(providers.twitter.id)}
className="justify-center flex-1 w-full px-2 py-1 btn btn-outline "
className="btn btn-outline w-full flex-1 justify-center px-2 py-1"
>
<span className="sr-only">Sign in with Twitter</span>
<FaTwitter className="w-5 h-5" />
<FaTwitter className="h-5 w-5" />
</button>
)}
</div>

28
src/lib/logger.ts Normal file
View File

@@ -0,0 +1,28 @@
import log from "loglevel";
import chalk from "chalk";
import prefix from "loglevel-plugin-prefix";
const colours = {
TRACE: chalk.magenta,
DEBUG: chalk.cyan,
INFO: chalk.blue,
WARN: chalk.yellow,
ERROR: chalk.red,
};
type ObjectKey = keyof typeof colours;
if (process.env.NODE_ENV == "development") {
log.setLevel("debug");
}
prefix.reg(log);
prefix.apply(log, {
format(level, name, timestamp) {
return `${chalk.gray(`[${timestamp.toISOString()}]`)} ${colours[
level.toUpperCase() as ObjectKey
](level)} ${chalk.green(`${name}:`)}`;
},
});
export { log as logger };

View File

@@ -1,5 +1,6 @@
import { postRouter } from "@/server/api/routers/post";
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
import { authRouter } from "./routers/auth";
/**
* This is the primary router for your server.
@@ -8,6 +9,7 @@ import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
*/
export const appRouter = createTRPCRouter({
post: postRouter,
auth: authRouter,
});
// export type definition of API

View File

@@ -4,7 +4,7 @@ import { users } from "@/server/db/schema";
export const authRouter = createTRPCRouter({
create: publicProcedure
.input(z.object({ email: z.string().email(), password: z.string().min(8) }))
.input(z.object({ email: z.string().email(), password: z.string().min(5) }))
.mutation(async ({ ctx, input }) => {
const user = await ctx.db.insert(users).values({
email: input.email,

View File

@@ -5,6 +5,7 @@ import {
protectedProcedure,
publicProcedure,
} from "@/server/api/trpc";
import { posts } from "@/server/db/schema";
export const postRouter = createTRPCRouter({

View File

@@ -6,9 +6,6 @@ import {
type NextAuthOptions,
} from "next-auth";
import { type Adapter } from "next-auth/adapters";
import { omit } from "lodash";
import { env } from "@/env";
import { db } from "@/server/db";
import {
accounts,
@@ -16,7 +13,8 @@ import {
users,
verificationTokens,
} from "@/server/db/schema";
import { confirmPassword } from "@/lib/crypt";
import { api } from "@/trpc/server";
import { env } from "@/env";
/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
@@ -71,6 +69,10 @@ export const authOptions: NextAuthOptions = {
if (!credentials) {
return null;
}
const result = await api.auth.login({
email: credentials.email,
password: credentials.password,
});
// const user = await prisma.users.findUnique({
// where: { email: credentials.email },
// select: {
@@ -92,6 +94,11 @@ export const authOptions: NextAuthOptions = {
},
}),
],
session: {
strategy: "jwt",
},
secret: env.NEXTAUTH_SECRET,
debug: env.NODE_ENV === "development",
pages: {
signIn: "/auth/signin",
signOut: "/auth/signout",

View File

@@ -11,13 +11,7 @@ import {
} from "drizzle-orm/pg-core";
import { type AdapterAccount } from "next-auth/adapters";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
*
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
*/
export const createTable = pgTableCreator((name) => `opengifame_${name}`);
export const createTable = pgTableCreator((name) => `${name}`);
export const posts = createTable(
"post",