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 PageHeader from "@/sections/PageHeader"
|
||||||
import React from "react";
|
import React from "react"
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer"
|
||||||
import Image from "next/image";
|
import Image from "next/image"
|
||||||
import Link from "next/link";
|
import Link from "next/link"
|
||||||
|
|
||||||
export default async function About() {
|
export default async function About() {
|
||||||
return (
|
return (
|
||||||
@ -15,20 +15,45 @@ export default async function About() {
|
|||||||
<PageHeader title="Helping Developers Build" />
|
<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">
|
<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">
|
<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
|
In 2023 I built{" "}
|
||||||
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
|
<Link
|
||||||
+ Pocketbase</Link>. As 2024 has come around I have had more and more
|
href={"https://sign365.com.au"}
|
||||||
requests for applications and features on the existing code. I built a
|
className="font-bold text-secondary"
|
||||||
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
|
Sign365
|
||||||
easy solution to give everyone a head start.
|
</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>
|
||||||
<p className="text-lg text-base-content font-thin">
|
<p className="text-lg text-base-content font-thin">
|
||||||
After reflecting on the challenges that developers face building their
|
After reflecting on the challenges that developers face building their
|
||||||
applications, I've seen that there will never be one codebase
|
applications, I've seen that there will never be one codebase
|
||||||
that suits all developers. But I am sure for those who are
|
that suits all developers. But I am sure for those who are
|
||||||
opensourcing, self-hosting and want to spin up an application quickly
|
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>
|
||||||
<p className="text-lg text-base-content font-thin">
|
<p className="text-lg text-base-content font-thin">
|
||||||
I am taking all of the knowledge that I have gained across 20+
|
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
|
that have been battle tested in enterprise code
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-base-content font-thin">
|
<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
|
So I've committed to building{" "}
|
||||||
projects faster to get paid.
|
<Link
|
||||||
|
href={"https://fastpocket.dev"}
|
||||||
|
className="font-bold text-secondary"
|
||||||
|
>
|
||||||
|
FastPocket
|
||||||
|
</Link>{" "}
|
||||||
|
to help you build your projects faster to get paid.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xl text-base-content font-bold">
|
<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>
|
||||||
<p className="text-lg text-base-content font-thin">
|
<p className="text-lg text-base-content font-thin">
|
||||||
I know that your next project will grow exponentially because of the
|
I know that your next project will grow exponentially because of the
|
||||||
@ -69,5 +106,5 @@ export default async function About() {
|
|||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -1,32 +1,28 @@
|
|||||||
import BlogContent from "@/sections/BlogContent";
|
import BlogContent from "@/sections/BlogContent"
|
||||||
import getPostMetadata from "@/utils/getPostMetaData";
|
import getPostMetadata from "@/utils/getPostMetaData"
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers"
|
||||||
import React from "react";
|
import React from "react"
|
||||||
import Spacer from "@/components/Utilities/Spacer";
|
import Spacer from "@/components/Utilities/Spacer"
|
||||||
import pb from "@/lib/pocketbase";
|
import pb from "@/lib/pocketbase"
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer"
|
||||||
import Background from "@/components/Utilities/Background";
|
import Background from "@/components/Utilities/Background"
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }) {
|
||||||
params,
|
const { slug } = params
|
||||||
}: {
|
const headersList = headers()
|
||||||
params: { slug: string };
|
const siteURL = headersList.get("host")
|
||||||
}) {
|
pb.autoCancellation(false)
|
||||||
const { slug } = params;
|
|
||||||
const headersList = headers();
|
|
||||||
const siteURL = headersList.get("host");
|
|
||||||
pb.autoCancellation(false);
|
|
||||||
const post = await pb.collection("blog").getFirstListItem(`slug="${slug}"`, {
|
const post = await pb.collection("blog").getFirstListItem(`slug="${slug}"`, {
|
||||||
requestKey: "metaData",
|
requestKey: "metaData"
|
||||||
});
|
})
|
||||||
pb.autoCancellation(true);
|
pb.autoCancellation(true)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${post.title} | FastPocket`,
|
title: `${post.title} | FastPocket`,
|
||||||
authors: [
|
authors: [
|
||||||
{
|
{
|
||||||
name: post.author || "Samuel Wyndham",
|
name: post.author || "Samuel Wyndham"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
description: post.description,
|
description: post.description,
|
||||||
keywords: post.keywords,
|
keywords: post.keywords,
|
||||||
@ -44,9 +40,9 @@ export async function generateMetadata({
|
|||||||
width: 1024,
|
width: 1024,
|
||||||
height: 576,
|
height: 576,
|
||||||
alt: post.title,
|
alt: post.title,
|
||||||
type: "image/png",
|
type: "image/png"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
@ -59,32 +55,32 @@ export async function generateMetadata({
|
|||||||
url: `${post.imageUrl}`,
|
url: `${post.imageUrl}`,
|
||||||
width: 1024,
|
width: 1024,
|
||||||
height: 576,
|
height: 576,
|
||||||
alt: post.title,
|
alt: post.title
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `https://${siteURL}/blogs/${post.slug}`,
|
canonical: `https://${siteURL}/blogs/${post.slug}`
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateStaticParams = async () => {
|
export const generateStaticParams = async () => {
|
||||||
const posts = await getPostMetadata();
|
const posts = await getPostMetadata()
|
||||||
console.log("static posts", posts.length);
|
console.log("static posts", posts.length)
|
||||||
const mappedPosts = posts.map((post) => ({
|
const mappedPosts = posts.map(post => ({
|
||||||
slug: post.slug,
|
slug: post.slug
|
||||||
}));
|
}))
|
||||||
return mappedPosts;
|
return mappedPosts
|
||||||
};
|
}
|
||||||
|
|
||||||
const PostPage = async (props: any) => {
|
const PostPage = async props => {
|
||||||
console.log("params", props.params);
|
console.log("params", props.params)
|
||||||
const post = await pb
|
const post = await pb
|
||||||
.collection("blog")
|
.collection("blog")
|
||||||
.getFirstListItem(`slug="` + props.params.slug + `"`, {
|
.getFirstListItem(`slug="` + props.params.slug + `"`, {
|
||||||
requestKey: "post",
|
requestKey: "post"
|
||||||
});
|
})
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
id="content"
|
id="content"
|
||||||
@ -99,7 +95,7 @@ const PostPage = async (props: any) => {
|
|||||||
<Footer />
|
<Footer />
|
||||||
</Background>
|
</Background>
|
||||||
</main>
|
</main>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default PostPage;
|
export default PostPage
|
@ -1,17 +1,17 @@
|
|||||||
import PageHeader from "@/sections/PageHeader";
|
import PageHeader from "@/sections/PageHeader"
|
||||||
import BlogCard from "@/components/BlogCard";
|
import BlogCard from "@/components/BlogCard"
|
||||||
import getPostMetadata from "@/utils/getPostMetaData";
|
import getPostMetadata from "@/utils/getPostMetaData"
|
||||||
import React from "react";
|
import React from "react"
|
||||||
import Background from "@/components/Utilities/Background";
|
import Background from "@/components/Utilities/Background"
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer"
|
||||||
|
|
||||||
export default async function BlogsPage() {
|
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
|
const postPreviews = postMetadata
|
||||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
.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 (
|
return (
|
||||||
<main
|
<main
|
||||||
id="content"
|
id="content"
|
||||||
@ -40,5 +40,5 @@ export default async function BlogsPage() {
|
|||||||
<Footer />
|
<Footer />
|
||||||
</Background>
|
</Background>
|
||||||
</main>
|
</main>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer"
|
||||||
import Background from "@/components/Utilities/Background";
|
import Background from "@/components/Utilities/Background"
|
||||||
import Spacer from "@/components/Utilities/Spacer";
|
import Spacer from "@/components/Utilities/Spacer"
|
||||||
import FormLeftDescriptionRightContactUs from "@/sections/ContactUs/FormLeftDescriptionRightContactUs";
|
import FormLeftDescriptionRightContactUs from "@/sections/ContactUs/FormLeftDescriptionRightContactUs"
|
||||||
import PageHeader from "@/sections/PageHeader";
|
import PageHeader from "@/sections/PageHeader"
|
||||||
import React from "react";
|
import React from "react"
|
||||||
|
|
||||||
const page = () => {
|
const page = () => {
|
||||||
return (
|
return (
|
||||||
@ -25,7 +25,7 @@ const page = () => {
|
|||||||
<Footer />
|
<Footer />
|
||||||
</Background>
|
</Background>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default page;
|
export default page
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
function layout({ children }: { children: React.ReactNode }) {
|
function layout({ children }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{children}
|
{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"
|
"use client"
|
||||||
|
|
||||||
import React from "react";
|
import React from "react"
|
||||||
import PageHeader from "@/sections/PageHeader";
|
import PageHeader from "@/sections/PageHeader"
|
||||||
import Newsletter from "@/sections/Newsletter/Newsletter";
|
import Payment from "@/sections/Payment"
|
||||||
import Background from "@/components/Utilities/Background";
|
import Newsletter from "@/sections/Newsletter/Newsletter"
|
||||||
import Footer from "@/components/Footer";
|
import Background from "@/components/Utilities/Background"
|
||||||
import Payment from "@/sections/Payment";
|
import Footer from "@/components/Footer"
|
||||||
import Spacer from "@/components/Utilities/Spacer";
|
import Spacer from "@/components/Utilities/Spacer"
|
||||||
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature";
|
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature"
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
return (
|
return (
|
||||||
@ -39,10 +39,10 @@ export default function PricingPage() {
|
|||||||
|
|
||||||
<Payment type="one_time" />
|
<Payment type="one_time" />
|
||||||
<Spacer className="mt-auto" />
|
<Spacer className="mt-auto" />
|
||||||
<Newsletter />
|
<Newsletter/>
|
||||||
<Spacer className="mb-8" />
|
<Spacer className="mb-8" />
|
||||||
<Footer />
|
<Footer />
|
||||||
</Background>
|
</Background>
|
||||||
</main>
|
</main>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -1,11 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer"
|
||||||
import Background from "@/components/Utilities/Background";
|
import Background from "@/components/Utilities/Background"
|
||||||
import Spacer from "@/components/Utilities/Spacer";
|
import Spacer from "@/components/Utilities/Spacer"
|
||||||
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature";
|
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature"
|
||||||
import PageHeader from "@/sections/PageHeader";
|
import PageHeader from "@/sections/PageHeader"
|
||||||
import React from "react";
|
import React from "react"
|
||||||
|
|
||||||
const page = () => {
|
const page = () => {
|
||||||
return (
|
return (
|
||||||
@ -28,7 +28,7 @@ const page = () => {
|
|||||||
<Footer />
|
<Footer />
|
||||||
</Background>
|
</Background>
|
||||||
</div>
|
</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 */
|
/* eslint-disable @next/next/no-before-interactive-script-outside-document */
|
||||||
import "./globals.css";
|
import "./globals.css"
|
||||||
import "../app/globals.css";
|
import "../app/globals.css"
|
||||||
import { Arimo, Indie_Flower } from "next/font/google";
|
import { Arimo, Indie_Flower } from "next/font/google"
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify"
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css"
|
||||||
import Header from "@/components/Header";
|
import { cookies } from "next/headers"
|
||||||
import { cookies } from "next/headers";
|
import { isAuthenticated } from "@/lib/auth"
|
||||||
import { isAuthenticated } from "@/lib/auth";
|
import { GTagProvider, PHProvider, ThemeProvider } from "./providers"
|
||||||
import { GTagProvider, PHProvider, ThemeProvider } from "./providers";
|
import Script from "next/script"
|
||||||
import Script from "next/script";
|
|
||||||
|
|
||||||
const raleway = Arimo({
|
const raleway = Arimo({
|
||||||
variable: "--body-font",
|
variable: "--body-font",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
display: "swap",
|
display: "swap"
|
||||||
});
|
})
|
||||||
|
|
||||||
const arimo = Arimo({
|
const arimo = Arimo({
|
||||||
variable: "--body-font",
|
variable: "--body-font",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
display: "swap",
|
display: "swap"
|
||||||
});
|
})
|
||||||
|
|
||||||
const indieFlower = Indie_Flower({
|
const indieFlower = Indie_Flower({
|
||||||
variable: "--accent-font",
|
variable: "--accent-font",
|
||||||
weight: "400",
|
weight: "400",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
display: "swap",
|
display: "swap"
|
||||||
});
|
})
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({ children }) {
|
||||||
children,
|
const isUserLoggedIn = await isAuthenticated(cookies())
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const isUserLoggedIn = await isAuthenticated(cookies());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
@ -46,7 +41,10 @@ export default async function RootLayout({
|
|||||||
<GTagProvider />
|
<GTagProvider />
|
||||||
<body className={`${arimo.className} bg-base-100 flex`}>
|
<body className={`${arimo.className} bg-base-100 flex`}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Header isUserLoggedIn={isUserLoggedIn} authString={cookies().get("pb_auth")?.value || ""} />
|
<Header
|
||||||
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
|
authString={cookies().get("pb_auth")?.value || ""}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
position="bottom-left"
|
position="bottom-left"
|
||||||
@ -76,7 +74,7 @@ export default async function RootLayout({
|
|||||||
id="promotekit-js"
|
id="promotekit-js"
|
||||||
async
|
async
|
||||||
defer
|
defer
|
||||||
strategy="afterInteractive"
|
strategy="afterInteractive"
|
||||||
src="https://cdn.promotekit.com/promotekit.js"
|
src="https://cdn.promotekit.com/promotekit.js"
|
||||||
data-promotekit="41c8e339-d2aa-414b-88b8-31b6d6346e2b"
|
data-promotekit="41c8e339-d2aa-414b-88b8-31b6d6346e2b"
|
||||||
/>
|
/>
|
||||||
@ -84,7 +82,7 @@ export default async function RootLayout({
|
|||||||
id="promotekit"
|
id="promotekit"
|
||||||
async
|
async
|
||||||
defer
|
defer
|
||||||
strategy="afterInteractive"
|
strategy="afterInteractive"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
@ -103,11 +101,11 @@ export default async function RootLayout({
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
`,
|
`
|
||||||
}}
|
}}
|
||||||
></Script>
|
></Script>
|
||||||
</body>
|
</body>
|
||||||
</PHProvider>
|
</PHProvider>
|
||||||
</html>
|
</html>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -1,7 +1,5 @@
|
|||||||
import Footer from "@/components/Footer";
|
import Background from "@/components/Utilities/Background"
|
||||||
import Background from "@/components/Utilities/Background";
|
import Link from "next/link"
|
||||||
import PageWrapper from "@/components/Utilities/PageWrapper";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
@ -70,5 +68,5 @@ export default function NotFound() {
|
|||||||
</Background>
|
</Background>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -1,22 +1,17 @@
|
|||||||
import "aos/dist/aos.css";
|
import "aos/dist/aos.css"
|
||||||
import React from "react";
|
import React from "react"
|
||||||
import { SquaredBackgroundHero } from "@/sections/Hero";
|
import { SquaredBackgroundHero } from "@/sections/Hero"
|
||||||
import PageWrapper from "@/components/Utilities/PageWrapper";
|
import PageWrapper from "@/components/Utilities/PageWrapper"
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer"
|
||||||
import PageHeader from "@/sections/PageHeader";
|
import PageHeader from "@/sections/PageHeader"
|
||||||
import ContainerImageIconBlocksFeature from "@/sections/Features/ContainerImageIconBlocksFeature";
|
import ContainerImageIconBlocksFeature from "@/sections/Features/ContainerImageIconBlocksFeature"
|
||||||
import Background from "@/components/Utilities/Background";
|
import Background from "@/components/Utilities/Background"
|
||||||
import CardTestemonial from "@/sections/Testemonial/CardTestemonial";
|
import CardTestemonial from "@/sections/Testemonial/CardTestemonial"
|
||||||
import Payment from "@/sections/Payment";
|
import CardsFeature from "@/sections/Features/CardsFeature"
|
||||||
import CardsFeature from "@/sections/Features/CardsFeature";
|
import FAQ from "@/sections/FAQ/RightAlignedBorderBottomFAQ"
|
||||||
import FAQ from "@/sections/FAQ/RightAlignedBorderBottomFAQ";
|
import VerticalTabsFeature from "@/sections/Features/VerticalTabsFeature"
|
||||||
import VerticalTabsFeature from "@/sections/Features/VerticalTabsFeature";
|
|
||||||
|
|
||||||
import { Metadata } from "next";
|
export const metadata = {
|
||||||
import Head from "next/head";
|
|
||||||
import Teste from "@/sections/Testing/Teste";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Pocketbase, Stripe, Next.js, Boilerplate | FastPocket",
|
title: "Pocketbase, Stripe, Next.js, Boilerplate | FastPocket",
|
||||||
description:
|
description:
|
||||||
"FastPocket - Is a boilerplate codebase for everyone to build a product quickly.",
|
"FastPocket - Is a boilerplate codebase for everyone to build a product quickly.",
|
||||||
@ -26,7 +21,7 @@ export const metadata: Metadata = {
|
|||||||
"next.js",
|
"next.js",
|
||||||
"boilerplate",
|
"boilerplate",
|
||||||
"template",
|
"template",
|
||||||
"codebase",
|
"codebase"
|
||||||
],
|
],
|
||||||
openGraph: {
|
openGraph: {
|
||||||
url: "https://fastpocket.dev",
|
url: "https://fastpocket.dev",
|
||||||
@ -39,9 +34,9 @@ export const metadata: Metadata = {
|
|||||||
url: "https://fastpocket.dev/images/home/thumbnail.png",
|
url: "https://fastpocket.dev/images/home/thumbnail.png",
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
alt: "fastpocket.degv",
|
alt: "fastpocket.degv"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
@ -55,24 +50,19 @@ export const metadata: Metadata = {
|
|||||||
url: "https://fastpocket.dev/images/home/thumbnail.png",
|
url: "https://fastpocket.dev/images/home/thumbnail.png",
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
alt: "fastpocket.degv",
|
alt: "fastpocket.degv"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: "https://fastpocket.dev",
|
canonical: "https://fastpocket.dev"
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<SquaredBackgroundHero />
|
|
||||||
|
|
||||||
<Teste/>
|
|
||||||
|
|
||||||
|
|
||||||
<SquaredBackgroundHero />
|
<SquaredBackgroundHero />
|
||||||
<VerticalTabsFeature />
|
<VerticalTabsFeature />
|
||||||
<div className="bg-primary/2">
|
<div className="bg-primary/2">
|
||||||
@ -105,5 +95,5 @@ export default function Home() {
|
|||||||
<Footer />
|
<Footer />
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import posthog from 'posthog-js'
|
import posthog from 'posthog-js'
|
||||||
import { PostHogProvider } from 'posthog-js/react'
|
import { PostHogProvider } from 'posthog-js/react'
|
||||||
import GoogleAnalytics from "@/components/GoogleAnalytics";
|
|
||||||
import { ThemeProvider as NTThemeProvider } from 'next-themes'
|
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 Image from "next/image";
|
import Link from "next/link"
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
function BlogCard({
|
function BlogCard({ title, slug, subtitle, imageUrl }) {
|
||||||
title,
|
|
||||||
slug,
|
|
||||||
subtitle,
|
|
||||||
imageUrl,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
slug: string;
|
|
||||||
subtitle: string;
|
|
||||||
imageUrl: string;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
@ -37,7 +26,7 @@ function BlogCard({
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BlogCard;
|
export default BlogCard
|
@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react"
|
||||||
const SvgComponent = (props: any) => (
|
const SvgComponent = props => (
|
||||||
<a href="https://fastpocket.dev">
|
<a href="https://fastpocket.dev">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -43,5 +43,5 @@ const SvgComponent = (props: any) => (
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
);
|
)
|
||||||
export default SvgComponent;
|
export default SvgComponent
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react"
|
||||||
import { title } from "@/constants";
|
import { title } from "@/constants"
|
||||||
import Logo from "@/components/Logo";
|
import Logo from "@/components/Logo"
|
||||||
import Link from "next/link";
|
import Link from "next/link"
|
||||||
import FastPocketBadge from "@/components/FastPocketBadge"
|
import FastPocketBadge from "@/components/FastPocketBadge"
|
||||||
|
|
||||||
function Footer() {
|
function Footer() {
|
||||||
@ -253,7 +253,7 @@ function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Footer;
|
export default Footer
|
@ -1,18 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Subscription, User } from "@/types";
|
|
||||||
import { getSubscriptions } from "@/app/actions";
|
import { getSubscriptions } from "@/app/actions";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
interface GetStartedSectionButtonProps {
|
export const GetStartedSectionButton = ({ user }) => {
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
export const GetStartedSectionButton = ({
|
|
||||||
user,
|
|
||||||
}: GetStartedSectionButtonProps) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [subscription, setSubscription] = useState<Subscription>();
|
const [subscription, setSubscription] = useState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -37,4 +32,4 @@ export const GetStartedSectionButton = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GetStartedSectionButton;
|
export default GetStartedSectionButton;
|
@ -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
|
<Script
|
||||||
async
|
async
|
||||||
@ -20,9 +20,9 @@ const GoogleAnalytics = ({ ga_id }: { ga_id: string }) => (
|
|||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
|
|
||||||
gtag('config', '${ga_id}');
|
gtag('config', '${ga_id}');
|
||||||
`,
|
`
|
||||||
}}
|
}}
|
||||||
></Script>
|
></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 {
|
function Logo({ imageProps, ...props }) {
|
||||||
label?: string;
|
|
||||||
className?: string;
|
|
||||||
imageProps?: ImageProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Logo({ imageProps, ...props }: LogoProps) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
@ -26,7 +20,7 @@ function Logo({ imageProps, ...props }: LogoProps) {
|
|||||||
{...imageProps}
|
{...imageProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Logo;
|
export default Logo
|
@ -1,31 +1,31 @@
|
|||||||
import React, { useRef } from "react";
|
import React from "react"
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form"
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup"
|
||||||
import { emailValidationSchema } from "@/utils/form";
|
import { emailValidationSchema } from "@/utils/form"
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify"
|
||||||
import pb from "@/lib/pocketbase";
|
import pb from "@/lib/pocketbase"
|
||||||
import {Dismiss20Filled} from "@fluentui/react-icons"
|
import { Dismiss20Filled } from "@fluentui/react-icons"
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
function ModalEmailChange({authString}:{authString:string}) {
|
function ModalEmailChange({ authString }) {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting }
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: yupResolver(emailValidationSchema),
|
resolver: yupResolver(emailValidationSchema)
|
||||||
});
|
})
|
||||||
|
|
||||||
const onSubmit = async (data: any) => {
|
const onSubmit = async data => {
|
||||||
console.log(data);
|
console.log(data)
|
||||||
pb.authStore.loadFromCookie(authString);
|
pb.authStore.loadFromCookie(authString)
|
||||||
!pb.authStore.isValid && redirect("/");
|
!pb.authStore.isValid && redirect("/")
|
||||||
try {
|
try {
|
||||||
if (await pb.collection("user").requestEmailChange(data.email)) {
|
if (await pb.collection("user").requestEmailChange(data.email)) {
|
||||||
reset();
|
reset()
|
||||||
document.getElementById("email-change-modal")?.click();
|
document.getElementById("email-change-modal")?.click()
|
||||||
document.getElementById("sign-in-modal")?.click();
|
document.getElementById("sign-in-modal")?.click()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@ -37,11 +37,11 @@ function ModalEmailChange({authString}:{authString:string}) {
|
|||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
progress: undefined,
|
progress: undefined,
|
||||||
theme: "colored",
|
theme: "colored"
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
@ -49,7 +49,7 @@ function ModalEmailChange({authString}:{authString:string}) {
|
|||||||
id="email-change-modal"
|
id="email-change-modal"
|
||||||
className="modal-toggle"
|
className="modal-toggle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset();
|
reset()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="email-change-modal" className="modal cursor-pointer">
|
<label htmlFor="email-change-modal" className="modal cursor-pointer">
|
||||||
@ -88,7 +88,7 @@ function ModalEmailChange({authString}:{authString:string}) {
|
|||||||
<button
|
<button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
type="submit"
|
type="submit"
|
||||||
className={isSubmitting ? "btn btn-gray": "btn btn-primary"}
|
className={isSubmitting ? "btn btn-gray" : "btn btn-primary"}
|
||||||
>
|
>
|
||||||
Change Email
|
Change Email
|
||||||
{isSubmitting && <div className="loading"></div>}
|
{isSubmitting && <div className="loading"></div>}
|
||||||
@ -99,7 +99,7 @@ function ModalEmailChange({authString}:{authString:string}) {
|
|||||||
</label>
|
</label>
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ModalEmailChange;
|
export default ModalEmailChange
|
@ -1,30 +1,29 @@
|
|||||||
import React, { useRef } from "react";
|
import React from "react"
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form"
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup"
|
||||||
import { emailValidationSchema } from "@/utils/form";
|
import { emailValidationSchema } from "@/utils/form"
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify"
|
||||||
import pb from "@/lib/pocketbase";
|
import pb from "@/lib/pocketbase"
|
||||||
import {Dismiss20Filled} from '@fluentui/react-icons'
|
import { Dismiss20Filled } from "@fluentui/react-icons"
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
function ModalPasswordReset() {
|
function ModalPasswordReset() {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting }
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: yupResolver(emailValidationSchema),
|
resolver: yupResolver(emailValidationSchema)
|
||||||
});
|
})
|
||||||
|
|
||||||
const onSubmit = async (data: any) => {
|
const onSubmit = async data => {
|
||||||
console.log(data);
|
console.log(data)
|
||||||
try {
|
try {
|
||||||
//login user
|
//login user
|
||||||
if (await pb.collection("user").requestPasswordReset(data.email)) {
|
if (await pb.collection("user").requestPasswordReset(data.email)) {
|
||||||
reset();
|
reset()
|
||||||
document.getElementById("password-reset-modal")?.click();
|
document.getElementById("password-reset-modal")?.click()
|
||||||
document.getElementById("sign-in-modal")?.click();
|
document.getElementById("sign-in-modal")?.click()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@ -36,11 +35,11 @@ function ModalPasswordReset() {
|
|||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
progress: undefined,
|
progress: undefined,
|
||||||
theme: "colored",
|
theme: "colored"
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
@ -48,7 +47,7 @@ function ModalPasswordReset() {
|
|||||||
id="password-reset-modal"
|
id="password-reset-modal"
|
||||||
className="modal-toggle"
|
className="modal-toggle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset();
|
reset()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="password-reset-modal" className="modal cursor-pointer">
|
<label htmlFor="password-reset-modal" className="modal cursor-pointer">
|
||||||
@ -98,7 +97,7 @@ function ModalPasswordReset() {
|
|||||||
</label>
|
</label>
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ModalPasswordReset;
|
export default ModalPasswordReset
|
@ -1,36 +1,34 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import React from "react";
|
import React from "react"
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form"
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup"
|
||||||
import { signInValidationSchema } from "@/utils/form";
|
import { signInValidationSchema } from "@/utils/form"
|
||||||
import { login } from "@/app/actions";
|
import { login } from "@/app/actions"
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify"
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation"
|
||||||
import {Dismiss20Filled} from '@fluentui/react-icons'
|
import { Dismiss20Filled } from "@fluentui/react-icons"
|
||||||
|
|
||||||
function ModalSignIn() {
|
function ModalSignIn() {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting }
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: yupResolver(signInValidationSchema),
|
resolver: yupResolver(signInValidationSchema)
|
||||||
});
|
})
|
||||||
|
|
||||||
const onSubmit = async (data: any) => {
|
const onSubmit = async data => {
|
||||||
try {
|
try {
|
||||||
//login user
|
//login user
|
||||||
const auth = (await login({ email: data.email, password: data.password }))
|
const auth = await login({ email: data.email, password: data.password })
|
||||||
if (
|
if (auth.success) {
|
||||||
auth.success
|
reset()
|
||||||
) {
|
document.getElementById("sign-in-modal")?.click()
|
||||||
reset();
|
router.push("/account")
|
||||||
document.getElementById("sign-in-modal")?.click();
|
|
||||||
router.push("/account");
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(auth.response.message)
|
throw new Error(auth.response.message)
|
||||||
}
|
}
|
||||||
@ -44,11 +42,11 @@ function ModalSignIn() {
|
|||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
progress: undefined,
|
progress: undefined,
|
||||||
theme: "colored",
|
theme: "colored"
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
@ -56,7 +54,7 @@ function ModalSignIn() {
|
|||||||
id="sign-in-modal"
|
id="sign-in-modal"
|
||||||
className="modal-toggle"
|
className="modal-toggle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset();
|
reset()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="sign-in-modal" className="modal cursor-pointer">
|
<label htmlFor="sign-in-modal" className="modal cursor-pointer">
|
||||||
@ -120,8 +118,8 @@ function ModalSignIn() {
|
|||||||
|
|
||||||
<span
|
<span
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
document.getElementById("sign-in-modal")?.click();
|
document.getElementById("sign-in-modal")?.click()
|
||||||
document.getElementById("sign-up-modal")?.click();
|
document.getElementById("sign-up-modal")?.click()
|
||||||
}}
|
}}
|
||||||
className="text-primary hover:text-primary/60 cursor-pointer "
|
className="text-primary hover:text-primary/60 cursor-pointer "
|
||||||
>
|
>
|
||||||
@ -143,7 +141,7 @@ function ModalSignIn() {
|
|||||||
</label>
|
</label>
|
||||||
</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";
|
function ModalSignUp({ setUser }) {
|
||||||
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>>;
|
|
||||||
}) {
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting }
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: yupResolver(signUpValidationSchema),
|
resolver: yupResolver(signUpValidationSchema)
|
||||||
});
|
})
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const generateCheckoutPage = async (price: Price, type: string) => {
|
const generateCheckoutPage = async (price, type) => {
|
||||||
try {
|
try {
|
||||||
const checkoutSessionResponse = await createCheckoutSession(
|
const checkoutSessionResponse = await createCheckoutSession(
|
||||||
price.price_id,
|
price.price_id,
|
||||||
type
|
type
|
||||||
);
|
)
|
||||||
console.log(checkoutSessionResponse);
|
console.log(checkoutSessionResponse)
|
||||||
router.push(checkoutSessionResponse.url);
|
router.push(checkoutSessionResponse.url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
toast.error(error.message, {
|
toast.error(error.message, {
|
||||||
@ -44,53 +38,51 @@ function ModalSignUp({
|
|||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
progress: undefined,
|
progress: undefined,
|
||||||
theme: "colored",
|
theme: "colored"
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const onSubmit = async (data: any) => {
|
const onSubmit = async data => {
|
||||||
data = {
|
data = {
|
||||||
emailVisibility: false,
|
emailVisibility: false,
|
||||||
lastSeen: new Date(),
|
lastSeen: new Date(),
|
||||||
role: "Admin",
|
role: "Admin",
|
||||||
displayName: `${data.firstName} ${data.lastName}`,
|
displayName: `${data.firstName} ${data.lastName}`,
|
||||||
passwordConfirm: data.password,
|
passwordConfirm: data.password,
|
||||||
...data,
|
...data
|
||||||
};
|
}
|
||||||
try {
|
try {
|
||||||
//create organisation
|
//create organisation
|
||||||
const organisation = await pb.collection("organisation").create({
|
const organisation = await pb.collection("organisation").create({
|
||||||
name: data.organisation,
|
name: data.organisation,
|
||||||
organisationSize: data.organisationSize,
|
organisationSize: data.organisationSize
|
||||||
});
|
})
|
||||||
//create user
|
//create user
|
||||||
const user = await pb
|
const user = await pb
|
||||||
.collection("user")
|
.collection("user")
|
||||||
.create({ ...data, organisation: organisation.id });
|
.create({ ...data, organisation: organisation.id })
|
||||||
//login user
|
//login user
|
||||||
const auth = (await login({ email: data.email, password: data.password }))
|
const auth = await login({ email: data.email, password: data.password })
|
||||||
if (
|
if (auth.success) {
|
||||||
auth.success
|
reset()
|
||||||
) {
|
document.getElementById("sign-up-modal")?.click()
|
||||||
reset();
|
const price = localStorage.getItem("price")
|
||||||
document.getElementById("sign-up-modal")?.click();
|
const type = localStorage.getItem("type")
|
||||||
const price = localStorage.getItem("price");
|
setUser(user)
|
||||||
const type = localStorage.getItem("type");
|
console.log("price", price)
|
||||||
setUser(user as User);
|
console.log("type", type)
|
||||||
console.log("price", price);
|
|
||||||
console.log("type", type);
|
|
||||||
price
|
price
|
||||||
? generateCheckoutPage(JSON.parse(price), type ?? "")
|
? generateCheckoutPage(JSON.parse(price), type ?? "")
|
||||||
: router.push("/account");
|
: router.push("/account")
|
||||||
} else {
|
} else {
|
||||||
throw new Error(auth.response.message)
|
throw new Error(auth.response.message)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
Object.values((error as any).data.data)
|
Object.values(error.data.data)
|
||||||
.map((x: any) => x.message)
|
.map(x => x.message)
|
||||||
.join(),
|
.join(),
|
||||||
{
|
{
|
||||||
position: "bottom-left",
|
position: "bottom-left",
|
||||||
@ -100,12 +92,12 @@ function ModalSignUp({
|
|||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
progress: undefined,
|
progress: undefined,
|
||||||
theme: "colored",
|
theme: "colored"
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
@ -114,7 +106,7 @@ function ModalSignUp({
|
|||||||
className="modal-toggle"
|
className="modal-toggle"
|
||||||
name=""
|
name=""
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset();
|
reset()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="sign-up-modal" className="modal cursor-pointer">
|
<label htmlFor="sign-up-modal" className="modal cursor-pointer">
|
||||||
@ -124,7 +116,7 @@ function ModalSignUp({
|
|||||||
htmlFor="sign-up-modal"
|
htmlFor="sign-up-modal"
|
||||||
className="cursor-pointer text-base-content"
|
className="cursor-pointer text-base-content"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset();
|
reset()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dismiss20Filled />
|
<Dismiss20Filled />
|
||||||
@ -225,7 +217,7 @@ function ModalSignUp({
|
|||||||
>
|
>
|
||||||
{companySizeOption}
|
{companySizeOption}
|
||||||
</option>
|
</option>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
<div className="text-start text-sm italic text-error-content">
|
<div className="text-start text-sm italic text-error-content">
|
||||||
@ -265,8 +257,8 @@ function ModalSignUp({
|
|||||||
|
|
||||||
<span
|
<span
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
document.getElementById("sign-up-modal")?.click();
|
document.getElementById("sign-up-modal")?.click()
|
||||||
document.getElementById("sign-in-modal")?.click();
|
document.getElementById("sign-in-modal")?.click()
|
||||||
}}
|
}}
|
||||||
className="text-primary hover:text-primary/60 cursor-pointer "
|
className="text-primary hover:text-primary/60 cursor-pointer "
|
||||||
>
|
>
|
||||||
@ -279,7 +271,7 @@ function ModalSignUp({
|
|||||||
</label>
|
</label>
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ModalSignUp;
|
export default ModalSignUp
|
@ -1,15 +1,15 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import Logo from "@/components/Logo";
|
import Logo from "@/components/Logo"
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react"
|
||||||
import Link from "next/link";
|
import Link from "next/link"
|
||||||
import {Navigation24Filled} from "@fluentui/react-icons"
|
import { Navigation24Filled } from "@fluentui/react-icons"
|
||||||
|
|
||||||
function Navigation({ isUserLoggedIn }: { isUserLoggedIn: boolean }) {
|
function Navigation({ isUserLoggedIn }) {
|
||||||
const [checked, setChecked] = useState<boolean>();
|
const [checked, setChecked] = useState()
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
checked ? setChecked(!checked) : setChecked(checked);
|
checked ? setChecked(!checked) : setChecked(checked)
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<div className="drawer max-w-fit">
|
<div className="drawer max-w-fit">
|
||||||
<input
|
<input
|
||||||
@ -23,10 +23,7 @@ function Navigation({ isUserLoggedIn }: { isUserLoggedIn: boolean }) {
|
|||||||
{/* Navbar */}
|
{/* Navbar */}
|
||||||
<div className="flex-grow navbar ">
|
<div className="flex-grow navbar ">
|
||||||
<div className="flex-none lg:hidden">
|
<div className="flex-none lg:hidden">
|
||||||
<label
|
<label htmlFor="my-drawer-3" className="items-center">
|
||||||
htmlFor="my-drawer-3"
|
|
||||||
className="items-center"
|
|
||||||
>
|
|
||||||
<Navigation24Filled />
|
<Navigation24Filled />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -107,7 +104,7 @@ function Navigation({ isUserLoggedIn }: { isUserLoggedIn: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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";
|
export default function PriceCard({ product, isAnnual, loading }) {
|
||||||
import { createCheckoutSession, isAuthenticated } from "@/app/actions";
|
const router = useRouter()
|
||||||
import { toast } from "react-toastify";
|
const openSignUpModalOnPriceClick = (price, type) => {
|
||||||
import { useRouter } from "next/navigation";
|
const signUpModal = document.getElementById("sign-up-modal")
|
||||||
import {CheckmarkCircle24Filled } from "@fluentui/react-icons"
|
if (!signUpModal) return
|
||||||
|
signUpModal.click()
|
||||||
export default function PriceCard({
|
}
|
||||||
product,
|
const generateCheckoutPage = async (price, type) => {
|
||||||
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) => {
|
|
||||||
try {
|
try {
|
||||||
const checkoutSessionResponse = await createCheckoutSession(
|
const checkoutSessionResponse = await createCheckoutSession(
|
||||||
price.price_id,
|
price.price_id,
|
||||||
type
|
type
|
||||||
);
|
)
|
||||||
console.log(checkoutSessionResponse);
|
console.log(checkoutSessionResponse)
|
||||||
router.push(checkoutSessionResponse.url);
|
router.push(checkoutSessionResponse.url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
toast.error(error.message, {
|
toast.error(error.message, {
|
||||||
@ -39,25 +29,25 @@ export default function PriceCard({
|
|||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
progress: undefined,
|
progress: undefined,
|
||||||
theme: "colored",
|
theme: "colored"
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const submitForm = async (product: Product) => {
|
const submitForm = async product => {
|
||||||
const userIsAuthenticated = await isAuthenticated();
|
const userIsAuthenticated = await isAuthenticated()
|
||||||
console.log("userIsAuthenticated", userIsAuthenticated);
|
console.log("userIsAuthenticated", userIsAuthenticated)
|
||||||
const price = isAnnual ? product.yearlyPrice : product.monthlyPrice;
|
const price = isAnnual ? product.yearlyPrice : product.monthlyPrice
|
||||||
console.log("price", price);
|
console.log("price", price)
|
||||||
console.log("product.type", product.type);
|
console.log("product.type", product.type)
|
||||||
localStorage.setItem("price", JSON.stringify(price));
|
localStorage.setItem("price", JSON.stringify(price))
|
||||||
localStorage.setItem("type", product.type);
|
localStorage.setItem("type", product.type)
|
||||||
if (userIsAuthenticated) {
|
if (userIsAuthenticated) {
|
||||||
await generateCheckoutPage(price, product.type);
|
await generateCheckoutPage(price, product.type)
|
||||||
} else {
|
} else {
|
||||||
openSignUpModalOnPriceClick(price, product.type);
|
openSignUpModalOnPriceClick(price, product.type)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative w-64 sm:w-80 bg-base-200 rounded-lg p-6 shadow-lg border border-primary/40 ${
|
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>
|
</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 {
|
import {
|
||||||
DefaultAudioLayout,
|
DefaultAudioLayout,
|
||||||
defaultLayoutIcons,
|
defaultLayoutIcons,
|
||||||
DefaultVideoLayout,
|
DefaultVideoLayout
|
||||||
} from "@vidstack/react/player/layouts/default";
|
} from "@vidstack/react/player/layouts/default"
|
||||||
import "@vidstack/react/player/styles/base.css";
|
import "@vidstack/react/player/styles/base.css"
|
||||||
export type Props = {
|
|
||||||
videotitle: string;
|
|
||||||
video: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function YouTubeFrame({ video, width, height }: Props) {
|
export default function YouTubeFrame({ video, width, height }) {
|
||||||
return (
|
return (
|
||||||
<MediaPlayer
|
<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"
|
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"
|
thumbnails="https://media-files.vidstack.io/thumbnails.vtt"
|
||||||
/>
|
/>
|
||||||
</MediaPlayer>
|
</MediaPlayer>
|
||||||
);
|
)
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ export const companySizeList = [
|
|||||||
"30-70 employees",
|
"30-70 employees",
|
||||||
"70-100 employees",
|
"70-100 employees",
|
||||||
"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