Think we're close to done with auth

This commit is contained in:
Fergal Moran
2023-05-12 21:49:01 +01:00
parent 1e22a33577
commit 59f0e4a26d
34 changed files with 2437 additions and 848 deletions

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

1
.gitignore vendored
View File

@@ -41,3 +41,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
.vscode/
.idea/

View File

@@ -7,12 +7,8 @@ await import("./src/env.mjs");
/** @type {import("next").NextConfig} */
const config = {
reactStrictMode: true,
experimental: {
appDir: true,
images: {
domains: ["cloudflare-ipfs.com"],
},
// i18n: {
// locales: ["en"],
// defaultLocale: "en",
// },
};
export default config;

View File

@@ -5,52 +5,60 @@
"scripts": {
"build": "next build",
"dev": "next dev",
"ssl": "NODE_ENV=development node ./server.js",
"postinstall": "prisma generate",
"lint": "next lint",
"start": "next start"
},
"dependencies": {
"@next-auth/prisma-adapter": "^1.0.5",
"next-auth": "^4.21.0",
"@prisma/client": "^4.13.0",
"@t3-oss/env-nextjs": "^0.2.1",
"@tanstack/react-query": "^4.28.0",
"@trpc/client": "^10.18.0",
"@trpc/next": "^10.18.0",
"@trpc/react-query": "^10.18.0",
"@trpc/server": "^10.18.0",
"@headlessui/react": "^1.7.14",
"@next-auth/prisma-adapter": "^1.0.6",
"@prisma/client": "^4.14.0",
"@t3-oss/env-nextjs": "^0.2.2",
"@tanstack/react-query": "^4.29.5",
"@trpc/client": "^10.25.1",
"@trpc/next": "^10.25.1",
"@trpc/react-query": "^10.25.1",
"@trpc/server": "^10.25.1",
"argon2": "^0.30.3",
"classnames": "^2.3.2",
"flowbite-react": "^0.4.4",
"install": "^0.13.0",
"next": "13.3.4",
"next": "13.4.1",
"next-auth": "^4.22.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hot-toast": "^2.4.1",
"react-icons": "^4.8.0",
"superjson": "1.12.2",
"superjson": "1.12.3",
"yup": "^1.1.1",
"zod": "^3.21.4"
},
"devDependencies": {
"@types/eslint": "^8.21.3",
"@types/node": "^18.15.5",
"@faker-js/faker": "^8.0.0",
"@types/eslint": "^8.37.0",
"@types/node": "^18.16.7",
"@types/prettier": "^2.7.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"autoprefixer": "^10.4.14",
"eslint": "^8.36.0",
"eslint-config-next": "^13.2.4",
"eslint": "^8.40.0",
"eslint-config-next": "^13.4.1",
"formik": "^2.2.9",
"postcss": "^8.4.21",
"prettier": "^2.8.6",
"prettier-plugin-tailwindcss": "^0.2.6",
"prisma": "^4.13.0",
"postcss": "^8.4.23",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.2.8",
"prisma": "^4.14.0",
"react-dropzone": "^14.2.3",
"tailwindcss": "^3.3.0",
"typescript": "^5.0.2",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.4",
"uuid": "^9.0.0"
},
"ct3aMetadata": {
"initVersion": "7.12.2"
}
},
"packageManager": "yarn@1.22.19"
}

1456
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -52,6 +52,8 @@ model Session {
model User {
id String @id @default(cuid())
username String?
password String?
name String?
email String? @unique
emailVerified DateTime?

BIN
public/default-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

23
server.js Normal file
View File

@@ -0,0 +1,23 @@
const https = require("https");
const fs = require("fs");
const next = require("next");
const port = 3000;
const dev = process.env.NODE_ENV !== "production";
const hostname = "mixyboos.dev.fergl.ie";
const app = next({ dev, hostname, port, dir: __dirname });
const handle = app.getRequestHandler();
const options = {
key: fs.readFileSync("/etc/letsencrypt/live/dev.fergl.ie/privkey.pem"),
cert: fs.readFileSync("/etc/letsencrypt/live/dev.fergl.ie/fullchain.pem"),
};
app.prepare().then(() => {
https
.createServer(options, (req, res) => handle(req, res))
.listen(port, (err) => {
if (err) throw err;
console.log(`> Ready on localhost:${port}`);
});
});

View File

@@ -0,0 +1,19 @@
"use client";
import Sidebar from "@/lib/components/layout/sidebar/Sidebar";
import Loading from "@/lib/components/widgets/Loading";
import { useSession } from "next-auth/react";
import React from "react";
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
const { data: session, status } = useSession();
if (!session) return <Loading />;
return (
<div className="mx-20 -mb-16 flex h-screen overflow-hidden bg-gray-50 pt-4 dark:bg-gray-900">
<Sidebar session={session} />
{children}
</div>
);
};
export default DashboardLayout;

View File

@@ -0,0 +1,18 @@
import PlayersComponent from "@/lib/components/stats/PlayersComponent";
import PlaysComponent from "@/lib/components/stats/PlaysComponent";
import React, { Component } from "react";
type DashboardPageProps = {
prop1: string;
};
const DashboardPage = ({ prop1 }: DashboardPageProps) => {
return (
<div className="grid gap-4 xl:grid-cols-2 2xl:grid-cols-3">
<PlaysComponent />
<PlayersComponent />
</div>
);
};
export default DashboardPage;

View File

@@ -0,0 +1,9 @@
import React from "react";
const LiveLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="mx-auto max-w-full overflow-hidden px-4 ">{children}</div>
);
};
export default LiveLayout;

View File

@@ -0,0 +1,11 @@
import React, { Component } from "react";
type LivePageProps = {
prop1: string;
};
const LivePage = ({ prop1 }: LivePageProps) => {
return <div>LivePage</div>;
};
export default LivePage;

View File

