mirror of
https://github.com/fergalmoran/opengifame.git
synced 2026-01-06 17:04:49 +00:00
T3 added
This commit is contained in:
5
.env
5
.env
@@ -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
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"workbench.colorTheme": "SynthWave '84"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
import PageLayout from './PageLayout';
|
||||
|
||||
export { PageLayout };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -8,5 +8,5 @@ export default {
|
||||
dbCredentials: {
|
||||
url: env.DATABASE_URL,
|
||||
},
|
||||
tablesFilter: ["opengifame_*"],
|
||||
tablesFilter: ["*"],
|
||||
} satisfies Config;
|
||||
|
||||
67
drizzle/0000_dazzling_cerebro.sql
Normal file
67
drizzle/0000_dazzling_cerebro.sql
Normal 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");
|
||||
@@ -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");
|
||||
@@ -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"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1725317790880,
|
||||
"tag": "0000_sleepy_whizzer",
|
||||
"when": 1725454391060,
|
||||
"tag": "0000_dazzling_cerebro",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
45
lib/slug.ts
45
lib/slug.ts
@@ -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
2777
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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")
|
||||
);
|
||||
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Gif" ALTER COLUMN "description" DROP NOT NULL,
|
||||
ALTER COLUMN "searchTerms" DROP NOT NULL;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "password" TEXT;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Gif" ADD COLUMN "publicVisible" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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";
|
||||
@@ -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)[];
|
||||
@@ -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")
|
||||
);
|
||||
@@ -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;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "oauth_token" TEXT,
|
||||
ADD COLUMN "oauth_token_secret" TEXT;
|
||||
@@ -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");
|
||||
@@ -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
|
||||
}
|
||||
101
prisma/seed.ts
101
prisma/seed.ts
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
125
src/app/(site)/auth/signup/page.tsx
Normal file
125
src/app/(site)/auth/signup/page.tsx
Normal 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;
|
||||
9
src/app/(site)/layout.tsx
Normal file
9
src/app/(site)/layout.tsx
Normal 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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
73
src/components/forms/auth/RegistrationForm.tsx
Normal file
73
src/components/forms/auth/RegistrationForm.tsx
Normal 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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
28
src/lib/logger.ts
Normal 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 };
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
|
||||
import { posts } from "@/server/db/schema";
|
||||
|
||||
export const postRouter = createTRPCRouter({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user