mirror of
https://github.com/fergalmoran/mixyboos.git
synced 2025-12-22 09:41:39 +00:00
Think we're close to done with auth
This commit is contained in:
13
.editorconfig
Normal file
13
.editorconfig
Normal 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
1
.gitignore
vendored
@@ -41,3 +41,4 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
@@ -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;
|
||||
|
||||
60
package.json
60
package.json
@@ -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
1456
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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
BIN
public/default-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
23
server.js
Normal file
23
server.js
Normal 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}`);
|
||||
});
|
||||
});
|
||||
19
src/app/(user)/dashboard/layout.tsx
Normal file
19
src/app/(user)/dashboard/layout.tsx
Normal 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;
|
||||
18
src/app/(user)/dashboard/page.tsx
Normal file
18
src/app/(user)/dashboard/page.tsx
Normal 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;
|
||||
9
src/app/(user)/live/layout.tsx
Normal file
9
src/app/(user)/live/layout.tsx
Normal 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;
|
||||
11
src/app/(user)/live/page.tsx
Normal file
11
src/app/(user)/live/page.tsx
Normal 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;
|
||||
@@ -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
24
src/app/auth/layout.tsx
Normal 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
241
src/app/auth/login/page.tsx
Normal 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! </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! </span>
|
||||
{errors.password}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{loginError && (
|
||||
<p className="mt-2 text-sm text-red-600 dark:text-red-500">
|
||||
<span className="font-medium">Oops! </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;
|
||||
211
src/app/auth/register/page.tsx
Normal file
211
src/app/auth/register/page.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
38
src/lib/components/notifications/toast.tsx
Normal file
38
src/lib/components/notifications/toast.tsx
Normal 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 }
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
22
src/lib/components/stats/Datepicker.tsx
Normal file
22
src/lib/components/stats/Datepicker.tsx
Normal 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;
|
||||
574
src/lib/components/stats/PlayersComponent.tsx
Normal file
574
src/lib/components/stats/PlayersComponent.tsx
Normal 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;
|
||||
64
src/lib/components/stats/PlaysComponent.tsx
Normal file
64
src/lib/components/stats/PlaysComponent.tsx
Normal 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;
|
||||
7
src/lib/components/stats/PlaysGraph.tsx
Normal file
7
src/lib/components/stats/PlaysGraph.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const PlaysGraph = () => {
|
||||
return <div>PlaysGraph</div>;
|
||||
};
|
||||
|
||||
export default PlaysGraph;
|
||||
55
src/lib/components/widgets/Button.tsx
Normal file
55
src/lib/components/widgets/Button.tsx
Normal 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;
|
||||
@@ -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"
|
||||
|
||||
126
src/lib/components/widgets/ProfileDropdown.tsx
Normal file
126
src/lib/components/widgets/ProfileDropdown.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
51
src/server/api/routers/auth.ts
Normal file
51
src/server/api/routers/auth.ts
Normal 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;
|
||||
}),
|
||||
});
|
||||
@@ -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
15
types/next-auth.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user