@@ -1,9 +1,18 @@
"use client";
import { api } from "@/lib/utils/api";
import { Flowbite } from "flowbite-react";
import { SessionProvider } from "next-auth/react";
import React from "react";
const Providers = ({ children }: { children: React.ReactNode }) => {
return <SessionProvider>{children}</SessionProvider>;
type ProvidersProps = {
children: React.ReactNode;
};
const Providers = ({ children }: ProvidersProps) => {
return (
<SessionProvider>
<Flowbite theme={{}}>{children}</Flowbite>
</SessionProvider>
);
};
export default Providers;
export default api.withTRPC(Providers);

24
src/app/auth/layout.tsx Normal file
View File

@@ -0,0 +1,24 @@
import React from "react";
import Image from "next/image";
const AuthLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="pt:mt-0 mx-auto -mt-16 flex flex-col items-center justify-center px-6 dark:bg-gray-900 md:h-screen">
<div className="flex items-center justify-center text-2xl font-semibold lg:mb-10">
<Image
src="/img/logo.svg"
className="mr-4 h-10 w-auto"
alt="Mixyboos Logo"
width={64}
height={64}
/>
<span className="self-center whitespace-nowrap text-2xl font-bold text-gray-900 dark:text-white">
MixyBoos Music Machine
</span>
</div>
{children}
</div>
);
};
export default AuthLayout;

241
src/app/auth/login/page.tsx Normal file
View File

@@ -0,0 +1,241 @@
"use client";
import React from "react";
import {
BsArrowReturnLeft,
BsFacebook,
BsGoogle,
BsTwitter,
} from "react-icons/bs";
import { signIn } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import { MdLogin } from "react-icons/md";
import * as Yup from "yup";
import { Formik } from "formik";
import { notice } from "@/lib/components/notifications/toast";
import Button from "@/lib/components/widgets/Button";
//https://github.com/jaredpalmer/formik/issues/3165
const AutofillSync = ({
values,
setValues,
}: {
values: { email: any; password: any };
setValues: any;
}) => {
React.useEffect(() => {
if (
(document.querySelector('input[name="email"]') as HTMLInputElement)
?.value ||
(document.querySelector('input[name="password"]') as HTMLInputElement)
?.value
) {
if (!values.email || !values.password) {
setValues({
email:
(document.querySelector('input[name="email"]') as HTMLInputElement)
?.value || "",
password:
(
document.querySelector(
'input[name="password"]'
) as HTMLInputElement
)?.value || "",
});
}
}
}, []);
return null;
};
const LoginPage = () => {
const [loginError, setLoginError] = React.useState(false);
const searchParams = useSearchParams();
const router = useRouter();
const schema = Yup.object().shape({
email: Yup.string()
.required("Email is a required field")
.email("Invalid email format"),
password: Yup.string()
.required("Password is a required field")
.min(8, "Password must be at least 8 characters"),
});
const handleLogin = async (email: string, password: string) => {
try {
setLoginError(false);
const result = await signIn("credentials", {
email: email,
password,
callbackUrl:
searchParams?.get("callbackUrl") ||
searchParams?.get("returnUrl") ||
"/",
redirect: false,
});
if (result?.ok) {
router.push(searchParams?.get("returnUrl") || "/");
return;
}
} catch (err) {
console.error("login", "handleLogin", err);
}
setLoginError(true);
};
return (
<div className="w-full max-w-xl space-y-8 rounded-lg bg-white p-6 shadow dark:bg-gray-800 sm:p-8">
<div className="space-y-8">
<div className="mt-6 grid grid-cols-3 gap-3">
<button
title="Sign in with Facebook"
onClick={() => {
notice("Warning", "Facebook login is not working yet");
// signIn('facebook')
}}
className="inline-flex w-full justify-center rounded-lg border border-gray-200 px-5 py-2.5 text-sm font-medium text-[#4267B2] shadow-sm hover:bg-gray-50"
>
<BsFacebook className="h-5 w-5" />
</button>
<button
title="Sign in with Google"
onClick={() => {
notice("Warning", "Google login is not working yet");
// signIn('google', {
// callbackUrl: `${window.location.origin}/`
// })
}}
className="inline-flex w-full justify-center rounded-lg border border-gray-200 px-5 py-2.5 text-sm font-medium text-[#DB4437] shadow-sm hover:bg-gray-50"
>
<BsGoogle className="h-5 w-5" />
</button>
<button
title="Sign in with Twitter"
onClick={() => {
notice("Warning", "Twitter login is not working yet");
// signIn('twitter')
}}
className="inline-flex w-full justify-center rounded-lg border border-gray-200 px-5 py-2.5 text-sm font-medium text-[#00acee] shadow-sm hover:bg-gray-50"
>
<BsTwitter className="h-5 w-5" />
</button>
</div>
<Formik
validationSchema={schema}
initialValues={{ email: "", password: "" }}
onSubmit={async (values) => {
const result = await handleLogin(values.email, values.password);
}}
>
{({
values,
setValues,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
}) => (
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<AutofillSync values={values} setValues={setValues} />
<div>
<label
htmlFor="email"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>
Your email
</label>
<input
id="email"
name="email"
type="email"
className="block w-full rounded-lg border border-gray-300 p-2.5 text-gray-900 focus:border-fuchsia-300 focus:ring-2 focus:ring-fuchsia-50 sm:text-sm"
placeholder="name@company.com"
onChange={handleChange}
/>
{errors.email && touched.email && (
<p className="mt-2 text-sm text-red-600 dark:text-red-500">
<span className="font-medium">Oops!&nbsp;</span>{" "}
{errors.email}
</p>
)}
</div>
<div>
<label
htmlFor="password"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>
Your password
</label>
<input
id="password"
name="password"
type="password"
placeholder="Password"
className="focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 sm:text-sm"
onChange={handleChange}
/>
{errors.password && touched.password && (
<p className="mt-2 text-sm text-red-600 dark:text-red-500">
<span className="font-medium">Oops!&nbsp;</span>
{errors.password}
</p>
)}
</div>
{loginError && (
<p className="mt-2 text-sm text-red-600 dark:text-red-500">
<span className="font-medium">Oops!&nbsp;</span>
Unable to log you in.
</p>
)}
<div className="flex items-start">
<div className="flex h-5 items-center">
<input
id="remember"
aria-describedby="remember"
name="remember"
type="checkbox"
className="checked:bg-dark-900 h-5 w-5 rounded border-gray-300 focus:outline-none focus:ring-0 "
/>
</div>
<div className="ml-3 text-sm">
<label
htmlFor="remember"
className="font-medium text-gray-900 dark:text-white"
>
Remember me
</label>
</div>
<Link
href="/auth/forgot"
className="ml-auto text-sm text-fuchsia-600 hover:underline"
>
Lost Password?
</Link>
</div>
<Button
id="login-button"
buttonStyle="fancy"
type="submit"
title="Login to your account"
icon={<MdLogin />}
></Button>
<div className="text-sm font-medium text-gray-500">
Not registered?
<Link
href="/auth/register"
className="ml-2 text-fuchsia-600 hover:underline"
>
Create account
</Link>
</div>
</form>
)}
</Formik>
</div>
</div>
);
};
export default LoginPage;

