newFeature/rebuild-in-js #15
23
Documentation/docs/How To/Background + image change.md
Normal file
23
Documentation/docs/How To/Background + image change.md
Normal file
File diff suppressed because one or more lines are too long
16
Documentation/docs/How To/Business Analyst Sample.md
Normal file
16
Documentation/docs/How To/Business Analyst Sample.md
Normal file
File diff suppressed because one or more lines are too long
17
Documentation/docs/How To/Email Setup.md
Normal file
17
Documentation/docs/How To/Email Setup.md
Normal file
File diff suppressed because one or more lines are too long
64
Documentation/docs/How To/Fly Deploy.md
Normal file
64
Documentation/docs/How To/Fly Deploy.md
Normal file
File diff suppressed because one or more lines are too long
22
Documentation/docs/How To/Frontend to Backend.md
Normal file
22
Documentation/docs/How To/Frontend to Backend.md
Normal file
File diff suppressed because one or more lines are too long
14
Documentation/docs/How To/Pocketbase.md
Normal file
14
Documentation/docs/How To/Pocketbase.md
Normal file
File diff suppressed because one or more lines are too long
18
Documentation/docs/How To/Posthog Analytics.md
Normal file
18
Documentation/docs/How To/Posthog Analytics.md
Normal file
File diff suppressed because one or more lines are too long
20
Documentation/docs/How To/SC Components.md
Normal file
20
Documentation/docs/How To/SC Components.md
Normal file
File diff suppressed because one or more lines are too long
69
Documentation/docs/How To/SC Fonts.md
Normal file
69
Documentation/docs/How To/SC Fonts.md
Normal file
File diff suppressed because one or more lines are too long
13
Documentation/docs/How To/SC Gitea.md
Normal file
13
Documentation/docs/How To/SC Gitea.md
Normal file
File diff suppressed because one or more lines are too long
23
Documentation/docs/How To/SC Mailersend.md
Normal file
23
Documentation/docs/How To/SC Mailersend.md
Normal file
File diff suppressed because one or more lines are too long
19
Documentation/docs/How To/SC Pocketbase.md
Normal file
19
Documentation/docs/How To/SC Pocketbase.md
Normal file
File diff suppressed because one or more lines are too long
16
Documentation/docs/How To/SC SEO.md
Normal file
16
Documentation/docs/How To/SC SEO.md
Normal file
File diff suppressed because one or more lines are too long
26
Documentation/docs/How To/SC Your App.md
Normal file
26
Documentation/docs/How To/SC Your App.md
Normal file
File diff suppressed because one or more lines are too long
20
Documentation/docs/How To/Stripe Branding Setup.md
Normal file
20
Documentation/docs/How To/Stripe Branding Setup.md
Normal file
File diff suppressed because one or more lines are too long
19
Documentation/docs/How To/Stripe Manual Add.md
Normal file
19
Documentation/docs/How To/Stripe Manual Add.md
Normal file
File diff suppressed because one or more lines are too long
40
Documentation/docs/How To/Stripe Restricted.md
Normal file
40
Documentation/docs/How To/Stripe Restricted.md
Normal file
File diff suppressed because one or more lines are too long
21
Documentation/docs/How To/Stripe Setup.md
Normal file
21
Documentation/docs/How To/Stripe Setup.md
Normal file
File diff suppressed because one or more lines are too long
27
Documentation/docs/How To/Stripe fixtures.md
Normal file
27
Documentation/docs/How To/Stripe fixtures.md
Normal file
File diff suppressed because one or more lines are too long
29
Documentation/docs/How To/Theme Change.md
Normal file
29
Documentation/docs/How To/Theme Change.md
Normal file
File diff suppressed because one or more lines are too long
1
Documentation/docs/How To/domain.md
Normal file
1
Documentation/docs/How To/domain.md
Normal file
File diff suppressed because one or more lines are too long
34
Documentation/docs/How To/logo.md
Normal file
34
Documentation/docs/How To/logo.md
Normal file
File diff suppressed because one or more lines are too long
41
Documentation/docs/NEW/Row I/Background + image change.md
Normal file
41
Documentation/docs/NEW/Row I/Background + image change.md
Normal file
File diff suppressed because one or more lines are too long
36
Documentation/docs/NEW/Row I/Business Analyst Sample.md
Normal file
36
Documentation/docs/NEW/Row I/Business Analyst Sample.md
Normal file
File diff suppressed because one or more lines are too long
31
Documentation/docs/NEW/Row I/Email Setup.md
Normal file
31
Documentation/docs/NEW/Row I/Email Setup.md
Normal file
File diff suppressed because one or more lines are too long
64
Documentation/docs/NEW/Row I/Fly Deploy.md
Normal file
64
Documentation/docs/NEW/Row I/Fly Deploy.md
Normal file
File diff suppressed because one or more lines are too long
1
Documentation/docs/NEW/Row I/domain.md
Normal file
1
Documentation/docs/NEW/Row I/domain.md
Normal file
File diff suppressed because one or more lines are too long
22
Documentation/docs/NEW/Row II/Frontend to Backend.md
Normal file
22
Documentation/docs/NEW/Row II/Frontend to Backend.md
Normal file
File diff suppressed because one or more lines are too long
34
Documentation/docs/NEW/Row II/Pocketbase.md
Normal file
34
Documentation/docs/NEW/Row II/Pocketbase.md
Normal file
File diff suppressed because one or more lines are too long
18
Documentation/docs/NEW/Row II/Posthog Analytics.md
Normal file
18
Documentation/docs/NEW/Row II/Posthog Analytics.md
Normal file
File diff suppressed because one or more lines are too long
38
Documentation/docs/NEW/Row II/SC Components.md
Normal file
38
Documentation/docs/NEW/Row II/SC Components.md
Normal file
File diff suppressed because one or more lines are too long
52
Documentation/docs/NEW/Row II/logo.md
Normal file
52
Documentation/docs/NEW/Row II/logo.md
Normal file
File diff suppressed because one or more lines are too long
69
Documentation/docs/NEW/Row III/SC Fonts.md
Normal file
69
Documentation/docs/NEW/Row III/SC Fonts.md
Normal file
File diff suppressed because one or more lines are too long
99
Documentation/docs/NEW/Row III/SC Gitea.md
Normal file
99
Documentation/docs/NEW/Row III/SC Gitea.md
Normal file
File diff suppressed because one or more lines are too long
54
Documentation/docs/NEW/Row III/SC Mailersend.md
Normal file
54
Documentation/docs/NEW/Row III/SC Mailersend.md
Normal file
File diff suppressed because one or more lines are too long
19
Documentation/docs/NEW/Row III/SC Pocketbase.md
Normal file
19
Documentation/docs/NEW/Row III/SC Pocketbase.md
Normal file
File diff suppressed because one or more lines are too long
26
Documentation/docs/NEW/Row III/SC Your App.md
Normal file
26
Documentation/docs/NEW/Row III/SC Your App.md
Normal file
File diff suppressed because one or more lines are too long
16
Documentation/docs/NEW/Row IV/SC SEO.md
Normal file
16
Documentation/docs/NEW/Row IV/SC SEO.md
Normal file
File diff suppressed because one or more lines are too long
20
Documentation/docs/NEW/Row IV/Stripe Branding Setup.md
Normal file
20
Documentation/docs/NEW/Row IV/Stripe Branding Setup.md
Normal file
File diff suppressed because one or more lines are too long
19
Documentation/docs/NEW/Row IV/Stripe Manual Add.md
Normal file
19
Documentation/docs/NEW/Row IV/Stripe Manual Add.md
Normal file
File diff suppressed because one or more lines are too long
40
Documentation/docs/NEW/Row IV/Stripe Restricted.md
Normal file
40
Documentation/docs/NEW/Row IV/Stripe Restricted.md
Normal file
File diff suppressed because one or more lines are too long
27
Documentation/docs/NEW/Row IV/Stripe fixtures.md
Normal file
27
Documentation/docs/NEW/Row IV/Stripe fixtures.md
Normal file
File diff suppressed because one or more lines are too long
21
Documentation/docs/NEW/Row V/Stripe Setup.md
Normal file
21
Documentation/docs/NEW/Row V/Stripe Setup.md
Normal file
File diff suppressed because one or more lines are too long
29
Documentation/docs/NEW/Row V/Theme Change.md
Normal file
29
Documentation/docs/NEW/Row V/Theme Change.md
Normal file
File diff suppressed because one or more lines are too long
8400
Documentation/yarn.lock
Normal file
8400
Documentation/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
36
Frontend/app/(authenticated)/account/page.jsx
Normal file
36
Frontend/app/(authenticated)/account/page.jsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from "react"
|
||||
import PageHeader from "@/sections/PageHeader"
|
||||
import { cookies } from "next/headers"
|
||||
import { getUserFromCookie } from "@/lib/auth"
|
||||
import AccountContent from "@/sections/AccountContent"
|
||||
import { redirect } from "next/navigation"
|
||||
import pb from "@/lib/pocketbase"
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import Footer from "@/components/Footer"
|
||||
import Spacer from "@/components/Utilities/Spacer"
|
||||
|
||||
export default async function AccountPage() {
|
||||
const user = await getUserFromCookie(cookies())
|
||||
const cookie = cookies().get("pb_auth")
|
||||
//server side
|
||||
pb.authStore.loadFromCookie(cookie?.value || "")
|
||||
!pb.authStore.isValid && redirect("/")
|
||||
return (
|
||||
user && (
|
||||
<main
|
||||
id="content"
|
||||
role="main"
|
||||
className="h-full flex flex-col min-h-screen mx-auto w-screen overflow-hidden bg-base-100"
|
||||
>
|
||||
<Background className="min-h-screen flex">
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<PageHeader title="Account" subtitle={<></>} />
|
||||
{/* <AccountContent user={user} /> */}
|
||||
<Spacer className="mt-auto mb-auto" />
|
||||
<Footer />
|
||||
</div>
|
||||
</Background>
|
||||
</main>
|
||||
)
|
||||
)
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import React from "react";
|
||||
import PageHeader from "@/sections/PageHeader";
|
||||
import { cookies } from "next/headers";
|
||||
import { getUserFromCookie } from "@/lib/auth";
|
||||
import { User } from "@/types";
|
||||
import AccountContent from "@/sections/AccountContent";
|
||||
import { redirect } from "next/navigation";
|
||||
import pb from "@/lib/pocketbase";
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import Footer from "@/components/Footer";
|
||||
import Spacer from "@/components/Utilities/Spacer";
|
||||
|
||||
export default async function AccountPage() {
|
||||
const user = (await getUserFromCookie(cookies())) as User;
|
||||
const cookie = cookies().get("pb_auth");
|
||||
//server side
|
||||
pb.authStore.loadFromCookie(cookie?.value || "");
|
||||
!pb.authStore.isValid && redirect("/");
|
||||
return (
|
||||
user && (
|
||||
<main
|
||||
id="content"
|
||||
role="main"
|
||||
className="h-full flex flex-col min-h-screen mx-auto w-screen overflow-hidden bg-base-100"
|
||||
>
|
||||
<Background className="min-h-screen flex">
|
||||
<div className="min-h-screen flex flex-col">
|
||||
|
||||
<PageHeader title="Account" subtitle={<></>} />
|
||||
<AccountContent user={user} />
|
||||
<Spacer className="mt-auto mb-auto" />
|
||||
<Footer />
|
||||
</div>
|
||||
</Background>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import { changeEmailValidationSchema } from "@/utils/form"
|
||||
import { toast } from "react-toastify"
|
||||
import PocketBase from "pocketbase"
|
||||
import PageWrapper from "@/components/Utilities/PageWrapper"
|
||||
import { usePathname } from "next/navigation"
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import PageHeader from "@/sections/PageHeader"
|
||||
|
||||
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL)
|
||||
|
||||
export default function ConfirmEmailChangePage() {
|
||||
const pathName = usePathname()
|
||||
const token = pathName.split("/").at(-1)
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
reset
|
||||
} = useForm({
|
||||
resolver: yupResolver(changeEmailValidationSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async data => {
|
||||
try {
|
||||
await pb.collection("user").confirmEmailChange(token ?? "", data.password)
|
||||
reset()
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error("There was a problem. Please try change your email again", {
|
||||
position: "bottom-left",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Background>
|
||||
<div className="h-screen w-screen flex items-center flex-col">
|
||||
<PageHeader
|
||||
title={"Enter Your Password To Change Your Email"}
|
||||
subtitle={<></>}
|
||||
/>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full max-w-xl px-4"
|
||||
>
|
||||
<div className="relative mt-6">
|
||||
<input
|
||||
type="password"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Password…"
|
||||
aria-label="Password…"
|
||||
autoComplete="on"
|
||||
{...register("password")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.password?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row w-full justify-between">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className={isSubmitting ? "btn btn-gray" : "btn btn-primary"}
|
||||
>
|
||||
Change Email
|
||||
{isSubmitting && <div className="loading"></div>}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Background>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { changeEmailValidationSchema } from "@/utils/form";
|
||||
import { toast } from "react-toastify";
|
||||
import PocketBase from 'pocketbase';
|
||||
import PageWrapper from "@/components/Utilities/PageWrapper";
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import PageHeader from "@/sections/PageHeader";
|
||||
|
||||
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL as string);
|
||||
|
||||
export default function ConfirmEmailChangePage() {
|
||||
const pathName = usePathname();
|
||||
const token = pathName.split('/').at(-1);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
reset
|
||||
} = useForm({
|
||||
resolver: yupResolver(changeEmailValidationSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
await pb.collection('user').confirmEmailChange(
|
||||
token ?? "",
|
||||
data.password,
|
||||
);
|
||||
reset();
|
||||
document.getElementById("sign-in-modal")?.click();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error("There was a problem. Please try change your email again", {
|
||||
position: "bottom-left",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Background>
|
||||
<div className="h-screen w-screen flex items-center flex-col">
|
||||
<PageHeader
|
||||
title={"Enter Your Password To Change Your Email"}
|
||||
subtitle={<></>}
|
||||
/>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full max-w-xl px-4">
|
||||
<div className="relative mt-6">
|
||||
<input
|
||||
type="password"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Password…"
|
||||
aria-label="Password…"
|
||||
autoComplete="on"
|
||||
{...register("password")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.password?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row w-full justify-between">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className={isSubmitting ? "btn btn-gray": "btn btn-primary"}
|
||||
>
|
||||
Change Email
|
||||
{isSubmitting && <div className="loading"></div>}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Background>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import { passwordValidationSchema } from "@/utils/form"
|
||||
import { toast } from "react-toastify"
|
||||
import PocketBase from "pocketbase"
|
||||
import PageWrapper from "@/components/Utilities/PageWrapper"
|
||||
import { usePathname } from "next/navigation"
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import PageHeader from "@/sections/PageHeader"
|
||||
|
||||
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL)
|
||||
|
||||
export default function ConfirmPasswordResetPage() {
|
||||
const pathName = usePathname()
|
||||
const token = pathName.split("/").at(-1)
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
reset
|
||||
} = useForm({
|
||||
resolver: yupResolver(passwordValidationSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async data => {
|
||||
try {
|
||||
await pb
|
||||
.collection("user")
|
||||
.confirmPasswordReset(
|
||||
token ?? "",
|
||||
data.newPassword,
|
||||
data.newPasswordConfirm
|
||||
)
|
||||
reset()
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(
|
||||
"There was a problem. Please try reset your password again",
|
||||
{
|
||||
position: "bottom-left",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Background>
|
||||
<div className="h-screen w-screen flex items-center flex-col">
|
||||
<PageHeader title={"Enter Your New Password"} subtitle={<></>} />
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full max-w-xl px-4"
|
||||
>
|
||||
<div className="relative mt-6">
|
||||
<input
|
||||
type="password"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="New password…"
|
||||
aria-label="New password…"
|
||||
autoComplete="on"
|
||||
{...register("newPassword")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.newPassword?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-6">
|
||||
<input
|
||||
type="password"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Confirm new password…"
|
||||
aria-label="Confirm new password…"
|
||||
autoComplete="on"
|
||||
{...register("newPasswordConfirm")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.newPasswordConfirm?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row w-full justify-between">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className={isSubmitting ? "btn btn-gray" : "btn btn-primary"}
|
||||
>
|
||||
Reset Password
|
||||
{isSubmitting && <div className="loading"></div>}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Background>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { passwordValidationSchema } from "@/utils/form";
|
||||
import { toast } from "react-toastify";
|
||||
import PocketBase from 'pocketbase';
|
||||
import PageWrapper from "@/components/Utilities/PageWrapper";
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import PageHeader from "@/sections/PageHeader";
|
||||
|
||||
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL as string);
|
||||
|
||||
export default function ConfirmPasswordResetPage() {
|
||||
const pathName = usePathname();
|
||||
const token = pathName.split('/').at(-1);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
reset
|
||||
} = useForm({
|
||||
resolver: yupResolver(passwordValidationSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
await pb.collection('user').confirmPasswordReset(
|
||||
token ?? "",
|
||||
data.newPassword,
|
||||
data.newPasswordConfirm,
|
||||
);
|
||||
reset()
|
||||
document.getElementById("sign-in-modal")?.click();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error("There was a problem. Please try reset your password again", {
|
||||
position: "bottom-left",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Background>
|
||||
<div className="h-screen w-screen flex items-center flex-col">
|
||||
<PageHeader
|
||||
title={"Enter Your New Password"}
|
||||
subtitle={<></>}
|
||||
/>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full max-w-xl px-4">
|
||||
<div className="relative mt-6">
|
||||
<input
|
||||
type="password"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="New password…"
|
||||
aria-label="New password…"
|
||||
autoComplete="on"
|
||||
{...register("newPassword")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.newPassword?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-6">
|
||||
<input
|
||||
type="password"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Confirm new password…"
|
||||
aria-label="Confirm new password…"
|
||||
autoComplete="on"
|
||||
{...register("newPasswordConfirm")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.newPasswordConfirm?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row w-full justify-between">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className={isSubmitting ? "btn btn-gray": "btn btn-primary"}
|
||||
>
|
||||
Reset Password
|
||||
{isSubmitting && <div className="loading"></div>}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Background>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import { passwordValidationSchema } from "@/utils/form"
|
||||
import { toast } from "react-toastify"
|
||||
import PocketBase from "pocketbase"
|
||||
import PageWrapper from "@/components/Utilities/PageWrapper"
|
||||
import { usePathname } from "next/navigation"
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import PageHeader from "@/sections/PageHeader"
|
||||
|
||||
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL)
|
||||
|
||||
export default function ConfirmPasswordResetPage() {
|
||||
const pathName = usePathname()
|
||||
const token = pathName.split("/").at(-1)
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
reset
|
||||
} = useForm({
|
||||
resolver: yupResolver(passwordValidationSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async data => {
|
||||
try {
|
||||
await pb
|
||||
.collection("user")
|
||||
.confirmPasswordReset(
|
||||
token ?? "",
|
||||
data.newPassword,
|
||||
data.newPasswordConfirm
|
||||
)
|
||||
reset()
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(
|
||||
"There was a problem. Please try reset your password again",
|
||||
{
|
||||
position: "bottom-left",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Background>
|
||||
<div className="h-screen w-screen flex items-center flex-col">
|
||||
<PageHeader title={"Enter Your New Password"} subtitle={<></>} />
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full max-w-xl px-4"
|
||||
>
|
||||
<div className="relative mt-6">
|
||||
<input
|
||||
type="password"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="New password…"
|
||||
aria-label="New password…"
|
||||
autoComplete="on"
|
||||
{...register("newPassword")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.newPassword?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-6">
|
||||
<input
|
||||
type="password"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Confirm new password…"
|
||||
aria-label="Confirm new password…"
|
||||
autoComplete="on"
|
||||
{...register("newPasswordConfirm")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.newPasswordConfirm?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row w-full justify-between">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className={isSubmitting ? "btn btn-gray" : "btn btn-primary"}
|
||||
>
|
||||
Reset Password
|
||||
{isSubmitting && <div className="loading"></div>}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Background>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import PocketBase from 'pocketbase';
|
||||
import PageWrapper from "@/components/Utilities/PageWrapper";
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import PageHeader from "@/sections/PageHeader";
|
||||
|
||||
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL as string);
|
||||
|
||||
export default function ConfirmVerification() {
|
||||
const pathName = usePathname();
|
||||
const router = useRouter();
|
||||
const token = pathName.split('/').at(-1);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
await pb.collection('user').confirmVerification(
|
||||
token ?? ""
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error("There was a problem. Please try confirm your email again", {
|
||||
position: "bottom-left",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
});
|
||||
}
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const handleSignInRedirect = () => {
|
||||
document.getElementById("sign-in-modal")?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Background>
|
||||
<div className="h-screen w-screen flex items-center flex-col">
|
||||
<PageHeader
|
||||
title={"Your Email Was Verified"}
|
||||
subtitle={<></>}
|
||||
/>
|
||||
<button onClick={handleSignInRedirect} className="btn btn-primary mt-4">
|
||||
Go to Sign In
|
||||
</button>
|
||||
</div>
|
||||
</Background>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import PageHeader from "@/sections/PageHeader";
|
||||
import React from "react";
|
||||
import Footer from "@/components/Footer";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import PageHeader from "@/sections/PageHeader"
|
||||
import React from "react"
|
||||
import Footer from "@/components/Footer"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
export default async function About() {
|
||||
return (
|
||||
@ -15,20 +15,45 @@ export default async function About() {
|
||||
<PageHeader title="Helping Developers Build" />
|
||||
<div className="max-w-4xl mx-auto mb-auto pb-24 h-full w-full py-12 px-8 flex flex-col gap-y-12 text-center items-center">
|
||||
<p className="text-lg text-base-content font-thin">
|
||||
In 2023 I built <Link href={"https://sign365.com.au"} className="font-bold text-secondary">Sign365</Link> using pocketbase
|
||||
and setup an open source library to help people get setup with <Link href={"https://github.com/mrwyndham/pocketbase-stripe"} className="font-bold text-secondary">Stripe
|
||||
+ Pocketbase</Link>. As 2024 has come around I have had more and more
|
||||
requests for applications and features on the existing code. I built a
|
||||
codebase that would save me 20 hours + in the bootstrapping time to
|
||||
get my applications ready. That is why I had to build <Link href={"https://fastpocket.dev"} className="font-bold text-secondary">FastPocket</Link> an
|
||||
easy solution to give everyone a head start.
|
||||
In 2023 I built{" "}
|
||||
<Link
|
||||
href={"https://sign365.com.au"}
|
||||
className="font-bold text-secondary"
|
||||
>
|
||||
Sign365
|
||||
</Link>{" "}
|
||||
using pocketbase and setup an open source library to help people get
|
||||
setup with{" "}
|
||||
<Link
|
||||
href={"https://github.com/mrwyndham/pocketbase-stripe"}
|
||||
className="font-bold text-secondary"
|
||||
>
|
||||
Stripe + Pocketbase
|
||||
</Link>
|
||||
. As 2024 has come around I have had more and more requests for
|
||||
applications and features on the existing code. I built a codebase
|
||||
that would save me 20 hours + in the bootstrapping time to get my
|
||||
applications ready. That is why I had to build{" "}
|
||||
<Link
|
||||
href={"https://fastpocket.dev"}
|
||||
className="font-bold text-secondary"
|
||||
>
|
||||
FastPocket
|
||||
</Link>{" "}
|
||||
an easy solution to give everyone a head start.
|
||||
</p>
|
||||
<p className="text-lg text-base-content font-thin">
|
||||
After reflecting on the challenges that developers face building their
|
||||
applications, I've seen that there will never be one codebase
|
||||
that suits all developers. But I am sure for those who are
|
||||
opensourcing, self-hosting and want to spin up an application quickly
|
||||
they will be able to do it using <Link href={"https://fastpocket.dev"} className="font-bold text-secondary">FastPocket</Link>
|
||||
they will be able to do it using{" "}
|
||||
<Link
|
||||
href={"https://fastpocket.dev"}
|
||||
className="font-bold text-secondary"
|
||||
>
|
||||
FastPocket
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-lg text-base-content font-thin">
|
||||
I am taking all of the knowledge that I have gained across 20+
|
||||
@ -38,11 +63,23 @@ export default async function About() {
|
||||
that have been battle tested in enterprise code
|
||||
</p>
|
||||
<p className="text-lg text-base-content font-thin">
|
||||
So I've committed to building <Link href={"https://fastpocket.dev"} className="font-bold text-secondary">FastPocket</Link> to help you build your
|
||||
projects faster to get paid.
|
||||
So I've committed to building{" "}
|
||||
<Link
|
||||
href={"https://fastpocket.dev"}
|
||||
className="font-bold text-secondary"
|
||||
>
|
||||
FastPocket
|
||||
</Link>{" "}
|
||||
to help you build your projects faster to get paid.
|
||||
</p>
|
||||
<p className="text-xl text-base-content font-bold">
|
||||
<Link href={"https://fastpocket.dev"} className="font-bold text-secondary">FastPocket</Link> will get you producing more with less development.
|
||||
<Link
|
||||
href={"https://fastpocket.dev"}
|
||||
className="font-bold text-secondary"
|
||||
>
|
||||
FastPocket
|
||||
</Link>{" "}
|
||||
will get you producing more with less development.
|
||||
</p>
|
||||
<p className="text-lg text-base-content font-thin">
|
||||
I know that your next project will grow exponentially because of the
|
||||
@ -69,5 +106,5 @@ export default async function About() {
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
@ -1,32 +1,28 @@
|
||||
import BlogContent from "@/sections/BlogContent";
|
||||
import getPostMetadata from "@/utils/getPostMetaData";
|
||||
import { headers } from "next/headers";
|
||||
import React from "react";
|
||||
import Spacer from "@/components/Utilities/Spacer";
|
||||
import pb from "@/lib/pocketbase";
|
||||
import Footer from "@/components/Footer";
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import BlogContent from "@/sections/BlogContent"
|
||||
import getPostMetadata from "@/utils/getPostMetaData"
|
||||
import { headers } from "next/headers"
|
||||
import React from "react"
|
||||
import Spacer from "@/components/Utilities/Spacer"
|
||||
import pb from "@/lib/pocketbase"
|
||||
import Footer from "@/components/Footer"
|
||||
import Background from "@/components/Utilities/Background"
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { slug: string };
|
||||
}) {
|
||||
const { slug } = params;
|
||||
const headersList = headers();
|
||||
const siteURL = headersList.get("host");
|
||||
pb.autoCancellation(false);
|
||||
export async function generateMetadata({ params }) {
|
||||
const { slug } = params
|
||||
const headersList = headers()
|
||||
const siteURL = headersList.get("host")
|
||||
pb.autoCancellation(false)
|
||||
const post = await pb.collection("blog").getFirstListItem(`slug="${slug}"`, {
|
||||
requestKey: "metaData",
|
||||
});
|
||||
pb.autoCancellation(true);
|
||||
requestKey: "metaData"
|
||||
})
|
||||
pb.autoCancellation(true)
|
||||
|
||||
return {
|
||||
title: `${post.title} | FastPocket`,
|
||||
authors: [
|
||||
{
|
||||
name: post.author || "Samuel Wyndham",
|
||||
},
|
||||
name: post.author || "Samuel Wyndham"
|
||||
}
|
||||
],
|
||||
description: post.description,
|
||||
keywords: post.keywords,
|
||||
@ -44,9 +40,9 @@ export async function generateMetadata({
|
||||
width: 1024,
|
||||
height: 576,
|
||||
alt: post.title,
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
type: "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
@ -59,32 +55,32 @@ export async function generateMetadata({
|
||||
url: `${post.imageUrl}`,
|
||||
width: 1024,
|
||||
height: 576,
|
||||
alt: post.title,
|
||||
},
|
||||
],
|
||||
alt: post.title
|
||||
}
|
||||
]
|
||||
},
|
||||
alternates: {
|
||||
canonical: `https://${siteURL}/blogs/${post.slug}`,
|
||||
},
|
||||
};
|
||||
canonical: `https://${siteURL}/blogs/${post.slug}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const posts = await getPostMetadata();
|
||||
console.log("static posts", posts.length);
|
||||
const mappedPosts = posts.map((post) => ({
|
||||
slug: post.slug,
|
||||
}));
|
||||
return mappedPosts;
|
||||
};
|
||||
const posts = await getPostMetadata()
|
||||
console.log("static posts", posts.length)
|
||||
const mappedPosts = posts.map(post => ({
|
||||
slug: post.slug
|
||||
}))
|
||||
return mappedPosts
|
||||
}
|
||||
|
||||
const PostPage = async (props: any) => {
|
||||
console.log("params", props.params);
|
||||
const PostPage = async props => {
|
||||
console.log("params", props.params)
|
||||
const post = await pb
|
||||
.collection("blog")
|
||||
.getFirstListItem(`slug="` + props.params.slug + `"`, {
|
||||
requestKey: "post",
|
||||
});
|
||||
requestKey: "post"
|
||||
})
|
||||
return (
|
||||
<main
|
||||
id="content"
|
||||
@ -99,7 +95,7 @@ const PostPage = async (props: any) => {
|
||||
<Footer />
|
||||
</Background>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default PostPage;
|
||||
export default PostPage
|
@ -1,17 +1,17 @@
|
||||
import PageHeader from "@/sections/PageHeader";
|
||||
import BlogCard from "@/components/BlogCard";
|
||||
import getPostMetadata from "@/utils/getPostMetaData";
|
||||
import React from "react";
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHeader from "@/sections/PageHeader"
|
||||
import BlogCard from "@/components/BlogCard"
|
||||
import getPostMetadata from "@/utils/getPostMetaData"
|
||||
import React from "react"
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import Footer from "@/components/Footer"
|
||||
|
||||
export default async function BlogsPage() {
|
||||
const postMetadata = await getPostMetadata();
|
||||
const postMetadata = await getPostMetadata()
|
||||
|
||||
console.log("blogs", postMetadata.length);
|
||||
console.log("blogs", postMetadata.length)
|
||||
const postPreviews = postMetadata
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
.map((post) => <BlogCard key={post.slug} {...(post as any)} />);
|
||||
.map(post => <BlogCard key={post.slug} {...post} />)
|
||||
return (
|
||||
<main
|
||||
id="content"
|
||||
@ -40,5 +40,5 @@ export default async function BlogsPage() {
|
||||
<Footer />
|
||||
</Background>
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import Footer from "@/components/Footer";
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import Spacer from "@/components/Utilities/Spacer";
|
||||
import FormLeftDescriptionRightContactUs from "@/sections/ContactUs/FormLeftDescriptionRightContactUs";
|
||||
import PageHeader from "@/sections/PageHeader";
|
||||
import React from "react";
|
||||
import Footer from "@/components/Footer"
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import Spacer from "@/components/Utilities/Spacer"
|
||||
import FormLeftDescriptionRightContactUs from "@/sections/ContactUs/FormLeftDescriptionRightContactUs"
|
||||
import PageHeader from "@/sections/PageHeader"
|
||||
import React from "react"
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
@ -25,7 +25,7 @@ const page = () => {
|
||||
<Footer />
|
||||
</Background>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default page;
|
||||
export default page
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
function layout({ children }: { children: React.ReactNode }) {
|
||||
function layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
51
Frontend/app/(public)/pricing/action.js
Normal file
51
Frontend/app/(public)/pricing/action.js
Normal file
@ -0,0 +1,51 @@
|
||||
"use server"
|
||||
import pb from "@/lib/pocketbase"
|
||||
|
||||
export async function apiPrices() {
|
||||
try {
|
||||
const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL
|
||||
if (!pocketbaseUrl) {
|
||||
throw Error("Connection Timeout")
|
||||
}
|
||||
const productRequest = await fetch(
|
||||
`${pocketbaseUrl}/api/collections/product/records?filter=(active=true)`,
|
||||
{
|
||||
cache: "no-cache",
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
const productResponse = await productRequest.json()
|
||||
console.log("app/pricing/action", "productResponse", productResponse)
|
||||
const unsortedProducts = productResponse.items
|
||||
console.log("app/pricing/action", "unsortedProducts", unsortedProducts)
|
||||
const prices = await pb.collection("price").getFullList()
|
||||
console.log("app/pricing/action", "prices", prices)
|
||||
for (const product of unsortedProducts) {
|
||||
product.metadata.benefits = product?.metadata?.benefits
|
||||
? JSON.parse(product.metadata.benefits)
|
||||
: []
|
||||
const pricesOfProduct = prices.filter(
|
||||
price => price.product_id === product.product_id
|
||||
)
|
||||
for (const priceOfProduct of pricesOfProduct) {
|
||||
product.type = priceOfProduct.type
|
||||
if (priceOfProduct.interval === "year") {
|
||||
product.yearlyPrice = priceOfProduct
|
||||
} else {
|
||||
product.monthlyPrice = priceOfProduct
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sortedProducts = unsortedProducts.sort(
|
||||
(a, b) => a.product_order - b.product_order
|
||||
)
|
||||
return sortedProducts
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return []
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { Product, Price } from "@/types";
|
||||
import pb from "@/lib/pocketbase";
|
||||
|
||||
export async function apiPrices() {
|
||||
try {
|
||||
const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL;
|
||||
if (!pocketbaseUrl) {
|
||||
throw Error('Connection Timeout');
|
||||
}
|
||||
const productRequest = await fetch(
|
||||
`${pocketbaseUrl}/api/collections/product/records?filter=(active=true)`,
|
||||
{
|
||||
cache: 'no-cache',
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
const productResponse = await productRequest.json();
|
||||
console.log("app/pricing/actions","productResponse",productResponse)
|
||||
const unsortedProducts: Product[] = productResponse.items;
|
||||
console.log("app/pricing/actions","unsortedProducts",unsortedProducts)
|
||||
const prices = await pb.collection("price").getFullList<Price>();
|
||||
console.log("app/pricing/actions","prices",prices)
|
||||
for (const product of unsortedProducts) {
|
||||
product.metadata.benefits = product?.metadata?.benefits ? JSON.parse(product.metadata.benefits as any) : [];
|
||||
const pricesOfProduct = prices.filter(price => price.product_id === product.product_id);
|
||||
for (const priceOfProduct of pricesOfProduct){
|
||||
product.type = priceOfProduct.type;
|
||||
if (priceOfProduct.interval === "year"){
|
||||
product.yearlyPrice = priceOfProduct;
|
||||
} else {
|
||||
product.monthlyPrice = priceOfProduct;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sortedProducts = unsortedProducts.sort((a: Product, b: Product) => a.product_order - b.product_order)
|
||||
return sortedProducts;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import React from "react";
|
||||
import PageHeader from "@/sections/PageHeader";
|
||||
import Newsletter from "@/sections/Newsletter/Newsletter";
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import Footer from "@/components/Footer";
|
||||
import Payment from "@/sections/Payment";
|
||||
import Spacer from "@/components/Utilities/Spacer";
|
||||
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature";
|
||||
import React from "react"
|
||||
import PageHeader from "@/sections/PageHeader"
|
||||
import Payment from "@/sections/Payment"
|
||||
import Newsletter from "@/sections/Newsletter/Newsletter"
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import Footer from "@/components/Footer"
|
||||
import Spacer from "@/components/Utilities/Spacer"
|
||||
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature"
|
||||
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
@ -39,10 +39,10 @@ export default function PricingPage() {
|
||||
|
||||
<Payment type="one_time" />
|
||||
<Spacer className="mt-auto" />
|
||||
<Newsletter />
|
||||
<Newsletter/>
|
||||
<Spacer className="mb-8" />
|
||||
<Footer />
|
||||
</Background>
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import Footer from "@/components/Footer";
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import Spacer from "@/components/Utilities/Spacer";
|
||||
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature";
|
||||
import PageHeader from "@/sections/PageHeader";
|
||||
import React from "react";
|
||||
import Footer from "@/components/Footer"
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import Spacer from "@/components/Utilities/Spacer"
|
||||
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature"
|
||||
import PageHeader from "@/sections/PageHeader"
|
||||
import React from "react"
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
@ -28,7 +28,7 @@ const page = () => {
|
||||
<Footer />
|
||||
</Background>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default page;
|
||||
export default page
|
287
Frontend/app/actions.js
Normal file
287
Frontend/app/actions.js
Normal file
@ -0,0 +1,287 @@
|
||||
"use server"
|
||||
import { redirect } from "next/navigation"
|
||||
import pb from "@/lib/pocketbase"
|
||||
import { cookies } from "next/headers"
|
||||
import CryptoJS from "crypto-js"
|
||||
|
||||
import { apiPrices } from "./(public)/pricing/action"
|
||||
|
||||
export async function mailchimp(formData) {
|
||||
const email = formData.email
|
||||
const mailchimpBaseUrl = process.env.NEXT_PUBLIC_MAILCHIMP_BASE_URL
|
||||
const mailchimpApiKey = process.env.NEXT_PUBLIC_MAILCHIMP_BASE64_API_KEY
|
||||
const mailchimpList = process.env.NEXT_PUBLIC_MAILCHIMP_LIST_ID
|
||||
if (!mailchimpApiKey) return
|
||||
try {
|
||||
const subscriberHash = CryptoJS.MD5(email.toLocaleLowerCase())
|
||||
const mailchimpResponse = await fetch(
|
||||
`${mailchimpBaseUrl}/3.0/lists/${mailchimpList}/members/${subscriberHash}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: mailchimpApiKey
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email_address: email,
|
||||
status: "subscribed",
|
||||
merge_fields: {
|
||||
EMAIL: formData.email,
|
||||
FNAME: formData.first_name,
|
||||
LNAME: formData.last_name,
|
||||
PHONE: !formData.phone_number ? "" : formData.phone_number,
|
||||
CSIZE: formData.company_size,
|
||||
SOURCE: formData.source
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
if (mailchimpResponse.status !== 200) {
|
||||
throw new Error("couldn't complete the request")
|
||||
}
|
||||
return mailchimpResponse.json()
|
||||
} catch (err) {
|
||||
throw new Error("couldn't complete the request")
|
||||
}
|
||||
}
|
||||
|
||||
export async function signup(formData) {
|
||||
const email = formData.email
|
||||
const password = formData.password
|
||||
const organisation = formData.organisation
|
||||
console.log("app/(authenticated)/actions", "organisation", organisation)
|
||||
const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL
|
||||
const adminToken = process.env.NEXT_PUBLIC_POCKETBASE_ADMIN_TOKEN
|
||||
try {
|
||||
const orgRes = await fetch(
|
||||
`${pocketbaseUrl}/api/collections/organisation/records`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: adminToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: organisation
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log("orgRes.status: ", orgRes.status)
|
||||
if (orgRes.status !== 200) {
|
||||
throw new Error("Failed to create organisation")
|
||||
}
|
||||
const orgData = await orgRes.json()
|
||||
console.log(orgData)
|
||||
const userRes = await fetch(
|
||||
`${pocketbaseUrl}/api/collections/user/records`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: adminToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
firstName: formData.first_name,
|
||||
lastName: formData.last_name,
|
||||
displayName: formData.first_name + " " + formData.last_name,
|
||||
email: email,
|
||||
password: password,
|
||||
passwordConfirm: password,
|
||||
organisation: orgData?.id,
|
||||
role: "Admin",
|
||||
lastSeen: new Date()
|
||||
})
|
||||
}
|
||||
)
|
||||
console.log("userRes.status: ", userRes.status)
|
||||
if (userRes.status !== 200) {
|
||||
console.log(userRes)
|
||||
throw new Error("Failed to create user")
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw new Error(err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function login(formData) {
|
||||
console.log("login")
|
||||
const email = formData.email
|
||||
const password = formData.password
|
||||
try {
|
||||
const { token, record: data } = await pb
|
||||
.collection("user")
|
||||
.authWithPassword(email, password)
|
||||
if (pb.authStore.isValid) {
|
||||
const cookie = pb.authStore.exportToCookie()
|
||||
|
||||
cookies().set("pb_auth", cookie, {
|
||||
secure: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
httpOnly: true
|
||||
})
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
error: "Failed to log in",
|
||||
token: token,
|
||||
data: data
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
if (error.status == 403) {
|
||||
await pb.collection("user").requestVerification(email)
|
||||
}
|
||||
return JSON.parse(JSON.stringify(error))
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAuthCookie() {
|
||||
try {
|
||||
const cookie = cookies().get("pb_auth")
|
||||
pb.authStore.loadFromCookie(cookie?.value || "")
|
||||
return pb.authStore.token
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export async function isAuthenticated() {
|
||||
try {
|
||||
const cookie = cookies().get("pb_auth")
|
||||
if (!cookie) return false
|
||||
pb.authStore.loadFromCookie(cookie?.value || "")
|
||||
return pb.authStore.isValid || false
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
export async function getUser() {
|
||||
try {
|
||||
const cookie = cookies().get("pb_auth")
|
||||
if (!cookie) return false
|
||||
pb.authStore.loadFromCookie(cookie?.value || "")
|
||||
return pb.authStore.model
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
cookies().delete("pb_auth")
|
||||
redirect("/")
|
||||
}
|
||||
|
||||
export async function createCheckoutSession(price_id, type) {
|
||||
const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL
|
||||
if (!pocketbaseUrl) {
|
||||
throw Error("Connection Timeout")
|
||||
}
|
||||
if (!price_id) {
|
||||
throw Error("There was an error during the payment processing")
|
||||
}
|
||||
const token = await getAuthCookie()
|
||||
if (!token) {
|
||||
throw Error("Could not authenticate")
|
||||
}
|
||||
console.log("token", token)
|
||||
console.log("url ", `${pocketbaseUrl}/create-checkout-session`)
|
||||
|
||||
const body = JSON.stringify({
|
||||
price: {
|
||||
id: price_id,
|
||||
type: type
|
||||
},
|
||||
quantity: 1
|
||||
})
|
||||
console.log("body", body)
|
||||
|
||||
try {
|
||||
const createCheckoutSessionResponse = await fetch(
|
||||
`${pocketbaseUrl}/create-checkout-session`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: token
|
||||
},
|
||||
body: body
|
||||
}
|
||||
)
|
||||
if (createCheckoutSessionResponse.status !== 200) {
|
||||
throw new Error("Failed to process Request")
|
||||
}
|
||||
|
||||
const createCheckoutSessionData = await createCheckoutSessionResponse.json()
|
||||
|
||||
if (createCheckoutSessionData.url === "") {
|
||||
throw new Error("Failed to process request an invalid URL was served")
|
||||
}
|
||||
return createCheckoutSessionData
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSubscriptions() {
|
||||
const cookie = cookies().get("pb_auth")
|
||||
pb.authStore.loadFromCookie(cookie?.value || "")
|
||||
const userId = pb.authStore.model.id
|
||||
const subscriptions = await pb
|
||||
.collection("subscription")
|
||||
.getFullList({ filter: `user_id="${userId}" && status="active"` })
|
||||
console.log("app/(authenticated)/actions", "subscriptions", subscriptions)
|
||||
if (subscriptions.length === 0) {
|
||||
return []
|
||||
}
|
||||
const products = await apiPrices()
|
||||
const subscriptionWithProducts = subscriptions.map(subscription => {
|
||||
return {
|
||||
...subscription,
|
||||
product: products.find(
|
||||
product =>
|
||||
product?.yearlyPrice?.price_id === subscription.price_id ||
|
||||
product?.monthlyPrice?.price_id === subscription.price_id
|
||||
)
|
||||
}
|
||||
})
|
||||
return subscriptionWithProducts
|
||||
}
|
||||
|
||||
export async function createManagementSubscriptionSession() {
|
||||
const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL
|
||||
if (!pocketbaseUrl) {
|
||||
throw Error("Connection Timeout")
|
||||
}
|
||||
const token = await getAuthCookie()
|
||||
if (!token) {
|
||||
throw Error("Could not authenticate")
|
||||
}
|
||||
console.log("token", token)
|
||||
console.log("url ", `${pocketbaseUrl}/create-checkout-session`)
|
||||
try {
|
||||
const createManagementSessionResponse = await fetch(
|
||||
`${pocketbaseUrl}/create-portal-link`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: token
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
}
|
||||
)
|
||||
if (createManagementSessionResponse.status !== 200) {
|
||||
console.log(createManagementSessionResponse.status)
|
||||
throw new Error(
|
||||
JSON.stringify(await createManagementSessionResponse.json())
|
||||
)
|
||||
}
|
||||
const createManagementSessionData = await createManagementSessionResponse.json()
|
||||
return createManagementSessionData
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
@ -1,278 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import pb from "@/lib/pocketbase";
|
||||
import { cookies } from "next/headers";
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
import { CheckoutSession, SignUpForm, SourceModal, Subscription, SubscriptionSession, User } from "@/types";
|
||||
import { apiPrices } from "./(public)/pricing/actions";
|
||||
|
||||
export async function mailchimp(formData: {
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
phone_number?: string;
|
||||
company_size: string;
|
||||
source?: SourceModal;
|
||||
}) {
|
||||
const email = formData.email;
|
||||
const mailchimpBaseUrl = process.env.NEXT_PUBLIC_MAILCHIMP_BASE_URL;
|
||||
const mailchimpApiKey = process.env.NEXT_PUBLIC_MAILCHIMP_BASE64_API_KEY;
|
||||
const mailchimpList = process.env.NEXT_PUBLIC_MAILCHIMP_LIST_ID;
|
||||
if (!mailchimpApiKey) return;
|
||||
try {
|
||||
const subscriberHash = CryptoJS.MD5(email.toLocaleLowerCase());
|
||||
const mailchimpResponse = await fetch(`${mailchimpBaseUrl}/3.0/lists/${mailchimpList}/members/${subscriberHash}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: mailchimpApiKey,
|
||||
},
|
||||
body: JSON.stringify(
|
||||
{
|
||||
"email_address": email,
|
||||
status: "subscribed",
|
||||
merge_fields: {
|
||||
EMAIL: formData.email,
|
||||
FNAME: formData.first_name,
|
||||
LNAME: formData.last_name,
|
||||
PHONE: !formData.phone_number ? '' : formData.phone_number,
|
||||
CSIZE: formData.company_size,
|
||||
SOURCE: formData.source
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
if (mailchimpResponse.status !== 200) {
|
||||
throw new Error("couldn't complete the request");
|
||||
}
|
||||
return mailchimpResponse.json();
|
||||
} catch (err) {
|
||||
throw new Error("couldn't complete the request");
|
||||
}
|
||||
}
|
||||
|
||||
export async function signup(formData: SignUpForm) {
|
||||
const email = formData.email;
|
||||
const password = formData.password;
|
||||
const organisation = formData.organisation;
|
||||
console.log('app/(authenticated)/actions', 'organisation', organisation)
|
||||
const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL as string;
|
||||
const adminToken = process.env.NEXT_PUBLIC_POCKETBASE_ADMIN_TOKEN as string;
|
||||
try {
|
||||
const orgRes = await fetch(
|
||||
`${pocketbaseUrl}/api/collections/organisation/records`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: adminToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: organisation,
|
||||
}),
|
||||
}
|
||||
);
|
||||
console.log("orgRes.status: ", orgRes.status);
|
||||
if (orgRes.status !== 200) {
|
||||
throw new Error("Failed to create organisation");
|
||||
}
|
||||
const orgData = await orgRes.json();
|
||||
console.log(orgData);
|
||||
const userRes = await fetch(
|
||||
`${pocketbaseUrl}/api/collections/user/records`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: adminToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
firstName: formData.first_name,
|
||||
lastName: formData.last_name,
|
||||
displayName: formData.first_name + ' ' + formData.last_name,
|
||||
email: email,
|
||||
password: password,
|
||||
passwordConfirm: password,
|
||||
organisation: orgData?.id,
|
||||
role: "Admin",
|
||||
lastSeen: new Date(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
console.log("userRes.status: ", userRes.status);
|
||||
if (userRes.status !== 200) {
|
||||
console.log(userRes);
|
||||
throw new Error("Failed to create user");
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function login(formData: { email: string; password: string }) {
|
||||
console.log('login')
|
||||
const email = formData.email as string;
|
||||
const password = formData.password as string;
|
||||
try {
|
||||
const { token, record: data } = await pb
|
||||
.collection("user")
|
||||
.authWithPassword(email, password);
|
||||
if (pb.authStore.isValid) {
|
||||
const cookie = pb.authStore.exportToCookie();
|
||||
|
||||
cookies().set("pb_auth", cookie, {
|
||||
secure: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
httpOnly: true,
|
||||
});
|
||||
}
|
||||
return { success: true, error: "Failed to log in", token: token, data: data };
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
if ((error as any).status == 403) {
|
||||
await pb.collection('user').requestVerification(email);
|
||||
}
|
||||
return JSON.parse(JSON.stringify(error))
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAuthCookie() {
|
||||
try {
|
||||
const cookie = cookies().get('pb_auth');
|
||||
pb.authStore.loadFromCookie(cookie?.value || '');
|
||||
return pb.authStore.token;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function isAuthenticated() {
|
||||
try {
|
||||
const cookie = cookies().get('pb_auth');
|
||||
if(!cookie) return false;
|
||||
pb.authStore.loadFromCookie(cookie?.value || '');
|
||||
return pb.authStore.isValid || false;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
export async function getUser() {
|
||||
try {
|
||||
const cookie = cookies().get('pb_auth');
|
||||
if(!cookie) return false;
|
||||
pb.authStore.loadFromCookie(cookie?.value || '');
|
||||
return pb.authStore.model;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
cookies().delete("pb_auth");
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
export async function createCheckoutSession(price_id: string, type: string) {
|
||||
const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL;
|
||||
if (!pocketbaseUrl) {
|
||||
throw Error('Connection Timeout');
|
||||
}
|
||||
if (!price_id) {
|
||||
throw Error('There was an error during the payment processing');
|
||||
}
|
||||
const token = await getAuthCookie();
|
||||
if (!token) {
|
||||
throw Error('Could not authenticate');
|
||||
}
|
||||
console.log('token', token);
|
||||
console.log('url ', `${pocketbaseUrl}/create-checkout-session`);
|
||||
|
||||
const body = JSON.stringify({
|
||||
price: {
|
||||
id: price_id,
|
||||
type: type
|
||||
},
|
||||
quantity: 1
|
||||
});
|
||||
console.log('body',body);
|
||||
|
||||
try{
|
||||
const createCheckoutSessionResponse = await fetch(
|
||||
`${pocketbaseUrl}/create-checkout-session`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: token,
|
||||
},
|
||||
body: body,
|
||||
}
|
||||
);
|
||||
if (createCheckoutSessionResponse.status !== 200) {
|
||||
throw new Error("Failed to process Request");
|
||||
}
|
||||
|
||||
const createCheckoutSessionData: CheckoutSession = await createCheckoutSessionResponse.json();
|
||||
|
||||
if (createCheckoutSessionData.url === "") {
|
||||
throw new Error("Failed to process request an invalid URL was served");
|
||||
}
|
||||
return createCheckoutSessionData;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSubscriptions() {
|
||||
const cookie = cookies().get('pb_auth');
|
||||
pb.authStore.loadFromCookie(cookie?.value || '');
|
||||
const userId = (pb.authStore.model as User).id
|
||||
const subscriptions = await pb.collection("subscription").getFullList<Subscription>({filter: `user_id="${userId}" && status="active"`});
|
||||
console.log('app/(authenticated)/actions', 'subscriptions', subscriptions)
|
||||
if (subscriptions.length === 0){
|
||||
return [];
|
||||
}
|
||||
const products = await apiPrices();
|
||||
const subscriptionWithProducts = subscriptions.map(subscription => {return {...subscription, product: products.find(product => product?.yearlyPrice?.price_id === subscription.price_id || product?.monthlyPrice?.price_id === subscription.price_id)}}) as Subscription[]
|
||||
return subscriptionWithProducts;
|
||||
}
|
||||
|
||||
export async function createManagementSubscriptionSession() {
|
||||
const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL;
|
||||
if (!pocketbaseUrl) {
|
||||
throw Error('Connection Timeout');
|
||||
}
|
||||
const token = await getAuthCookie();
|
||||
if (!token) {
|
||||
throw Error('Could not authenticate');
|
||||
}
|
||||
console.log('token', token);
|
||||
console.log('url ', `${pocketbaseUrl}/create-checkout-session`);
|
||||
try{
|
||||
const createManagementSessionResponse = await fetch(
|
||||
`${pocketbaseUrl}/create-portal-link`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: token,
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
if (createManagementSessionResponse.status !== 200) {
|
||||
console.log(createManagementSessionResponse.status)
|
||||
throw new Error(JSON.stringify( await createManagementSessionResponse.json()));
|
||||
}
|
||||
const createManagementSessionData: SubscriptionSession = await createManagementSessionResponse.json();
|
||||
return createManagementSessionData;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
@ -1,40 +1,35 @@
|
||||
/* eslint-disable @next/next/no-before-interactive-script-outside-document */
|
||||
import "./globals.css";
|
||||
import "../app/globals.css";
|
||||
import { Arimo, Indie_Flower } from "next/font/google";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import Header from "@/components/Header";
|
||||
import { cookies } from "next/headers";
|
||||
import { isAuthenticated } from "@/lib/auth";
|
||||
import { GTagProvider, PHProvider, ThemeProvider } from "./providers";
|
||||
import Script from "next/script";
|
||||
import "./globals.css"
|
||||
import "../app/globals.css"
|
||||
import { Arimo, Indie_Flower } from "next/font/google"
|
||||
import { ToastContainer } from "react-toastify"
|
||||
import "react-toastify/dist/ReactToastify.css"
|
||||
import { cookies } from "next/headers"
|
||||
import { isAuthenticated } from "@/lib/auth"
|
||||
import { GTagProvider, PHProvider, ThemeProvider } from "./providers"
|
||||
import Script from "next/script"
|
||||
|
||||
const raleway = Arimo({
|
||||
variable: "--body-font",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
display: "swap"
|
||||
})
|
||||
|
||||
const arimo = Arimo({
|
||||
variable: "--body-font",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
display: "swap"
|
||||
})
|
||||
|
||||
const indieFlower = Indie_Flower({
|
||||
variable: "--accent-font",
|
||||
weight: "400",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
display: "swap"
|
||||
})
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const isUserLoggedIn = await isAuthenticated(cookies());
|
||||
export default async function RootLayout({ children }) {
|
||||
const isUserLoggedIn = await isAuthenticated(cookies())
|
||||
|
||||
return (
|
||||
<html
|
||||
@ -46,7 +41,10 @@ export default async function RootLayout({
|
||||
<GTagProvider />
|
||||
<body className={`${arimo.className} bg-base-100 flex`}>
|
||||
<ThemeProvider>
|
||||
<Header isUserLoggedIn={isUserLoggedIn} authString={cookies().get("pb_auth")?.value || ""} />
|
||||
<Header
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
authString={cookies().get("pb_auth")?.value || ""}
|
||||
/>
|
||||
{children}
|
||||
<ToastContainer
|
||||
position="bottom-left"
|
||||
@ -76,7 +74,7 @@ export default async function RootLayout({
|
||||
id="promotekit-js"
|
||||
async
|
||||
defer
|
||||
strategy="afterInteractive"
|
||||
strategy="afterInteractive"
|
||||
src="https://cdn.promotekit.com/promotekit.js"
|
||||
data-promotekit="41c8e339-d2aa-414b-88b8-31b6d6346e2b"
|
||||
/>
|
||||
@ -84,7 +82,7 @@ export default async function RootLayout({
|
||||
id="promotekit"
|
||||
async
|
||||
defer
|
||||
strategy="afterInteractive"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
$(document).ready(function(){
|
||||
@ -103,11 +101,11 @@ export default async function RootLayout({
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
`,
|
||||
`
|
||||
}}
|
||||
></Script>
|
||||
</body>
|
||||
</PHProvider>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
import Footer from "@/components/Footer";
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import PageWrapper from "@/components/Utilities/PageWrapper";
|
||||
import Link from "next/link";
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
@ -70,5 +68,5 @@ export default function NotFound() {
|
||||
</Background>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
@ -1,22 +1,17 @@
|
||||
import "aos/dist/aos.css";
|
||||
import React from "react";
|
||||
import { SquaredBackgroundHero } from "@/sections/Hero";
|
||||
import PageWrapper from "@/components/Utilities/PageWrapper";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHeader from "@/sections/PageHeader";
|
||||
import ContainerImageIconBlocksFeature from "@/sections/Features/ContainerImageIconBlocksFeature";
|
||||
import Background from "@/components/Utilities/Background";
|
||||
import CardTestemonial from "@/sections/Testemonial/CardTestemonial";
|
||||
import Payment from "@/sections/Payment";
|
||||
import CardsFeature from "@/sections/Features/CardsFeature";
|
||||
import FAQ from "@/sections/FAQ/RightAlignedBorderBottomFAQ";
|
||||
import VerticalTabsFeature from "@/sections/Features/VerticalTabsFeature";
|
||||
import "aos/dist/aos.css"
|
||||
import React from "react"
|
||||
import { SquaredBackgroundHero } from "@/sections/Hero"
|
||||
import PageWrapper from "@/components/Utilities/PageWrapper"
|
||||
import Footer from "@/components/Footer"
|
||||
import PageHeader from "@/sections/PageHeader"
|
||||
import ContainerImageIconBlocksFeature from "@/sections/Features/ContainerImageIconBlocksFeature"
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import CardTestemonial from "@/sections/Testemonial/CardTestemonial"
|
||||
import CardsFeature from "@/sections/Features/CardsFeature"
|
||||
import FAQ from "@/sections/FAQ/RightAlignedBorderBottomFAQ"
|
||||
import VerticalTabsFeature from "@/sections/Features/VerticalTabsFeature"
|
||||
|
||||
import { Metadata } from "next";
|
||||
import Head from "next/head";
|
||||
import Teste from "@/sections/Testing/Teste";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
export const metadata = {
|
||||
title: "Pocketbase, Stripe, Next.js, Boilerplate | FastPocket",
|
||||
description:
|
||||
"FastPocket - Is a boilerplate codebase for everyone to build a product quickly.",
|
||||
@ -26,7 +21,7 @@ export const metadata: Metadata = {
|
||||
"next.js",
|
||||
"boilerplate",
|
||||
"template",
|
||||
"codebase",
|
||||
"codebase"
|
||||
],
|
||||
openGraph: {
|
||||
url: "https://fastpocket.dev",
|
||||
@ -39,9 +34,9 @@ export const metadata: Metadata = {
|
||||
url: "https://fastpocket.dev/images/home/thumbnail.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "fastpocket.degv",
|
||||
},
|
||||
],
|
||||
alt: "fastpocket.degv"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
@ -55,24 +50,19 @@ export const metadata: Metadata = {
|
||||
url: "https://fastpocket.dev/images/home/thumbnail.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "fastpocket.degv",
|
||||
},
|
||||
],
|
||||
alt: "fastpocket.degv"
|
||||
}
|
||||
]
|
||||
},
|
||||
alternates: {
|
||||
canonical: "https://fastpocket.dev",
|
||||
},
|
||||
};
|
||||
canonical: "https://fastpocket.dev"
|
||||
}
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<PageWrapper>
|
||||
<SquaredBackgroundHero />
|
||||
|
||||
<Teste/>
|
||||
|
||||
|
||||
<SquaredBackgroundHero />
|
||||
<VerticalTabsFeature />
|
||||
<div className="bg-primary/2">
|
||||
@ -105,5 +95,5 @@ export default function Home() {
|
||||
<Footer />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
import posthog from 'posthog-js'
|
||||
import { PostHogProvider } from 'posthog-js/react'
|
||||
import GoogleAnalytics from "@/components/GoogleAnalytics";
|
||||
import { ThemeProvider as NTThemeProvider } from 'next-themes'
|
||||
|
||||
|
||||
|
51
Frontend/app/sitemap.js
Normal file
51
Frontend/app/sitemap.js
Normal file
@ -0,0 +1,51 @@
|
||||
import getPostMetadata from "@/utils/getPostMetaData"
|
||||
|
||||
export default async function sitemap() {
|
||||
const defaultPages = [
|
||||
{
|
||||
url: "https://fastpocket.dev",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "daily",
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
url: "https://fastpocket.dev/about",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9
|
||||
},
|
||||
{
|
||||
url: "https://fastpocket.dev/pricing",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9
|
||||
},
|
||||
{
|
||||
url: "https://fastpocket.dev/blogs",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9
|
||||
},
|
||||
{
|
||||
url: "https://fastpocket.dev/whatsnew",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9
|
||||
}
|
||||
// other pages
|
||||
]
|
||||
|
||||
const postSlugs = await getPostMetadata()
|
||||
|
||||
const sitemap = [
|
||||
...defaultPages,
|
||||
...postSlugs.map(e => ({
|
||||
url: `https://fastpocket.dev/blogs/${e.slug}`,
|
||||
lastModified: e.modified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8
|
||||
}))
|
||||
]
|
||||
|
||||
return sitemap
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import getPostMetadata from "@/utils/getPostMetaData";
|
||||
import { MetadataRoute } from "next";
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const defaultPages = [
|
||||
{
|
||||
url: "https://fastpocket.dev",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "daily",
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
url: "https://fastpocket.dev/about",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9
|
||||
},
|
||||
{
|
||||
url: "https://fastpocket.dev/pricing",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9
|
||||
},
|
||||
{
|
||||
url: "https://fastpocket.dev/blogs",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9
|
||||
},
|
||||
{
|
||||
url: "https://fastpocket.dev/whatsnew",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.9
|
||||
}
|
||||
// other pages
|
||||
];
|
||||
|
||||
const postSlugs = await getPostMetadata();
|
||||
|
||||
const sitemap = [
|
||||
...defaultPages,
|
||||
...postSlugs.map((e: any) => ({
|
||||
url: `https://fastpocket.dev/blogs/${e.slug}`,
|
||||
lastModified: e.modified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8
|
||||
} as any))
|
||||
];
|
||||
|
||||
return sitemap;
|
||||
}
|
9
Frontend/app/teste/page.js
Normal file
9
Frontend/app/teste/page.js
Normal file
@ -0,0 +1,9 @@
|
||||
function Page() {
|
||||
return (
|
||||
<>
|
||||
Qualquercoisa
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
@ -1,20 +1,9 @@
|
||||
import React from "react";
|
||||
import React from "react"
|
||||
|
||||
import { PostMetadata } from "@/types";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
function BlogCard({
|
||||
title,
|
||||
slug,
|
||||
subtitle,
|
||||
imageUrl,
|
||||
}: {
|
||||
title: string;
|
||||
slug: string;
|
||||
subtitle: string;
|
||||
imageUrl: string;
|
||||
}) {
|
||||
function BlogCard({ title, slug, subtitle, imageUrl }) {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
@ -37,7 +26,7 @@ function BlogCard({
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogCard;
|
||||
export default BlogCard
|
@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
const SvgComponent = (props: any) => (
|
||||
import * as React from "react"
|
||||
const SvgComponent = props => (
|
||||
<a href="https://fastpocket.dev">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -43,5 +43,5 @@ const SvgComponent = (props: any) => (
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
export default SvgComponent;
|
||||
)
|
||||
export default SvgComponent
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { title } from "@/constants";
|
||||
import Logo from "@/components/Logo";
|
||||
import Link from "next/link";
|
||||
import React from "react"
|
||||
import { title } from "@/constants"
|
||||
import Logo from "@/components/Logo"
|
||||
import Link from "next/link"
|
||||
import FastPocketBadge from "@/components/FastPocketBadge"
|
||||
|
||||
function Footer() {
|
||||
@ -253,7 +253,7 @@ function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
export default Footer
|
@ -1,18 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Subscription, User } from "@/types";
|
||||
import { getSubscriptions } from "@/app/actions";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface GetStartedSectionButtonProps {
|
||||
user: User;
|
||||
}
|
||||
export const GetStartedSectionButton = ({
|
||||
user,
|
||||
}: GetStartedSectionButtonProps) => {
|
||||
export const GetStartedSectionButton = ({ user }) => {
|
||||
const router = useRouter();
|
||||
const [subscription, setSubscription] = useState<Subscription>();
|
||||
const [subscription, setSubscription] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!user) {
|
@ -1,6 +1,6 @@
|
||||
import Script from "next/script";
|
||||
import Script from "next/script"
|
||||
|
||||
const GoogleAnalytics = ({ ga_id }: { ga_id: string }) => (
|
||||
const GoogleAnalytics = ({ ga_id }) => (
|
||||
<>
|
||||
<Script
|
||||
async
|
||||
@ -20,9 +20,9 @@ const GoogleAnalytics = ({ ga_id }: { ga_id: string }) => (
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '${ga_id}');
|
||||
`,
|
||||
`
|
||||
}}
|
||||
></Script>
|
||||
</>
|
||||
);
|
||||
export default GoogleAnalytics;
|
||||
)
|
||||
export default GoogleAnalytics
|
28
Frontend/components/Header.jsx
Normal file
28
Frontend/components/Header.jsx
Normal file
@ -0,0 +1,28 @@
|
||||
import Script from "next/script"
|
||||
|
||||
const GoogleAnalytics = ({ ga_id }) => (
|
||||
<>
|
||||
<Script
|
||||
async
|
||||
defer
|
||||
strategy="afterInteractive"
|
||||
src={`https://www.googletagmanager.com/gtag/js?
|
||||
id=${ga_id}`}
|
||||
></Script>
|
||||
<Script
|
||||
id="google-analytics"
|
||||
defer
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '${ga_id}');
|
||||
`
|
||||
}}
|
||||
></Script>
|
||||
</>
|
||||
)
|
||||
export default GoogleAnalytics
|
@ -1,162 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import Navigation from "@/components/Navigation";
|
||||
import ModalSignUp from "@/components/Modals/ModalSignUp";
|
||||
import ModalSignIn from "@/components/Modals/ModalSignIn";
|
||||
import { getAuthCookie, getUser, logout } from "@/app/actions";
|
||||
import { User } from "@/types";
|
||||
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||
import pb from "@/lib/pocketbase";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import ModalPasswordReset from "./Modals/ModalPasswordReset";
|
||||
import {Person24Filled, SignOut24Filled} from '@fluentui/react-icons'
|
||||
import ModalEmailChange from "./Modals/ModalEmailChange";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface HeaderProps {
|
||||
isUserLoggedIn: boolean;
|
||||
authString: string;
|
||||
}
|
||||
|
||||
export default function Header({ isUserLoggedIn, authString }: HeaderProps) {
|
||||
//Advice: Remove this and replace it with proper state management like Redux, Recoil or Zustand
|
||||
const [user, setUser] = useState<User | undefined>();
|
||||
const [statefulUser, setStatefulUser] = useState<User | undefined>();
|
||||
const pathName = usePathname();
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const user = await getUser();
|
||||
setUser(user as User);
|
||||
})();
|
||||
}, [statefulUser]);
|
||||
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
return (
|
||||
<header className="absolute w-full z-30">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 md:pt-6">
|
||||
<div className="flex items-center justify-between h-20">
|
||||
<Navigation isUserLoggedIn={isUserLoggedIn} />
|
||||
|
||||
{/* Desktop navigation */}
|
||||
<nav className=" md:flex">
|
||||
{/* Desktop sign in links */}
|
||||
<div className="flex flex-row grow sm:grow-0 items-center pr-2 gap-x-2">
|
||||
{/* <button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setTheme("dark")}
|
||||
>
|
||||
dark
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setTheme("light")}
|
||||
>
|
||||
light
|
||||
</button> */}
|
||||
<label className="swap swap-rotate" aria-label="theme-toggle">
|
||||
{/* this hidden checkbox controls the state */}
|
||||
<input
|
||||
type="checkbox"
|
||||
className="opacity-0"
|
||||
aria-label="theme-toggle-input"
|
||||
onClick={() =>
|
||||
theme === "light" ? setTheme("dark") : setTheme("light")
|
||||
}
|
||||
/>
|
||||
|
||||
{/* sun icon */}
|
||||
<svg
|
||||
className="swap-on fill-base-content w-6 h-6 z-10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
|
||||
</svg>
|
||||
|
||||
{/* moon icon */}
|
||||
<svg
|
||||
className="swap-off fill-base-content w-6 h-6 z-1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
|
||||
</svg>
|
||||
</label>
|
||||
|
||||
{!isUserLoggedIn ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() =>
|
||||
(async (router?: AppRouterInstance) => {
|
||||
console.log("firing");
|
||||
const authCookie = await getAuthCookie();
|
||||
pb.authStore.loadFromCookie(authCookie || "");
|
||||
if (!authCookie || !pb.authStore.isValid) {
|
||||
document.getElementById("sign-in-modal")?.click();
|
||||
} else {
|
||||
router?.push("/account");
|
||||
}
|
||||
})()
|
||||
}
|
||||
className={`btn btn-sm btn-ghost text-primary mr-1`}
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.getItem("price") &&
|
||||
localStorage.removeItem("price");
|
||||
document.getElementById("sign-up-modal")?.click();
|
||||
}}
|
||||
className={`btn btn-primary btn-sm text-primary-content `}
|
||||
id="sign-up-modal-button"
|
||||
>
|
||||
Get First Access
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="dropdown dropdown-end">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-ghost btn-circle avatar"
|
||||
>
|
||||
<div className="w-6 rounded-full !flex justify-center items-center bg-base-300">
|
||||
<Person24Filled />
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
|
||||
>
|
||||
<li>
|
||||
<Link href="/account" aria-label="profile">
|
||||
<div className=" capitalize flex flex-row gap-x-2">
|
||||
<Person24Filled />
|
||||
{user?.displayName}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
<li onClick={() => logout()}>
|
||||
<div className="capitalize flex flex-row gap-x-2">
|
||||
<SignOut24Filled />
|
||||
Logout
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<ModalSignIn />
|
||||
<ModalPasswordReset />
|
||||
<ModalEmailChange authString={authString} />
|
||||
<ModalSignUp setUser={setStatefulUser} />
|
||||
</header>
|
||||
);
|
||||
}
|
@ -1,14 +1,8 @@
|
||||
import React from "react";
|
||||
import React from "react"
|
||||
|
||||
import Image, { ImageProps } from "next/image";
|
||||
import Image from "next/image"
|
||||
|
||||
interface LogoProps {
|
||||
label?: string;
|
||||
className?: string;
|
||||
imageProps?: ImageProps;
|
||||
}
|
||||
|
||||
function Logo({ imageProps, ...props }: LogoProps) {
|
||||
function Logo({ imageProps, ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
@ -26,7 +20,7 @@ function Logo({ imageProps, ...props }: LogoProps) {
|
||||
{...imageProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Logo;
|
||||
export default Logo
|
@ -1,31 +1,31 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { emailValidationSchema } from "@/utils/form";
|
||||
import { toast } from "react-toastify";
|
||||
import pb from "@/lib/pocketbase";
|
||||
import {Dismiss20Filled} from "@fluentui/react-icons"
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import { emailValidationSchema } from "@/utils/form"
|
||||
import { toast } from "react-toastify"
|
||||
import pb from "@/lib/pocketbase"
|
||||
import { Dismiss20Filled } from "@fluentui/react-icons"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
function ModalEmailChange({authString}:{authString:string}) {
|
||||
function ModalEmailChange({ authString }) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm({
|
||||
resolver: yupResolver(emailValidationSchema),
|
||||
});
|
||||
resolver: yupResolver(emailValidationSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
console.log(data);
|
||||
pb.authStore.loadFromCookie(authString);
|
||||
!pb.authStore.isValid && redirect("/");
|
||||
const onSubmit = async data => {
|
||||
console.log(data)
|
||||
pb.authStore.loadFromCookie(authString)
|
||||
!pb.authStore.isValid && redirect("/")
|
||||
try {
|
||||
if (await pb.collection("user").requestEmailChange(data.email)) {
|
||||
reset();
|
||||
document.getElementById("email-change-modal")?.click();
|
||||
document.getElementById("sign-in-modal")?.click();
|
||||
reset()
|
||||
document.getElementById("email-change-modal")?.click()
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@ -37,11 +37,11 @@ function ModalEmailChange({authString}:{authString:string}) {
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
});
|
||||
theme: "colored"
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
@ -49,7 +49,7 @@ function ModalEmailChange({authString}:{authString:string}) {
|
||||
id="email-change-modal"
|
||||
className="modal-toggle"
|
||||
onClick={() => {
|
||||
reset();
|
||||
reset()
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="email-change-modal" className="modal cursor-pointer">
|
||||
@ -88,7 +88,7 @@ function ModalEmailChange({authString}:{authString:string}) {
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className={isSubmitting ? "btn btn-gray": "btn btn-primary"}
|
||||
className={isSubmitting ? "btn btn-gray" : "btn btn-primary"}
|
||||
>
|
||||
Change Email
|
||||
{isSubmitting && <div className="loading"></div>}
|
||||
@ -99,7 +99,7 @@ function ModalEmailChange({authString}:{authString:string}) {
|
||||
</label>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalEmailChange;
|
||||
export default ModalEmailChange
|
@ -1,30 +1,29 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { emailValidationSchema } from "@/utils/form";
|
||||
import { toast } from "react-toastify";
|
||||
import pb from "@/lib/pocketbase";
|
||||
import {Dismiss20Filled} from '@fluentui/react-icons'
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import { emailValidationSchema } from "@/utils/form"
|
||||
import { toast } from "react-toastify"
|
||||
import pb from "@/lib/pocketbase"
|
||||
import { Dismiss20Filled } from "@fluentui/react-icons"
|
||||
|
||||
function ModalPasswordReset() {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm({
|
||||
resolver: yupResolver(emailValidationSchema),
|
||||
});
|
||||
resolver: yupResolver(emailValidationSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
console.log(data);
|
||||
const onSubmit = async data => {
|
||||
console.log(data)
|
||||
try {
|
||||
//login user
|
||||
if (await pb.collection("user").requestPasswordReset(data.email)) {
|
||||
reset();
|
||||
document.getElementById("password-reset-modal")?.click();
|
||||
document.getElementById("sign-in-modal")?.click();
|
||||
reset()
|
||||
document.getElementById("password-reset-modal")?.click()
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@ -36,11 +35,11 @@ function ModalPasswordReset() {
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
});
|
||||
theme: "colored"
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
@ -48,7 +47,7 @@ function ModalPasswordReset() {
|
||||
id="password-reset-modal"
|
||||
className="modal-toggle"
|
||||
onClick={() => {
|
||||
reset();
|
||||
reset()
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="password-reset-modal" className="modal cursor-pointer">
|
||||
@ -98,7 +97,7 @@ function ModalPasswordReset() {
|
||||
</label>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalPasswordReset;
|
||||
export default ModalPasswordReset
|
@ -1,36 +1,34 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { signInValidationSchema } from "@/utils/form";
|
||||
import { login } from "@/app/actions";
|
||||
import { toast } from "react-toastify";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {Dismiss20Filled} from '@fluentui/react-icons'
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import { signInValidationSchema } from "@/utils/form"
|
||||
import { login } from "@/app/actions"
|
||||
import { toast } from "react-toastify"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Dismiss20Filled } from "@fluentui/react-icons"
|
||||
|
||||
function ModalSignIn() {
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm({
|
||||
resolver: yupResolver(signInValidationSchema),
|
||||
});
|
||||
resolver: yupResolver(signInValidationSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
const onSubmit = async data => {
|
||||
try {
|
||||
//login user
|
||||
const auth = (await login({ email: data.email, password: data.password }))
|
||||
if (
|
||||
auth.success
|
||||
) {
|
||||
reset();
|
||||
document.getElementById("sign-in-modal")?.click();
|
||||
router.push("/account");
|
||||
const auth = await login({ email: data.email, password: data.password })
|
||||
if (auth.success) {
|
||||
reset()
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
router.push("/account")
|
||||
} else {
|
||||
throw new Error(auth.response.message)
|
||||
}
|
||||
@ -44,11 +42,11 @@ function ModalSignIn() {
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
});
|
||||
theme: "colored"
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
@ -56,7 +54,7 @@ function ModalSignIn() {
|
||||
id="sign-in-modal"
|
||||
className="modal-toggle"
|
||||
onClick={() => {
|
||||
reset();
|
||||
reset()
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="sign-in-modal" className="modal cursor-pointer">
|
||||
@ -120,8 +118,8 @@ function ModalSignIn() {
|
||||
|
||||
<span
|
||||
onClick={() => {
|
||||
document.getElementById("sign-in-modal")?.click();
|
||||
document.getElementById("sign-up-modal")?.click();
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
document.getElementById("sign-up-modal")?.click()
|
||||
}}
|
||||
className="text-primary hover:text-primary/60 cursor-pointer "
|
||||
>
|
||||
@ -143,7 +141,7 @@ function ModalSignIn() {
|
||||
</label>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalSignIn;
|
||||
export default ModalSignIn
|
147
Frontend/components/Modals/ModalSignIn.jsx
Normal file
147
Frontend/components/Modals/ModalSignIn.jsx
Normal file
@ -0,0 +1,147 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import { signInValidationSchema } from "@/utils/form"
|
||||
import { login } from "@/app/actions"
|
||||
import { toast } from "react-toastify"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Dismiss20Filled } from "@fluentui/react-icons"
|
||||
|
||||
function ModalSignIn() {
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm({
|
||||
resolver: yupResolver(signInValidationSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async data => {
|
||||
try {
|
||||
//login user
|
||||
const auth = await login({ email: data.email, password: data.password })
|
||||
if (auth.success) {
|
||||
reset()
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
router.push("/account")
|
||||
} else {
|
||||
throw new Error(auth.response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message, {
|
||||
position: "bottom-left",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="sign-in-modal"
|
||||
className="modal-toggle"
|
||||
onClick={() => {
|
||||
reset()
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="sign-in-modal" className="modal cursor-pointer">
|
||||
<label className="modal-box relative bg-base-100 max-w-full md:max-w-[550px] py-4 px-3 md:p-6">
|
||||
<div className="flex justify-end pb-2 select-none">
|
||||
<label
|
||||
htmlFor="sign-in-modal"
|
||||
className="cursor-pointer text-base-content"
|
||||
>
|
||||
<Dismiss20Filled />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div className="w-[100%] bg-gradient-to-r from-primary to-secondary px-6 pt-2 mt-3 pb-6 rounded-lg text-primary-content">
|
||||
<h3 className="pb-1 text-3xl font-bold md:text-3xl pt-6">
|
||||
Welcome back!
|
||||
</h3>
|
||||
<p className="text-sm md:text-base">Lets kick some more ass?</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
|
||||
<div className="relative mt-6">
|
||||
<input
|
||||
type="text"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Your email…"
|
||||
aria-label="Your email…"
|
||||
autoComplete="on"
|
||||
{...register("email")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.email?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-1 mb-2 ">
|
||||
<input
|
||||
type="password"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Your password..."
|
||||
aria-label="Your password..."
|
||||
{...register("password")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.password?.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row w-full justify-between">
|
||||
<div className="flex flex-row gap-x-4">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Sign In
|
||||
{isSubmitting && <div className="loading"></div>}
|
||||
</button>
|
||||
<div className=" text-xs w-28 block">
|
||||
<span className="whitespace-normal">
|
||||
Dont have an account?{" "}
|
||||
</span>
|
||||
|
||||
<span
|
||||
onClick={() => {
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
document.getElementById("sign-up-modal")?.click()
|
||||
}}
|
||||
className="text-primary hover:text-primary/60 cursor-pointer "
|
||||
>
|
||||
Sign up
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
document.getElementById("password-reset-modal")?.click()
|
||||
}
|
||||
className="btn btn-ghost text-primary capitalize border-none"
|
||||
>
|
||||
Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalSignIn
|
@ -1,39 +1,33 @@
|
||||
"use client";
|
||||
"use client"
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import { signUpValidationSchema } from "@/utils/form"
|
||||
import { companySizeList } from "@/constants"
|
||||
import pb from "@/lib/pocketbase"
|
||||
import { createCheckoutSession, login } from "@/app/actions"
|
||||
import { toast } from "react-toastify"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Dismiss20Filled } from "@fluentui/react-icons"
|
||||
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { signUpValidationSchema } from "@/utils/form";
|
||||
import { companySizeList } from "@/constants";
|
||||
import pb from "@/lib/pocketbase";
|
||||
import { createCheckoutSession, login } from "@/app/actions";
|
||||
import { toast } from "react-toastify";
|
||||
import { Price, User } from "@/types";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {Dismiss20Filled} from '@fluentui/react-icons'
|
||||
|
||||
function ModalSignUp({
|
||||
setUser,
|
||||
}: {
|
||||
setUser: Dispatch<SetStateAction<User | undefined>>;
|
||||
}) {
|
||||
function ModalSignUp({ setUser }) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm({
|
||||
resolver: yupResolver(signUpValidationSchema),
|
||||
});
|
||||
const router = useRouter();
|
||||
const generateCheckoutPage = async (price: Price, type: string) => {
|
||||
resolver: yupResolver(signUpValidationSchema)
|
||||
})
|
||||
const router = useRouter()
|
||||
const generateCheckoutPage = async (price, type) => {
|
||||
try {
|
||||
const checkoutSessionResponse = await createCheckoutSession(
|
||||
price.price_id,
|
||||
type
|
||||
);
|
||||
console.log(checkoutSessionResponse);
|
||||
router.push(checkoutSessionResponse.url);
|
||||
)
|
||||
console.log(checkoutSessionResponse)
|
||||
router.push(checkoutSessionResponse.url)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message, {
|
||||
@ -44,53 +38,51 @@ function ModalSignUp({
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
});
|
||||
theme: "colored"
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
const onSubmit = async (data: any) => {
|
||||
}
|
||||
const onSubmit = async data => {
|
||||
data = {
|
||||
emailVisibility: false,
|
||||
lastSeen: new Date(),
|
||||
role: "Admin",
|
||||
displayName: `${data.firstName} ${data.lastName}`,
|
||||
passwordConfirm: data.password,
|
||||
...data,
|
||||
};
|
||||
...data
|
||||
}
|
||||
try {
|
||||
//create organisation
|
||||
const organisation = await pb.collection("organisation").create({
|
||||
name: data.organisation,
|
||||
organisationSize: data.organisationSize,
|
||||
});
|
||||
organisationSize: data.organisationSize
|
||||
})
|
||||
//create user
|
||||
const user = await pb
|
||||
.collection("user")
|
||||
.create({ ...data, organisation: organisation.id });
|
||||
.create({ ...data, organisation: organisation.id })
|
||||
//login user
|
||||
const auth = (await login({ email: data.email, password: data.password }))
|
||||
if (
|
||||
auth.success
|
||||
) {
|
||||
reset();
|
||||
document.getElementById("sign-up-modal")?.click();
|
||||
const price = localStorage.getItem("price");
|
||||
const type = localStorage.getItem("type");
|
||||
setUser(user as User);
|
||||
console.log("price", price);
|
||||
console.log("type", type);
|
||||
const auth = await login({ email: data.email, password: data.password })
|
||||
if (auth.success) {
|
||||
reset()
|
||||
document.getElementById("sign-up-modal")?.click()
|
||||
const price = localStorage.getItem("price")
|
||||
const type = localStorage.getItem("type")
|
||||
setUser(user)
|
||||
console.log("price", price)
|
||||
console.log("type", type)
|
||||
price
|
||||
? generateCheckoutPage(JSON.parse(price), type ?? "")
|
||||
: router.push("/account");
|
||||
: router.push("/account")
|
||||
} else {
|
||||
throw new Error(auth.response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(
|
||||
Object.values((error as any).data.data)
|
||||
.map((x: any) => x.message)
|
||||
Object.values(error.data.data)
|
||||
.map(x => x.message)
|
||||
.join(),
|
||||
{
|
||||
position: "bottom-left",
|
||||
@ -100,12 +92,12 @@ function ModalSignUp({
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
theme: "colored"
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
@ -114,7 +106,7 @@ function ModalSignUp({
|
||||
className="modal-toggle"
|
||||
name=""
|
||||
onClick={() => {
|
||||
reset();
|
||||
reset()
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="sign-up-modal" className="modal cursor-pointer">
|
||||
@ -124,7 +116,7 @@ function ModalSignUp({
|
||||
htmlFor="sign-up-modal"
|
||||
className="cursor-pointer text-base-content"
|
||||
onClick={() => {
|
||||
reset();
|
||||
reset()
|
||||
}}
|
||||
>
|
||||
<Dismiss20Filled />
|
||||
@ -225,7 +217,7 @@ function ModalSignUp({
|
||||
>
|
||||
{companySizeOption}
|
||||
</option>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
@ -265,8 +257,8 @@ function ModalSignUp({
|
||||
|
||||
<span
|
||||
onClick={() => {
|
||||
document.getElementById("sign-up-modal")?.click();
|
||||
document.getElementById("sign-in-modal")?.click();
|
||||
document.getElementById("sign-up-modal")?.click()
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
}}
|
||||
className="text-primary hover:text-primary/60 cursor-pointer "
|
||||
>
|
||||
@ -279,7 +271,7 @@ function ModalSignUp({
|
||||
</label>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalSignUp;
|
||||
export default ModalSignUp
|
@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import Logo from "@/components/Logo";
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {Navigation24Filled} from "@fluentui/react-icons"
|
||||
import Logo from "@/components/Logo"
|
||||
import React, { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { Navigation24Filled } from "@fluentui/react-icons"
|
||||
|
||||
function Navigation({ isUserLoggedIn }: { isUserLoggedIn: boolean }) {
|
||||
const [checked, setChecked] = useState<boolean>();
|
||||
function Navigation({ isUserLoggedIn }) {
|
||||
const [checked, setChecked] = useState()
|
||||
const handleClick = () => {
|
||||
checked ? setChecked(!checked) : setChecked(checked);
|
||||
};
|
||||
checked ? setChecked(!checked) : setChecked(checked)
|
||||
}
|
||||
return (
|
||||
<div className="drawer max-w-fit">
|
||||
<input
|
||||
@ -23,10 +23,7 @@ function Navigation({ isUserLoggedIn }: { isUserLoggedIn: boolean }) {
|
||||
{/* Navbar */}
|
||||
<div className="flex-grow navbar ">
|
||||
<div className="flex-none lg:hidden">
|
||||
<label
|
||||
htmlFor="my-drawer-3"
|
||||
className="items-center"
|
||||
>
|
||||
<label htmlFor="my-drawer-3" className="items-center">
|
||||
<Navigation24Filled />
|
||||
</label>
|
||||
</div>
|
||||
@ -107,7 +104,7 @@ function Navigation({ isUserLoggedIn }: { isUserLoggedIn: boolean }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Navigation;
|
||||
export default Navigation
|
@ -1,34 +1,24 @@
|
||||
"use client";
|
||||
"use client"
|
||||
import { createCheckoutSession, isAuthenticated } from "@/app/actions"
|
||||
import { toast } from "react-toastify"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { CheckmarkCircle24Filled } from "@fluentui/react-icons"
|
||||
|
||||
import { Price, Product, SourceModal } from "@/types";
|
||||
import { createCheckoutSession, isAuthenticated } from "@/app/actions";
|
||||
import { toast } from "react-toastify";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {CheckmarkCircle24Filled } from "@fluentui/react-icons"
|
||||
|
||||
export default function PriceCard({
|
||||
product,
|
||||
isAnnual,
|
||||
loading,
|
||||
}: {
|
||||
product?: Product;
|
||||
isAnnual?: boolean;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const openSignUpModalOnPriceClick = (price: Price, type: string) => {
|
||||
const signUpModal = document.getElementById("sign-up-modal");
|
||||
if (!signUpModal) return;
|
||||
signUpModal.click();
|
||||
};
|
||||
const generateCheckoutPage = async (price: Price, type: string) => {
|
||||
export default function PriceCard({ product, isAnnual, loading }) {
|
||||
const router = useRouter()
|
||||
const openSignUpModalOnPriceClick = (price, type) => {
|
||||
const signUpModal = document.getElementById("sign-up-modal")
|
||||
if (!signUpModal) return
|
||||
signUpModal.click()
|
||||
}
|
||||
const generateCheckoutPage = async (price, type) => {
|
||||
try {
|
||||
const checkoutSessionResponse = await createCheckoutSession(
|
||||
price.price_id,
|
||||
type
|
||||
);
|
||||
console.log(checkoutSessionResponse);
|
||||
router.push(checkoutSessionResponse.url);
|
||||
)
|
||||
console.log(checkoutSessionResponse)
|
||||
router.push(checkoutSessionResponse.url)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message, {
|
||||
@ -39,25 +29,25 @@ export default function PriceCard({
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
});
|
||||
theme: "colored"
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
const submitForm = async (product: Product) => {
|
||||
const userIsAuthenticated = await isAuthenticated();
|
||||
console.log("userIsAuthenticated", userIsAuthenticated);
|
||||
const price = isAnnual ? product.yearlyPrice : product.monthlyPrice;
|
||||
console.log("price", price);
|
||||
console.log("product.type", product.type);
|
||||
localStorage.setItem("price", JSON.stringify(price));
|
||||
localStorage.setItem("type", product.type);
|
||||
}
|
||||
const submitForm = async product => {
|
||||
const userIsAuthenticated = await isAuthenticated()
|
||||
console.log("userIsAuthenticated", userIsAuthenticated)
|
||||
const price = isAnnual ? product.yearlyPrice : product.monthlyPrice
|
||||
console.log("price", price)
|
||||
console.log("product.type", product.type)
|
||||
localStorage.setItem("price", JSON.stringify(price))
|
||||
localStorage.setItem("type", product.type)
|
||||
if (userIsAuthenticated) {
|
||||
await generateCheckoutPage(price, product.type);
|
||||
await generateCheckoutPage(price, product.type)
|
||||
} else {
|
||||
openSignUpModalOnPriceClick(price, product.type);
|
||||
openSignUpModalOnPriceClick(price, product.type)
|
||||
}
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`relative w-64 sm:w-80 bg-base-200 rounded-lg p-6 shadow-lg border border-primary/40 ${
|
||||
@ -124,5 +114,5 @@ export default function PriceCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
29
Frontend/components/PriceToggle.jsx
Normal file
29
Frontend/components/PriceToggle.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
export default function PriceToggle({ isAnnual, onChange }) {
|
||||
return (
|
||||
<>
|
||||
<label className="shadow-card relative inline-flex cursor-pointer select-none items-center justify-center rounded-lg bg-base-100 p-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only"
|
||||
checked={isAnnual}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<span
|
||||
className={`flex items-center space-x-[6px] rounded py-2 px-[18px] text-sm font-medium ${
|
||||
!isAnnual ? " bg-secondary" : "text-secondary-content"
|
||||
}`}
|
||||
>
|
||||
Monthly Billing
|
||||
</span>
|
||||
<span
|
||||
className={`flex items-center space-x-[6px] rounded py-2 px-[18px] text-sm font-medium ${
|
||||
isAnnual ? " bg-secondary" : "text-secondary-content"
|
||||
}`}
|
||||
>
|
||||
Yearly Billing
|
||||
</span>
|
||||
</label>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
import { ChangeEventHandler } from "react";
|
||||
|
||||
export default function PriceToggle({
|
||||
isAnnual,
|
||||
onChange,
|
||||
}: {
|
||||
isAnnual: boolean;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<label className="shadow-card relative inline-flex cursor-pointer select-none items-center justify-center rounded-lg bg-base-100 p-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only"
|
||||
checked={isAnnual}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<span
|
||||
className={`flex items-center space-x-[6px] rounded py-2 px-[18px] text-sm font-medium ${
|
||||
!isAnnual ? " bg-secondary" : "text-secondary-content"
|
||||
}`}
|
||||
>
|
||||
Monthly Billing
|
||||
</span>
|
||||
<span
|
||||
className={`flex items-center space-x-[6px] rounded py-2 px-[18px] text-sm font-medium ${
|
||||
isAnnual ? " bg-secondary" : "text-secondary-content"
|
||||
}`}
|
||||
>
|
||||
Yearly Billing
|
||||
</span>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
}
|
34
Frontend/components/Utilities/Background.jsx
Normal file
34
Frontend/components/Utilities/Background.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import React from "react"
|
||||
import Image from "next/image"
|
||||
|
||||
const Background = ({ children, className }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"h-full relative w-full bg-center bg-no-repeat bg-cover bg-fixed flex flex-col " +
|
||||
className
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
theme == "dark"
|
||||
? "/images/dark-gradient.webp"
|
||||
: "/images/gradient.webp"
|
||||
}
|
||||
alt="background"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
objectPosition="center"
|
||||
priority
|
||||
/>
|
||||
<div className="z-10">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Background
|
@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import React, { ReactNode } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const Background = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={"h-full relative w-full bg-center bg-no-repeat bg-cover bg-fixed flex flex-col " + className}
|
||||
>
|
||||
<Image
|
||||
src={theme == "dark" ? "/images/dark-gradient.webp": "/images/gradient.webp"}
|
||||
alt="background"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
objectPosition="center"
|
||||
priority
|
||||
/>
|
||||
<div className="z-10">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Background;
|
11
Frontend/components/Utilities/PageWrapper.jsx
Normal file
11
Frontend/components/Utilities/PageWrapper.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from "react"
|
||||
|
||||
function PageWrapper({ children }) {
|
||||
return (
|
||||
<main className="h-full flex flex-col my-auto mx-auto size-full flex-grow">
|
||||
{children}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageWrapper
|
@ -1,12 +0,0 @@
|
||||
import { WaitingListWithImageHero } from "@/sections/Hero";
|
||||
import React from "react";
|
||||
|
||||
function PageWrapper({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className="h-full flex flex-col my-auto mx-auto size-full flex-grow">
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageWrapper;
|
7
Frontend/components/Utilities/Spacer.jsx
Normal file
7
Frontend/components/Utilities/Spacer.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react"
|
||||
|
||||
const Spacer = props => {
|
||||
return <div {...props}></div>
|
||||
}
|
||||
|
||||
export default Spacer
|
@ -1,12 +0,0 @@
|
||||
import React, { ReactHTMLElement } from "react";
|
||||
|
||||
const Spacer = (
|
||||
props: React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>
|
||||
) => {
|
||||
return <div {...props}></div>;
|
||||
};
|
||||
|
||||
export default Spacer;
|
@ -1,20 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react"
|
||||
|
||||
import { MediaPlayer, MediaProvider } from "@vidstack/react";
|
||||
import { MediaPlayer, MediaProvider } from "@vidstack/react"
|
||||
import {
|
||||
DefaultAudioLayout,
|
||||
defaultLayoutIcons,
|
||||
DefaultVideoLayout,
|
||||
} from "@vidstack/react/player/layouts/default";
|
||||
import "@vidstack/react/player/styles/base.css";
|
||||
export type Props = {
|
||||
videotitle: string;
|
||||
video: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
DefaultVideoLayout
|
||||
} from "@vidstack/react/player/layouts/default"
|
||||
import "@vidstack/react/player/styles/base.css"
|
||||
|
||||
export default function YouTubeFrame({ video, width, height }: Props) {
|
||||
export default function YouTubeFrame({ video, width, height }) {
|
||||
return (
|
||||
<MediaPlayer
|
||||
className="w-full aspect-video bg-slate-900 bg-base-content font-sans overflow-hidden rounded-xl ring-media-focus data-[focus]:ring-4"
|
||||
@ -29,5 +23,5 @@ export default function YouTubeFrame({ video, width, height }: Props) {
|
||||
thumbnails="https://media-files.vidstack.io/thumbnails.vtt"
|
||||
/>
|
||||
</MediaPlayer>
|
||||
);
|
||||
)
|
||||
}
|
@ -4,6 +4,7 @@ export const companySizeList = [
|
||||
"30-70 employees",
|
||||
"70-100 employees",
|
||||
"100+ employees"
|
||||
]
|
||||
]
|
||||
|
||||
export const title = "FastPocket"
|
||||
|
||||
export const title = "FastPocket"
|
7
Frontend/jsconfig.json
Normal file
7
Frontend/jsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
20
Frontend/lib/auth.js
Normal file
20
Frontend/lib/auth.js
Normal file
@ -0,0 +1,20 @@
|
||||
import pb from "@/lib/pocketbase"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
export const getUserFromCookie = async cookies => {
|
||||
const cookie = cookies.get("pb_auth")
|
||||
if (!cookie) {
|
||||
redirect("/")
|
||||
//throw new Error("No authenticated user");
|
||||
} else {
|
||||
pb.authStore.loadFromCookie(cookie?.value || "")
|
||||
return pb.authStore.model
|
||||
}
|
||||
}
|
||||
|
||||
export const isAuthenticated = async cookieStore => {
|
||||
const cookie = cookieStore.get("pb_auth")
|
||||
if (!cookie) return false
|
||||
pb.authStore.loadFromCookie(cookie?.value || "")
|
||||
return pb.authStore.isValid || false
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
||||
import pb from "@/lib/pocketbase";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const getUserFromCookie = async (cookies: ReadonlyRequestCookies) => {
|
||||
const cookie = cookies.get('pb_auth');
|
||||
if (!cookie) {
|
||||
redirect('/');
|
||||
//throw new Error("No authenticated user");
|
||||
} else {
|
||||
pb.authStore.loadFromCookie(cookie?.value || '');
|
||||
return pb.authStore.model;
|
||||
}
|
||||
}
|
||||
|
||||
export const isAuthenticated = async (cookieStore: ReadonlyRequestCookies) => {
|
||||
const cookie = cookieStore.get('pb_auth');
|
||||
if(!cookie) return false;
|
||||
pb.authStore.loadFromCookie(cookie?.value || '');
|
||||
return pb.authStore.isValid || false;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user