View File

@@ -0,0 +1,211 @@
"use client";
import Link from "next/link";
import React from "react";
import { MdLogin } from "react-icons/md";
import Button from "@/lib/components/widgets/Button";
import { BsFacebook, BsGoogle, BsTwitter } from "react-icons/bs";
import { notice } from "@/lib/components/notifications/toast";
import { Formik } from "formik";
import * as Yup from "yup";
import { api } from "@/lib/utils/api";
import { signIn } from "next-auth/react";
const RegisterPage = () => {
const register = api.auth.signUp.useMutation({
onSuccess: (result) => {
console.log("page", "register_success", result);
},
});
const schema = Yup.object().shape({
email: Yup.string()
.required("Email is a required field")
.email("Invalid email format"),
username: Yup.string()
.required("Username is a required field")
.max(20, "Username cannot be more than 20 characters"),
password: Yup.string()
.required("Password is a required field")
.min(8, "Password must be at least 8 characters"),
});
const handleRegister = async (
email: string,
username: string,
password: string
) => {
try {
const result = await register.mutateAsync({ email, username, password });
console.log("page", "handleRegister", result);
if (result?.status === 201) {
await signIn();
}
} catch (err) {
console.error("RegisterPage", "handleLogin", err);
}
};
return (
<div className="w-full max-w-xl space-y-8 rounded-lg bg-white p-6 shadow dark:bg-gray-800 sm:p-8">
<div className="space-y-8">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Create a new account
</h2>
<div className="mt-6 grid grid-cols-3 gap-3">
<button
title="Sign in with Facebook"
onClick={() => {
notice("Warning", "Facebook login is not working yet");
// signIn('facebook')
}}
className="inline-flex w-full justify-center rounded-lg border border-gray-200 px-5 py-2.5 text-sm font-medium text-[#4267B2] shadow-sm hover:bg-gray-50"
>
<BsFacebook className="h-5 w-5" />
</button>
<button
title="Sign in with Google"
onClick={() => {
notice("Warning", "Google login is not working yet");
// signIn('google', {
// callbackUrl: `${window.location.origin}/`
// })
}}
className="inline-flex w-full justify-center rounded-lg border border-gray-200 px-5 py-2.5 text-sm font-medium text-[#DB4437] shadow-sm hover:bg-gray-50"
>
<BsGoogle className="h-5 w-5" />
</button>
<button
title="Sign in with Twitter"
onClick={() => {
notice("Warning", "Twitter login is not working yet");
// signIn('twitter')
}}
className="inline-flex w-full justify-center rounded-lg border border-gray-200 px-5 py-2.5 text-sm font-medium text-[#00acee] shadow-sm hover:bg-gray-50"
>
<BsTwitter className="h-5 w-5" />
</button>
</div>
<Formik
validationSchema={schema}
initialValues={{ email: "", username: "", password: "" }}
onSubmit={async (values) => {
await handleRegister(
values.email,
values.username,
values.password
);
}}
>
{({
values,
setValues,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
}) => (
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div>
<label
htmlFor="email"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>
Your email
</label>
<input
id="email"
name="email"
type="email"
className="block w-full rounded-lg border border-gray-300 p-2.5 text-gray-900 focus:border-fuchsia-300 focus:ring-2 focus:ring-fuchsia-50 sm:text-sm"
placeholder="name@company.com"
onChange={handleChange}
/>
{errors.email && touched.email && (
<p className="mt-2 text-sm text-red-600 dark:text-red-500">
<span className="font-medium">Oops!</span> {errors.email}
</p>
)}
</div>
<div>
<label
htmlFor="username"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>
Your user name
</label>
<input
id="username"
name="username"
type="text"
className="block w-full rounded-lg border border-gray-300 p-2.5 text-gray-900 focus:border-fuchsia-300 focus:ring-2 focus:ring-fuchsia-50 sm:text-sm"
placeholder="Your name on the site"
onChange={handleChange}
/>
{errors.username && touched.username && (
<p className="mt-2 text-sm text-red-600 dark:text-red-500">
<span className="font-medium">Oops!</span> {errors.username}
</p>
)}
</div>
<div>
<label
htmlFor="password"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>
Your password
</label>
<input
id="password"
name="password"
type="password"
placeholder="Password"
className="focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 sm:text-sm"
onChange={handleChange}
/>
{errors.password && touched.password && (
<p className="mt-2 text-sm text-red-600 dark:text-red-500">
<span className="font-medium">Oops!</span>
{errors.password}
</p>
)}
</div>
<div>
<label
htmlFor="confirmpassword"
className="mb-2 block text-sm font-medium text-gray-900 dark:text-white"
>
Confirm password
</label>
<input
id="confirmpassword"
name="confirmpassword"
type="password"
placeholder="Confirm password"
className="focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 sm:text-sm"
onChange={handleChange}
/>
{errors.password && touched.password && (
<p className="mt-2 text-sm text-red-600 dark:text-red-500">
<span className="font-medium">Oops!</span>
{errors.password}
</p>
)}
</div>
<Button
id="login-button"
buttonStyle="fancy"
type="submit"
title="Create new account"
icon={<MdLogin />}
></Button>
</form>
)}
</Formik>
</div>
</div>
);
};
export default RegisterPage;

View File

@@ -1,25 +1,29 @@
import Navbar from "@/lib/components/layout/Navbar";
import { SessionProvider } from "next-auth/react";
import "./globals.css";
import Providers from "./Providers";
import { Raleway } from 'next/font/google';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const font = Raleway({
subsets: ["latin"],
display: "swap",
});
const RootLayout = ({ children }: { children: React.ReactNode }) => {
return (
<Providers>
<html lang="en">
<html lang="en" className={font.className}>
<Providers>
<body>
<Navbar />
<div className="pt-16">{children}</div>
<div className="bg-gray-50 pt-16 dark:bg-gray-900">{children}</div>
</body>
</html>
</Providers>
</Providers>
</html>
);
}
};
export const metadata = {
title: "Mixy::Boos",
description: "Robot Powered Mixes",
};
export default RootLayout;

View File

@@ -1,12 +1,18 @@
import HeroPage from "@/lib/components/pages/HeroPage";
import React from "react";
import { authOptions } from "@/server/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
const Home = () => {
const Home = async () => {
const session = await getServerSession(authOptions);
if (session) {
redirect("/dashboard");
}
return (
<main className="flex min-h-screen flex-col items-center justify-center dark:bg-slate-800">
<div>
<HeroPage />
</div>
{session ? <h1>Hello Sailor</h1> : <HeroPage />}
</main>
);
};

View File

@@ -20,6 +20,7 @@ export const env = createEnv({
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL ? z.string().min(1) : z.string().url()
),
JWT_SECRET: z.string(),
// Add `.min(1) on ID and SECRET if you want to make sure they're not empty
GOOGLE_CLIENT_ID: z.string(),
GOOGLE_CLIENT_SECRET: z.string(),
@@ -43,6 +44,7 @@ export const env = createEnv({
NODE_ENV: process.env.NODE_ENV,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
JWT_SECRET: process.env.JWT_SECRET,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
},

View File

@@ -2,7 +2,7 @@
import React from "react";
import { GiHamburgerMenu } from "react-icons/gi";
import { RiFindReplaceLine } from "react-icons/ri";
import { AiOutlineClose } from "react-icons/ai";
import { AiOutlineClose, AiOutlineLogin } from "react-icons/ai";
import { GoBroadcast } from "react-icons/go";
import { MdOutlineCloudUpload } from "react-icons/md";
import { BsSearch } from "react-icons/bs";
@@ -10,9 +10,34 @@ import Link from "next/link";
import Image from "next/image";
import NavLink from "../widgets/NavLink";
import { useSession } from "next-auth/react";
import ProfileDropdown from "@/lib/components/widgets/ProfileDropdown";
import { Session } from "next-auth";
import Loading from "../widgets/Loading";
const NavbarLogin = ({
session,
status,
}: {
session: Session | null;
status: "authenticated" | "loading" | "unauthenticated";
}) => {
if (status === "loading") return <Loading />;
return session ? (
<ProfileDropdown session={session} />
) : (
<NavLink
title="Login"
href="/auth/login"
icon={
<AiOutlineLogin className="text-cerise-800 leading-lg text-lg opacity-75 dark:text-slate-300" />
}
/>
);
};
const Navbar = () => {
const { data: sessionData } = useSession();
const { data: session, status } = useSession();
return (
<nav className="fixed z-30 w-full border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-slate-800">
<div className="px-3 py-3 lg:px-5 lg:pl-3">
@@ -91,8 +116,7 @@ const Navbar = () => {
{false && <AppsDropdownComponent />}
<ThemeToggler />
{session && <ProfileDropdown session={session} />} */}
{!sessionData?.user && <Link href="/auth/login">Login</Link>}
{!!sessionData?.user && <Link href="/auth/logout">Logout</Link>}
<NavbarLogin session={session} status={status} />
</div>
</div>
</div>

View File

@@ -1,19 +1,7 @@
"use client";
import React from "react";
import { Sidebar, TextInput, Avatar } from "flowbite-react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import classNames from "classnames";
import { UserModel } from "@lib/data/models";
import {
HiShoppingBag,
HiPencil,
HiLogin,
HiThumbUp,
HiOutlineDeviceMobile,
HiPhotograph,
HiPresentationChartBar,
} from "react-icons/hi";
import { FiTrendingUp } from "react-icons/fi";
import { BsPersonBoundingBox, BsPersonVcard } from "react-icons/bs";
import {
@@ -26,12 +14,13 @@ import {
import { CgSandClock } from "react-icons/cg";
import { BiCategoryAlt } from "react-icons/bi";
import Loading from "../../widgets/Loading";
import UserImage from "../../widgets/UserImage";
interface IIDashboardSidebarProps {
user: UserModel | undefined;
}
type DashboardSidebarProps = {
session: Session | undefined;
};
const DashboardSidebar = ({ user }: IIDashboardSidebarProps) => {
const DashboardSidebar = ({ session }: DashboardSidebarProps) => {
const router = useRouter();
const _sidebarItemClick = (path: string | undefined): void => {
if (!path) return;
@@ -40,24 +29,29 @@ const DashboardSidebar = ({ user }: IIDashboardSidebarProps) => {
router.push(path);
}
};
React.useEffect(() => {
console.log("Sidebar", "useEffect", session);
}, [session]);
return !user ? (
<Loading />
) : (
if (!session?.user) return <Loading />;
return (
<div className="h-full w-60 space-y-2 p-3 dark:bg-slate-900 dark:text-gray-100">
<div className="flex items-center space-x-4 p-2">
{user.profileImage && (
<UserImage src={user.profileImage} status={"offline"} size={"md"} />
{session.user.image && (
<UserImage src={session.user.image} status={"offline"} size={"md"} />
)}
<div>
<h2 className="text-lg font-semibold">{user.displayName}</h2>
<h2 className="text-lg font-semibold">
{session.user.name || "Argle Bargle"}
</h2>
<span className="flex items-center space-x-1">
<a
rel="noopener noreferrer"
href="#"
className="text-xs hover:underline dark:text-gray-400"
>
{user.biography}
{session.user.biography || "Hello Lover"}
</a>
</span>
</div>

View File

@@ -0,0 +1,38 @@
import toast from "react-hot-toast";
import { IoClose } from "react-icons/io5";
import { TiWarningOutline } from "react-icons/ti";
export const notice = (title: string, body: string) => {
toast.custom(
(t) => (
<div
id="toast-success"
className="mb-4 flex w-full max-w-xs items-center rounded-lg bg-white p-4 text-gray-500 shadow dark:bg-slate-800 dark:text-gray-400"
role="alert"
>
<div className="flex">
<div className="inline-flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-blue-100 text-blue-500 dark:bg-blue-900 dark:text-blue-300">
<TiWarningOutline className="h-5 w-5" fill="currentColor" />
</div>
<div className="ml-3 text-sm font-normal">
<span className="mb-1 text-sm font-semibold text-gray-900 dark:text-white">
{title}
</span>
<div className="mb-2 text-sm font-normal">{body}</div>
</div>
<button
type="button"
className="-mx-1.5 -my-1.5 ml-auto inline-flex h-8 w-8 rounded-lg bg-white p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-900 focus:ring-2 focus:ring-gray-300 dark:bg-slate-800 dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white"
data-dismiss-target="#toast-interactive"
aria-label="Close"
onClick={() => toast.dismiss(t.id)}
>
<span className="sr-only">Close</span>
<IoClose className="h-5 w-5" fill="currentColor" />
</button>
</div>
</div>
),
{ id: "unique-notification", position: "top-center", duration: 4000 }
);
};

View File

@@ -40,7 +40,7 @@ const testimonials = [
];
const HeroPage = () => {
return (
<div className="container mx-auto px-4 text-center lg:px-0 xl:px-32">
<div className="container mx-auto -mt-16 px-4 text-center lg:px-0 xl:px-32">
<h1 className="mb-3 text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-200 sm:text-5xl sm:leading-none md:tracking-wide">
Welcome to MixyBoos
</h1>

View File

@@ -0,0 +1,22 @@
import { Dropdown } from "flowbite-react";
const Datepicker: React.FC = () => {
return (
<span className="text-sm text-gray-600">
<Dropdown inline label="Last 7 days">
<Dropdown.Item>
<strong>Sep 16, 2021 - Sep 22, 2021</strong>
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item>Yesterday</Dropdown.Item>
<Dropdown.Item>Today</Dropdown.Item>
<Dropdown.Item>Last 7 days</Dropdown.Item>
<Dropdown.Item>Last 30 days</Dropdown.Item>
<Dropdown.Item>Last 90 days</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item>Custom...</Dropdown.Item>
</Dropdown>
</span>
);
};
export default Datepicker;

View File

@@ -0,0 +1,574 @@
import React from "react";
const PlayersComponent = () => {
return (
<div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
<h3 className="mb-4 flex items-center text-lg font-semibold text-gray-900 dark:text-white">
Statistics this month
<button
data-popover-target="popover-description"
data-popover-placement="bottom-end"
type="button"
>
<svg
className="ml-2 h-4 w-4 text-gray-400 hover:text-gray-500"
aria-hidden="true"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z"
clipRule="evenodd"
/>
</svg>
<span className="sr-only">Show information</span>
</button>
</h3>
<div
data-popover=""
id="popover-description"
role="tooltip"
className="invisible absolute z-10 inline-block w-72 rounded-lg border border-gray-200 bg-white text-sm font-light text-gray-500 opacity-0 shadow-sm transition-opacity duration-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
style={{
position: "absolute",
inset: "0px 0px auto auto",
margin: 0,
transform: "translate3d(-320.5px, 81px, 0px)",
}}
data-popper-placement="bottom-end"
>
<div className="space-y-2 p-3">
<h3 className="font-semibold text-gray-900 dark:text-white">
Statistics
</h3>
<p>
Statistics is a branch of applied mathematics that involves the
collection, description, analysis, and inference of conclusions from
quantitative data.
</p>
<a
href="#"
className="text-primary-600 dark:text-primary-500 dark:hover:text-primary-600 hover:text-primary-700 flex items-center font-medium"
>
Read more{" "}
<svg
className="ml-1 h-4 w-4"
aria-hidden="true"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</a>
</div>
<div
data-popper-arrow=""
style={{
position: "absolute",
left: 0,
transform: "translate3d(271px, 0px, 0px)",
}}
/>
</div>
<div className="sm:hidden">
<label htmlFor="tabs" className="sr-only">
Select tab
</label>
<select
id="tabs"
className="focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500 block w-full rounded-t-lg border-0 border-b border-gray-200 bg-gray-50 p-2.5 text-sm text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
>
<option>Statistics</option>
<option>Services</option>
<option>FAQ</option>
</select>
</div>
<ul
className="hidden divide-x divide-gray-200 rounded-lg text-center text-sm font-medium text-gray-500 dark:divide-gray-600 dark:text-gray-400 sm:flex"
id="fullWidthTab"
data-tabs-toggle="#fullWidthTabContent"
role="tablist"
>
<li className="w-full">
<button
id="faq-tab"
data-tabs-target="#faq"
type="button"
role="tab"
aria-controls="faq"
aria-selected="true"
className="inline-block w-full rounded-tl-lg border-blue-600 bg-gray-50 p-4 text-blue-600 hover:bg-gray-100 hover:text-blue-600 focus:outline-none dark:border-blue-500 dark:bg-gray-700 dark:text-blue-500 dark:hover:bg-gray-600 dark:hover:text-blue-500"
>
Top products
</button>
</li>
<li className="w-full">
<button
id="about-tab"
data-tabs-target="#about"
type="button"
role="tab"
aria-controls="about"
aria-selected="false"
className="inline-block w-full rounded-tr-lg border-gray-100 bg-gray-50 p-4 text-gray-500 hover:border-gray-300 hover:bg-gray-100 hover:text-gray-600 focus:outline-none dark:border-gray-700 dark:border-transparent dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-gray-300"
>
Top Customers
</button>
</li>
</ul>
<div
id="fullWidthTabContent"
className="border-t border-gray-200 dark:border-gray-600"
>
<div
className="pt-4"
id="faq"
role="tabpanel"
aria-labelledby="faq-tab"
>
<ul
role="list"
className="divide-y divide-gray-200 dark:divide-gray-700"
>
<li className="py-3 sm:py-4">
<div className="flex items-center justify-between">
<div className="flex min-w-0 items-center">
<img
className="h-10 w-10 flex-shrink-0"
src="https://flowbite-admin-dashboard.vercel.app/images/products/iphone.png"
alt="imac image"
/>
<div className="ml-3">
<p className="truncate font-medium text-gray-900 dark:text-white">
iPhone 14 Pro
</p>
<div className="flex flex-1 items-center justify-end text-sm text-green-500 dark:text-green-400">
<svg
className="h-4 w-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
clipRule="evenodd"
fillRule="evenodd"
d="M10 17a.75.75 0 01-.75-.75V5.612L5.29 9.77a.75.75 0 01-1.08-1.04l5.25-5.5a.75.75 0 011.08 0l5.25 5.5a.75.75 0 11-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0110 17z"
/>
</svg>
2.5%
<span className="ml-2 text-gray-500">vs last month</span>
</div>
</div>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
$445,467
</div>
</div>
</li>
<li className="py-3 sm:py-4">
<div className="flex items-center justify-between">
<div className="flex min-w-0 items-center">
<img
className="h-10 w-10 flex-shrink-0"
src="https://flowbite-admin-dashboard.vercel.app/images/products/imac.png"
alt="imac image"
/>
<div className="ml-3">
<p className="truncate font-medium text-gray-900 dark:text-white">
Apple iMac 27"
</p>
<div className="flex flex-1 items-center justify-end text-sm text-green-500 dark:text-green-400">
<svg
className="h-4 w-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
clipRule="evenodd"
fillRule="evenodd"
d="M10 17a.75.75 0 01-.75-.75V5.612L5.29 9.77a.75.75 0 01-1.08-1.04l5.25-5.5a.75.75 0 011.08 0l5.25 5.5a.75.75 0 11-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0110 17z"
/>
</svg>
12.5%
<span className="ml-2 text-gray-500">vs last month</span>
</div>
</div>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
$256,982
</div>
</div>
</li>
<li className="py-3 sm:py-4">
<div className="flex items-center justify-between">
<div className="flex min-w-0 items-center">
<img
className="h-10 w-10 flex-shrink-0"
src="https://flowbite-admin-dashboard.vercel.app/images/products/watch.png"
alt="watch image"
/>
<div className="ml-3">
<p className="truncate font-medium text-gray-900 dark:text-white">
Apple Watch SE
</p>
<div className="flex flex-1 items-center justify-end text-sm text-red-600 dark:text-red-500">
<svg
className="h-4 w-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
clipRule="evenodd"
fillRule="evenodd"
d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
/>
</svg>
1.35%
<span className="ml-2 text-gray-500">vs last month</span>
</div>
</div>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
$201,869
</div>
</div>
</li>
<li className="py-3 sm:py-4">
<div className="flex items-center justify-between">
<div className="flex min-w-0 items-center">
<img
className="h-10 w-10 flex-shrink-0"
src="https://flowbite-admin-dashboard.vercel.app/images/products/ipad.png"
alt="ipad image"
/>
<div className="ml-3">
<p className="truncate font-medium text-gray-900 dark:text-white">
Apple iPad Air
</p>
<div className="flex flex-1 items-center justify-end text-sm text-green-500 dark:text-green-400">
<svg
className="h-4 w-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
clipRule="evenodd"
fillRule="evenodd"
d="M10 17a.75.75 0 01-.75-.75V5.612L5.29 9.77a.75.75 0 01-1.08-1.04l5.25-5.5a.75.75 0 011.08 0l5.25 5.5a.75.75 0 11-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0110 17z"
/>
</svg>
12.5%
<span className="ml-2 text-gray-500">vs last month</span>
</div>
</div>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
$103,967
</div>
</div>
</li>
<li className="py-3 sm:py-4">
<div className="flex items-center justify-between">
<div className="flex min-w-0 items-center">
<img
className="h-10 w-10 flex-shrink-0"
src="https://flowbite-admin-dashboard.vercel.app/images/products/imac.png"
alt="imac image"
/>
<div className="ml-3">
<p className="truncate font-medium text-gray-900 dark:text-white">
Apple iMac 24"
</p>
<div className="flex flex-1 items-center justify-end text-sm text-red-600 dark:text-red-500">
<svg
className="h-4 w-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
clipRule="evenodd"
fillRule="evenodd"
d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
/>
</svg>
2%
<span className="ml-2 text-gray-500">vs last month</span>
</div>
</div>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
$98,543
</div>
</div>
</li>
</ul>
</div>
<div
className="hidden pt-4"
id="about"
role="tabpanel"
aria-labelledby="about-tab"
>
<ul
role="list"
className="divide-y divide-gray-200 dark:divide-gray-700"
>
<li className="py-3 sm:py-4">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<img
className="h-8 w-8 rounded-full"
src="https://flowbite-admin-dashboard.vercel.app/images/users/neil-sims.png"
alt="Neil image"
/>
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-gray-900 dark:text-white">
Neil Sims
</p>
<p className="truncate text-sm text-gray-500 dark:text-gray-400">
email@flowbite.com
</p>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
$3320
</div>
</div>
</li>
<li className="py-3 sm:py-4">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<img
className="h-8 w-8 rounded-full"
src="https://flowbite-admin-dashboard.vercel.app/images/users/bonnie-green.png"
alt="Neil image"
/>
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-gray-900 dark:text-white">
Bonnie Green
</p>
<p className="truncate text-sm text-gray-500 dark:text-gray-400">
email@flowbite.com
</p>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
$2467
</div>
</div>
</li>
<li className="py-3 sm:py-4">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<img
className="h-8 w-8 rounded-full"
src="https://flowbite-admin-dashboard.vercel.app/images/users/michael-gough.png"
alt="Neil image"
/>
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-gray-900 dark:text-white">
Michael Gough
</p>
<p className="truncate text-sm text-gray-500 dark:text-gray-400">
email@flowbite.com
</p>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
$2235
</div>
</div>
</li>
<li className="py-3 sm:py-4">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<img
className="h-8 w-8 rounded-full"
src="https://flowbite-admin-dashboard.vercel.app/images/users/thomas-lean.png"
alt="Neil image"
/>
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-gray-900 dark:text-white">
Thomes Lean
</p>
<p className="truncate text-sm text-gray-500 dark:text-gray-400">
email@flowbite.com
</p>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
$1842
</div>
</div>
</li>
<li className="py-3 sm:py-4">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<img
className="h-8 w-8 rounded-full"
src="https://flowbite-admin-dashboard.vercel.app/images/users/lana-byrd.png"
alt="Neil image"
/>
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-gray-900 dark:text-white">
Lana Byrd
</p>
<p className="truncate text-sm text-gray-500 dark:text-gray-400">
email@flowbite.com
</p>
</div>
<div className="inline-flex items-center text-base font-semibold text-gray-900 dark:text-white">
$1044
</div>
</div>
</li>
</ul>
</div>
</div>
{/* Card Footer */}
<div className="mt-5 flex items-center justify-between border-t border-gray-200 pt-3 dark:border-gray-700 sm:pt-6">
<div>
<button
className="inline-flex items-center rounded-lg p-2 text-center text-sm font-medium text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
type="button"
data-dropdown-toggle="stats-dropdown"
>
Last 7 days{" "}
<svg
className="ml-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{/* Dropdown menu */}
<div
className="z-50 my-4 hidden list-none divide-y divide-gray-100 rounded bg-white text-base shadow dark:divide-gray-600 dark:bg-gray-700"
id="stats-dropdown"
style={{
position: "absolute",
inset: "0px auto auto 0px",
margin: 0,
transform: "translate3d(940px, 701px, 0px)",
}}
data-popper-placement="bottom"
>
<div className="px-4 py-3" role="none">
<p
className="truncate text-sm font-medium text-gray-900 dark:text-white"
role="none"
>
Sep 16, 2021 - Sep 22, 2021
</p>
</div>
<ul className="py-1" role="none">
<li>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem"
>
Yesterday
</a>
</li>
<li>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem"
>
Today
</a>
</li>
<li>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem"
>
Last 7 days
</a>
</li>
<li>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem"
>
Last 30 days
</a>
</li>
<li>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem"
>
Last 90 days
</a>
</li>
</ul>
<div className="py-1" role="none">
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem"
>
Custom...
</a>
</div>
</div>
</div>
<div className="flex-shrink-0">
<a
href="#"
className="text-primary-700 dark:text-primary-500 inline-flex items-center rounded-lg p-2 text-xs font-medium uppercase hover:bg-gray-100 dark:hover:bg-gray-700 sm:text-sm"
>
Full Report
<svg
className="ml-1 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</a>
</div>
</div>
</div>
);
};
export default PlayersComponent;

View File

@@ -0,0 +1,64 @@
"use client";
import React from "react";
import PlaysGraph from "./PlaysGraph";
import Datepicker from "./Datepicker";
const PlaysComponent = () => {
return (
<div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6 2xl:col-span-2">
<div className="mb-4 flex items-center justify-between">
<div className="shrink-0">
<span className="text-2xl font-bold leading-none text-gray-900 dark:text-white sm:text-3xl">
$45,385
</span>
<h3 className="text-base font-normal text-gray-600 dark:text-gray-400">
Sales this week
</h3>
</div>
<div className="flex flex-1 items-center justify-end text-base font-bold text-green-600 dark:text-green-400">
12.5%
<svg
className="h-5 w-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M5.293 7.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L6.707 7.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</div>
</div>
<PlaysGraph />
<div className="mt-5 flex items-center justify-between border-t border-gray-200 pt-3 dark:border-gray-700 sm:pt-6">
<Datepicker />
<div className="shrink-0">
<a
href="#"
className="text-primary-700 dark:text-primary-500 inline-flex items-center rounded-lg p-2 text-xs font-medium uppercase hover:bg-gray-100 dark:hover:bg-gray-700 sm:text-sm"
>
Sales Report
<svg
className="ml-1 h-4 w-4 sm:h-5 sm:w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</a>
</div>
</div>
</div>
);
};
export default PlaysComponent;

View File

@@ -0,0 +1,7 @@
import React from "react";
const PlaysGraph = () => {
return <div>PlaysGraph</div>;
};
export default PlaysGraph;

View File

@@ -0,0 +1,55 @@
import React, { ReactElement } from "react";
const ButtonStyle = {
primary:
"text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800 inline-flex items-center",
secondary:
"text-white bg-gradient-to-br from-slate-400 to-slate-600 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 text-center inline-flex items-center shadow-sm shadow-gray-200 hover:scale-[1.02] transition-transform",
basic:
"text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-slate-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700",
fancy:
"text-white bg-gradient-to-br from-pink-500 to-orange-400 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-pink-200 dark:focus:ring-pink-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center mr-3",
delete:
"text-white bg-gradient-to-br from-red-400 to-red-600 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 text-center inline-flex items-center shadow-sm shadow-gray-200 hover:scale-[1.02] transition-transform",
icon: "text-pink-700 border border-pink-500 hover:bg-pink-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-md text-center inline-flex items-center mr-2 dark:border-pink-500 dark:text-pink-500 dark:hover:text-white dark:focus:ring-pink-800",
};
const ButtonSize = {
sm: "py-1 px-4 text-sm",
md: "py-2 px-4 text-md",
lg: "py-3 px-6 text-lg",
};
interface IButtonProps extends React.ComponentPropsWithoutRef<"button"> {
title: string;
buttonSize?: "sm" | "md" | "lg";
buttonStyle?: "icon" | "primary" | "secondary" | "basic" | "fancy" | "delete";
buttonType?: "submit" | "reset" | "button";
showText?: boolean;
icon?: ReactElement;
}
const Button = ({
title,
buttonSize = "md",
buttonStyle = "primary",
buttonType = "button",
showText = true,
icon,
...rest
}: IButtonProps) => {
const classNames = `${ButtonStyle[buttonStyle ?? "basic"]} ${
ButtonSize[buttonSize]
} inline-flex items-center rounded transition-all duration-150 ease-linear shadow outline-none focus:ring-0 focus:border-1`;
return (
<button type={buttonType ?? "button"} className={classNames} {...rest}>
<>
{icon && <span className={showText ? "mr-1" : ""}>{icon}</span>}
{showText && <span>{title}</span>}
</>
</button>
);
};
export { ButtonStyle, ButtonSize };
export default Button;

View File

@@ -6,7 +6,7 @@ interface ILoadingProps {
const Loading = ({ message = "" }: ILoadingProps) => {
return (
<>
<div role="status" className="mr-3 flex flex-row space-x-1">
<div role="status" className="flex flex-row space-x-1">
<svg
aria-hidden="true"
className="mr-2 h-5 w-5 animate-spin fill-blue-600 text-gray-200 dark:text-gray-600"

View File

@@ -0,0 +1,126 @@
import React from "react";
import type { Session } from "next-auth";
import { Menu, Transition } from "@headlessui/react";
import UserImage from "@/lib/components/widgets/UserImage";
import Link from "next/link";
import {
MdOutlineEditNote,
MdOutlineLogout,
MdSpaceDashboard,
} from "react-icons/md";
import { signOut } from "next-auth/react";
type ProfileDropdownProps = {
session: Session;
};
const ProfileDropdown = ({ session }: ProfileDropdownProps) => {
if (!session) return null;
return (
<Menu as="div" className="relative ml-3">
{({ open }) => (
<div className="ml-3">
<div>
<Menu.Button
type="button"
className="bg-cerise-800 focus:ring-cerise-300 flex rounded-full text-sm focus:ring-4"
>
<span className="sr-only">Open user menu</span>
<UserImage
src={session.user.image || "/img/default-avatar.png"}
size={"sm"}
/>
</Menu.Button>
</div>
<Transition
show={open}
as={React.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
static
className="absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<Menu.Item>
{({ active }) => (
<div className="px-4 py-3" role="none">
<p className="text-sm" role="none">
<Link href={`/${session.user.slug}`}>
{session.user.displayName}
</Link>
</p>
<p
className="text-cerise-900 truncate text-sm font-medium"
role="none"
>
{session.user.email}
</p>
</div>
)}
</Menu.Item>
<div className="py-1">
<Menu.Item>
{({ active }) => (
<Link
href="/dashboard"
className={`${
active ? "bg-violet-500 text-white" : "text-cerise-900"
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<MdSpaceDashboard
className="mr-2 h-5 w-5"
aria-hidden="true"
/>
Dashboard
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<Link
href="/profile/edit"
className={`${
active ? "bg-violet-500 text-white" : "text-cerise-900"
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<MdOutlineEditNote
className="mr-2 h-5 w-5"
aria-hidden="true"
/>
Edit Profile
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
onClick={async () =>
(await signOut({ callbackUrl: "/" })) as void
}
className={`${
active ? "bg-violet-500 text-white" : "text-cerise-900"
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<MdOutlineLogout
className="mr-2 h-5 w-5"
aria-hidden="true"
/>
Sign out
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</div>
)}
</Menu>
);
};
export default ProfileDropdown;

View File

@@ -1,8 +1,10 @@
import { createTRPCRouter } from "@/server/api/trpc";
import { mixRouter } from "@/server/api/routers/mix";
import { authRouter } from "@/server/api/routers/auth";
export const appRouter = createTRPCRouter({
mix: mixRouter,
auth: authRouter,
});
// export type definition of API

View File

@@ -0,0 +1,51 @@
import { createTRPCRouter, publicProcedure } from "@/server/api/trpc";
import { faker } from "@faker-js/faker";
import * as trpc from "@trpc/server";
import { hash } from "argon2";
import { z } from "zod";
export const authRouter = createTRPCRouter({
signUp: publicProcedure
.input(
z.object({
email: z.string(),
username: z.string(),
password: z.string(),
})
)
.mutation(async ({ input: { email, username, password }, ctx }) => {
const exists = await ctx.prisma.user.findFirst({
where: { email },
});
if (exists) {
throw new trpc.TRPCError({
code: "CONFLICT",
message: "User already exists.",
});
}
const hashedPassword = await hash(password);
const result = await ctx.prisma.user.create({
data: {
username,
email,
password: hashedPassword,
image: faker.image.avatar(),
},
});
return {
status: 201,
message: "Account created successfully",
result: result.email,
};
}),
getAll: publicProcedure.query(({ ctx }) => {
const mixes = ctx.prisma.mix.findMany({
take: 10,
orderBy: [{ createdAt: "desc" }],
});
return mixes;
}),
});

View File

@@ -2,42 +2,35 @@ import { type GetServerSidePropsContext } from "next";
import {
getServerSession,
type NextAuthOptions,
type DefaultSession,
} from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { env } from "@/env.mjs";
import { prisma } from "@/server/db";
import { verify } from "argon2";
declare module "next-auth" {
interface Session extends DefaultSession {
user: {
id: string;
// ...other properties
// role: UserRole;
} & DefaultSession["user"];
}
// interface User {
// // ...other properties
// // role: UserRole;
// }
}
/**
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
*
* @see https://next-auth.js.org/configuration/options
*/
export const authOptions: NextAuthOptions = {
session: {
strategy: "jwt",
},
callbacks: {
session: ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
},
}),
session: ({ session, token }) => {
console.log("auth", "session", session, token);
if (token) {
session.id = token.id as string;
}
return session;
},
jwt: ({ token, user }) => {
if (user) {
token.id = user.id;
token.email = user.email;
}
return token;
},
},
adapter: PrismaAdapter(prisma),
providers: [
@@ -45,7 +38,58 @@ export const authOptions: NextAuthOptions = {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
}),
CredentialsProvider({
name: "Credentials",
credentials: {
email: {
label: "Email",
type: "email",
placeholder: "email@domain.com",
},
password: { label: "Password", type: "password" },
},
authorize: async (credentials) => {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await prisma.user.findFirst({
where: { email: credentials.email },
});
if (!user || !user.password) {
return null;
}
const isValidPassword = await verify(
user.password,
credentials.password
);
if (!isValidPassword) {
return null;
}
return {
id: user.id,
email: user.email,
username: user.username,
image: user.image,
};
},
}),
],
jwt: {
secret: env.JWT_SECRET,
maxAge: 15 * 24 * 30 * 60, // 15 days
},
pages: {
signIn: "/auth/login",
signOut: "/auth/login",
error: "/auth/error", // Error code passed in query string as ?error=
verifyRequest: "/auth/verify-request", // (used for check email message)
newUser: "/auth/register",
},
};
/**

15
types/next-auth.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
id: string;
user: {
id: string;
};
}
interface User {
// ...other properties
// role: UserRole;
}
}