newFeature/rebuild-in-js #15

Open
coliveira wants to merge 6 commits from newFeature/rebuild-in-js into main
134 changed files with 11762 additions and 2573 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8400
Documentation/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View 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>
)
)
}

View File

@ -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>
)
);
}

View File

@ -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}&nbsp;
</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>
)
}

View File

@ -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}&nbsp;
</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>
);
}

View File

@ -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}&nbsp;
</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}&nbsp;
</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>
)
}

View File

@ -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}&nbsp;
</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}&nbsp;
</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>
);
}

View File

@ -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}&nbsp;
</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}&nbsp;
</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>
)
}

View File

@ -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>
);
}

View File

@ -1,8 +1,8 @@
import PageHeader from "@/sections/PageHeader";
import React from "react";
import Footer from "@/components/Footer";
import Image from "next/image";
import Link from "next/link";
import PageHeader from "@/sections/PageHeader"
import React from "react"
import Footer from "@/components/Footer"
import Image from "next/image"
import Link from "next/link"
export default async function About() {
return (
@ -15,20 +15,45 @@ export default async function About() {
<PageHeader title="Helping Developers Build" />
<div className="max-w-4xl mx-auto mb-auto pb-24 h-full w-full py-12 px-8 flex flex-col gap-y-12 text-center items-center">
<p className="text-lg text-base-content font-thin">
In 2023 I built <Link href={"https://sign365.com.au"} className="font-bold text-secondary">Sign365</Link> using pocketbase
and setup an open source library to help people get setup with <Link href={"https://github.com/mrwyndham/pocketbase-stripe"} className="font-bold text-secondary">Stripe
+ Pocketbase</Link>. As 2024 has come around I have had more and more
requests for applications and features on the existing code. I built a
codebase that would save me 20 hours + in the bootstrapping time to
get my applications ready. That is why I had to build <Link href={"https://fastpocket.dev"} className="font-bold text-secondary">FastPocket</Link> an
easy solution to give everyone a head start.
In 2023 I built{" "}
<Link
href={"https://sign365.com.au"}
className="font-bold text-secondary"
>
Sign365
</Link>{" "}
using pocketbase and setup an open source library to help people get
setup with{" "}
<Link
href={"https://github.com/mrwyndham/pocketbase-stripe"}
className="font-bold text-secondary"
>
Stripe + Pocketbase
</Link>
. As 2024 has come around I have had more and more requests for
applications and features on the existing code. I built a codebase
that would save me 20 hours + in the bootstrapping time to get my
applications ready. That is why I had to build{" "}
<Link
href={"https://fastpocket.dev"}
className="font-bold text-secondary"
>
FastPocket
</Link>{" "}
an easy solution to give everyone a head start.
</p>
<p className="text-lg text-base-content font-thin">
After reflecting on the challenges that developers face building their
applications, I&apos;ve seen that there will never be one codebase
that suits all developers. But I am sure for those who are
opensourcing, self-hosting and want to spin up an application quickly
they will be able to do it using <Link href={"https://fastpocket.dev"} className="font-bold text-secondary">FastPocket</Link>
they will be able to do it using{" "}
<Link
href={"https://fastpocket.dev"}
className="font-bold text-secondary"
>
FastPocket
</Link>
</p>
<p className="text-lg text-base-content font-thin">
I am taking all of the knowledge that I have gained across 20+
@ -38,11 +63,23 @@ export default async function About() {
that have been battle tested in enterprise code
</p>
<p className="text-lg text-base-content font-thin">
So I&apos;ve committed to building <Link href={"https://fastpocket.dev"} className="font-bold text-secondary">FastPocket</Link> to help you build your
projects faster to get paid.
So I&apos;ve committed to building{" "}
<Link
href={"https://fastpocket.dev"}
className="font-bold text-secondary"
>
FastPocket
</Link>{" "}
to help you build your projects faster to get paid.
</p>
<p className="text-xl text-base-content font-bold">
<Link href={"https://fastpocket.dev"} className="font-bold text-secondary">FastPocket</Link> will get you producing more with less development.
<Link
href={"https://fastpocket.dev"}
className="font-bold text-secondary"
>
FastPocket
</Link>{" "}
will get you producing more with less development.
</p>
<p className="text-lg text-base-content font-thin">
I know that your next project will grow exponentially because of the
@ -69,5 +106,5 @@ export default async function About() {
</div>
<Footer />
</main>
);
)
}

View File

@ -1,32 +1,28 @@
import BlogContent from "@/sections/BlogContent";
import getPostMetadata from "@/utils/getPostMetaData";
import { headers } from "next/headers";
import React from "react";
import Spacer from "@/components/Utilities/Spacer";
import pb from "@/lib/pocketbase";
import Footer from "@/components/Footer";
import Background from "@/components/Utilities/Background";
import BlogContent from "@/sections/BlogContent"
import getPostMetadata from "@/utils/getPostMetaData"
import { headers } from "next/headers"
import React from "react"
import Spacer from "@/components/Utilities/Spacer"
import pb from "@/lib/pocketbase"
import Footer from "@/components/Footer"
import Background from "@/components/Utilities/Background"
export async function generateMetadata({
params,
}: {
params: { slug: string };
}) {
const { slug } = params;
const headersList = headers();
const siteURL = headersList.get("host");
pb.autoCancellation(false);
export async function generateMetadata({ params }) {
const { slug } = params
const headersList = headers()
const siteURL = headersList.get("host")
pb.autoCancellation(false)
const post = await pb.collection("blog").getFirstListItem(`slug="${slug}"`, {
requestKey: "metaData",
});
pb.autoCancellation(true);
requestKey: "metaData"
})
pb.autoCancellation(true)
return {
title: `${post.title} | FastPocket`,
authors: [
{
name: post.author || "Samuel Wyndham",
},
name: post.author || "Samuel Wyndham"
}
],
description: post.description,
keywords: post.keywords,
@ -44,9 +40,9 @@ export async function generateMetadata({
width: 1024,
height: 576,
alt: post.title,
type: "image/png",
},
],
type: "image/png"
}
]
},
twitter: {
card: "summary_large_image",
@ -59,32 +55,32 @@ export async function generateMetadata({
url: `${post.imageUrl}`,
width: 1024,
height: 576,
alt: post.title,
},
],
alt: post.title
}
]
},
alternates: {
canonical: `https://${siteURL}/blogs/${post.slug}`,
},
};
canonical: `https://${siteURL}/blogs/${post.slug}`
}
}
}
export const generateStaticParams = async () => {
const posts = await getPostMetadata();
console.log("static posts", posts.length);
const mappedPosts = posts.map((post) => ({
slug: post.slug,
}));
return mappedPosts;
};
const posts = await getPostMetadata()
console.log("static posts", posts.length)
const mappedPosts = posts.map(post => ({
slug: post.slug
}))
return mappedPosts
}
const PostPage = async (props: any) => {
console.log("params", props.params);
const PostPage = async props => {
console.log("params", props.params)
const post = await pb
.collection("blog")
.getFirstListItem(`slug="` + props.params.slug + `"`, {
requestKey: "post",
});
requestKey: "post"
})
return (
<main
id="content"
@ -99,7 +95,7 @@ const PostPage = async (props: any) => {
<Footer />
</Background>
</main>
);
};
)
}
export default PostPage;
export default PostPage

View File

@ -1,17 +1,17 @@
import PageHeader from "@/sections/PageHeader";
import BlogCard from "@/components/BlogCard";
import getPostMetadata from "@/utils/getPostMetaData";
import React from "react";
import Background from "@/components/Utilities/Background";
import Footer from "@/components/Footer";
import PageHeader from "@/sections/PageHeader"
import BlogCard from "@/components/BlogCard"
import getPostMetadata from "@/utils/getPostMetaData"
import React from "react"
import Background from "@/components/Utilities/Background"
import Footer from "@/components/Footer"
export default async function BlogsPage() {
const postMetadata = await getPostMetadata();
const postMetadata = await getPostMetadata()
console.log("blogs", postMetadata.length);
console.log("blogs", postMetadata.length)
const postPreviews = postMetadata
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.map((post) => <BlogCard key={post.slug} {...(post as any)} />);
.map(post => <BlogCard key={post.slug} {...post} />)
return (
<main
id="content"
@ -40,5 +40,5 @@ export default async function BlogsPage() {
<Footer />
</Background>
</main>
);
)
}

View File

@ -1,9 +1,9 @@
import Footer from "@/components/Footer";
import Background from "@/components/Utilities/Background";
import Spacer from "@/components/Utilities/Spacer";
import FormLeftDescriptionRightContactUs from "@/sections/ContactUs/FormLeftDescriptionRightContactUs";
import PageHeader from "@/sections/PageHeader";
import React from "react";
import Footer from "@/components/Footer"
import Background from "@/components/Utilities/Background"
import Spacer from "@/components/Utilities/Spacer"
import FormLeftDescriptionRightContactUs from "@/sections/ContactUs/FormLeftDescriptionRightContactUs"
import PageHeader from "@/sections/PageHeader"
import React from "react"
const page = () => {
return (
@ -25,7 +25,7 @@ const page = () => {
<Footer />
</Background>
</div>
);
};
)
}
export default page;
export default page

View File

@ -1,6 +1,6 @@
import React from "react";
function layout({ children }: { children: React.ReactNode }) {
function layout({ children }) {
return (
<>
{children}

View 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 []
}
}

View File

@ -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 [];
}
}

View File

@ -1,13 +1,13 @@
"use client"
import React from "react";
import PageHeader from "@/sections/PageHeader";
import Newsletter from "@/sections/Newsletter/Newsletter";
import Background from "@/components/Utilities/Background";
import Footer from "@/components/Footer";
import Payment from "@/sections/Payment";
import Spacer from "@/components/Utilities/Spacer";
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature";
import React from "react"
import PageHeader from "@/sections/PageHeader"
import Payment from "@/sections/Payment"
import Newsletter from "@/sections/Newsletter/Newsletter"
import Background from "@/components/Utilities/Background"
import Footer from "@/components/Footer"
import Spacer from "@/components/Utilities/Spacer"
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature"
export default function PricingPage() {
return (
@ -39,10 +39,10 @@ export default function PricingPage() {
<Payment type="one_time" />
<Spacer className="mt-auto" />
<Newsletter />
<Newsletter/>
<Spacer className="mb-8" />
<Footer />
</Background>
</main>
);
)
}

View File

@ -1,11 +1,11 @@
"use client"
import Footer from "@/components/Footer";
import Background from "@/components/Utilities/Background";
import Spacer from "@/components/Utilities/Spacer";
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature";
import PageHeader from "@/sections/PageHeader";
import React from "react";
import Footer from "@/components/Footer"
import Background from "@/components/Utilities/Background"
import Spacer from "@/components/Utilities/Spacer"
import SolidBackgrondIconFeature from "@/sections/Features/SolidBackgrondIconFeature"
import PageHeader from "@/sections/PageHeader"
import React from "react"
const page = () => {
return (
@ -28,7 +28,7 @@ const page = () => {
<Footer />
</Background>
</div>
);
};
)
}
export default page;
export default page

287
Frontend/app/actions.js Normal file
View 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
}
}

View File

@ -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;
}
}

View File

@ -1,40 +1,35 @@
/* eslint-disable @next/next/no-before-interactive-script-outside-document */
import "./globals.css";
import "../app/globals.css";
import { Arimo, Indie_Flower } from "next/font/google";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import Header from "@/components/Header";
import { cookies } from "next/headers";
import { isAuthenticated } from "@/lib/auth";
import { GTagProvider, PHProvider, ThemeProvider } from "./providers";
import Script from "next/script";
import "./globals.css"
import "../app/globals.css"
import { Arimo, Indie_Flower } from "next/font/google"
import { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css"
import { cookies } from "next/headers"
import { isAuthenticated } from "@/lib/auth"
import { GTagProvider, PHProvider, ThemeProvider } from "./providers"
import Script from "next/script"
const raleway = Arimo({
variable: "--body-font",
subsets: ["latin"],
display: "swap",
});
display: "swap"
})
const arimo = Arimo({
variable: "--body-font",
subsets: ["latin"],
display: "swap",
});
display: "swap"
})
const indieFlower = Indie_Flower({
variable: "--accent-font",
weight: "400",
subsets: ["latin"],
display: "swap",
});
display: "swap"
})
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const isUserLoggedIn = await isAuthenticated(cookies());
export default async function RootLayout({ children }) {
const isUserLoggedIn = await isAuthenticated(cookies())
return (
<html
@ -46,7 +41,10 @@ export default async function RootLayout({
<GTagProvider />
<body className={`${arimo.className} bg-base-100 flex`}>
<ThemeProvider>
<Header isUserLoggedIn={isUserLoggedIn} authString={cookies().get("pb_auth")?.value || ""} />
<Header
isUserLoggedIn={isUserLoggedIn}
authString={cookies().get("pb_auth")?.value || ""}
/>
{children}
<ToastContainer
position="bottom-left"
@ -76,7 +74,7 @@ export default async function RootLayout({
id="promotekit-js"
async
defer
strategy="afterInteractive"
strategy="afterInteractive"
src="https://cdn.promotekit.com/promotekit.js"
data-promotekit="41c8e339-d2aa-414b-88b8-31b6d6346e2b"
/>
@ -84,7 +82,7 @@ export default async function RootLayout({
id="promotekit"
async
defer
strategy="afterInteractive"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
$(document).ready(function(){
@ -103,11 +101,11 @@ export default async function RootLayout({
}, 2000);
});
`,
`
}}
></Script>
</body>
</PHProvider>
</html>
);
)
}

View File

@ -1,7 +1,5 @@
import Footer from "@/components/Footer";
import Background from "@/components/Utilities/Background";
import PageWrapper from "@/components/Utilities/PageWrapper";
import Link from "next/link";
import Background from "@/components/Utilities/Background"
import Link from "next/link"
export default function NotFound() {
return (
@ -70,5 +68,5 @@ export default function NotFound() {
</Background>
</main>
</>
);
)
}

View File

@ -1,22 +1,17 @@
import "aos/dist/aos.css";
import React from "react";
import { SquaredBackgroundHero } from "@/sections/Hero";
import PageWrapper from "@/components/Utilities/PageWrapper";
import Footer from "@/components/Footer";
import PageHeader from "@/sections/PageHeader";
import ContainerImageIconBlocksFeature from "@/sections/Features/ContainerImageIconBlocksFeature";
import Background from "@/components/Utilities/Background";
import CardTestemonial from "@/sections/Testemonial/CardTestemonial";
import Payment from "@/sections/Payment";
import CardsFeature from "@/sections/Features/CardsFeature";
import FAQ from "@/sections/FAQ/RightAlignedBorderBottomFAQ";
import VerticalTabsFeature from "@/sections/Features/VerticalTabsFeature";
import "aos/dist/aos.css"
import React from "react"
import { SquaredBackgroundHero } from "@/sections/Hero"
import PageWrapper from "@/components/Utilities/PageWrapper"
import Footer from "@/components/Footer"
import PageHeader from "@/sections/PageHeader"
import ContainerImageIconBlocksFeature from "@/sections/Features/ContainerImageIconBlocksFeature"
import Background from "@/components/Utilities/Background"
import CardTestemonial from "@/sections/Testemonial/CardTestemonial"
import CardsFeature from "@/sections/Features/CardsFeature"
import FAQ from "@/sections/FAQ/RightAlignedBorderBottomFAQ"
import VerticalTabsFeature from "@/sections/Features/VerticalTabsFeature"
import { Metadata } from "next";
import Head from "next/head";
import Teste from "@/sections/Testing/Teste";
export const metadata: Metadata = {
export const metadata = {
title: "Pocketbase, Stripe, Next.js, Boilerplate | FastPocket",
description:
"FastPocket - Is a boilerplate codebase for everyone to build a product quickly.",
@ -26,7 +21,7 @@ export const metadata: Metadata = {
"next.js",
"boilerplate",
"template",
"codebase",
"codebase"
],
openGraph: {
url: "https://fastpocket.dev",
@ -39,9 +34,9 @@ export const metadata: Metadata = {
url: "https://fastpocket.dev/images/home/thumbnail.png",
width: 1200,
height: 630,
alt: "fastpocket.degv",
},
],
alt: "fastpocket.degv"
}
]
},
twitter: {
card: "summary_large_image",
@ -55,24 +50,19 @@ export const metadata: Metadata = {
url: "https://fastpocket.dev/images/home/thumbnail.png",
width: 1200,
height: 630,
alt: "fastpocket.degv",
},
],
alt: "fastpocket.degv"
}
]
},
alternates: {
canonical: "https://fastpocket.dev",
},
};
canonical: "https://fastpocket.dev"
}
}
export default function Home() {
return (
<>
<PageWrapper>
<SquaredBackgroundHero />
<Teste/>
<SquaredBackgroundHero />
<VerticalTabsFeature />
<div className="bg-primary/2">
@ -105,5 +95,5 @@ export default function Home() {
<Footer />
</PageWrapper>
</>
);
)
}

View File

@ -1,7 +1,6 @@
"use client";
import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
import GoogleAnalytics from "@/components/GoogleAnalytics";
import { ThemeProvider as NTThemeProvider } from 'next-themes'

51
Frontend/app/sitemap.js Normal file
View 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
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
function Page() {
return (
<>
Qualquercoisa
</>
);
}
export default Page;

View File

@ -1,20 +1,9 @@
import React from "react";
import React from "react"
import { PostMetadata } from "@/types";
import Image from "next/image";
import Link from "next/link";
import Image from "next/image"
import Link from "next/link"
function BlogCard({
title,
slug,
subtitle,
imageUrl,
}: {
title: string;
slug: string;
subtitle: string;
imageUrl: string;
}) {
function BlogCard({ title, slug, subtitle, imageUrl }) {
return (
<>
<Link
@ -37,7 +26,7 @@ function BlogCard({
</div>
</Link>
</>
);
)
}
export default BlogCard;
export default BlogCard

View File

@ -1,5 +1,5 @@
import * as React from "react";
const SvgComponent = (props: any) => (
import * as React from "react"
const SvgComponent = props => (
<a href="https://fastpocket.dev">
<svg
xmlns="http://www.w3.org/2000/svg"
@ -43,5 +43,5 @@ const SvgComponent = (props: any) => (
/>
</svg>
</a>
);
export default SvgComponent;
)
export default SvgComponent

View File

@ -1,7 +1,7 @@
import React from "react";
import { title } from "@/constants";
import Logo from "@/components/Logo";
import Link from "next/link";
import React from "react"
import { title } from "@/constants"
import Logo from "@/components/Logo"
import Link from "next/link"
import FastPocketBadge from "@/components/FastPocketBadge"
function Footer() {
@ -253,7 +253,7 @@ function Footer() {
</div>
</div>
</footer>
);
)
}
export default Footer;
export default Footer

View File

@ -1,18 +1,13 @@
"use client";
import React, { useEffect, useState } from "react";
import { Subscription, User } from "@/types";
import { getSubscriptions } from "@/app/actions";
import { useRouter } from "next/navigation";
interface GetStartedSectionButtonProps {
user: User;
}
export const GetStartedSectionButton = ({
user,
}: GetStartedSectionButtonProps) => {
export const GetStartedSectionButton = ({ user }) => {
const router = useRouter();
const [subscription, setSubscription] = useState<Subscription>();
const [subscription, setSubscription] = useState();
useEffect(() => {
(async () => {
if (!user) {
@ -37,4 +32,4 @@ export const GetStartedSectionButton = ({
);
};
export default GetStartedSectionButton;
export default GetStartedSectionButton;

View File

@ -1,6 +1,6 @@
import Script from "next/script";
import Script from "next/script"
const GoogleAnalytics = ({ ga_id }: { ga_id: string }) => (
const GoogleAnalytics = ({ ga_id }) => (
<>
<Script
async
@ -20,9 +20,9 @@ const GoogleAnalytics = ({ ga_id }: { ga_id: string }) => (
gtag('js', new Date());
gtag('config', '${ga_id}');
`,
`
}}
></Script>
</>
);
export default GoogleAnalytics;
)
export default GoogleAnalytics

View 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

View File

@ -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>
);
}

View File

@ -1,14 +1,8 @@
import React from "react";
import React from "react"
import Image, { ImageProps } from "next/image";
import Image from "next/image"
interface LogoProps {
label?: string;
className?: string;
imageProps?: ImageProps;
}
function Logo({ imageProps, ...props }: LogoProps) {
function Logo({ imageProps, ...props }) {
return (
<div
className={
@ -26,7 +20,7 @@ function Logo({ imageProps, ...props }: LogoProps) {
{...imageProps}
/>
</div>
);
)
}
export default Logo;
export default Logo

View File

@ -1,31 +1,31 @@
import React, { useRef } from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { emailValidationSchema } from "@/utils/form";
import { toast } from "react-toastify";
import pb from "@/lib/pocketbase";
import {Dismiss20Filled} from "@fluentui/react-icons"
import { redirect } from "next/navigation";
import React from "react"
import { useForm } from "react-hook-form"
import { yupResolver } from "@hookform/resolvers/yup"
import { emailValidationSchema } from "@/utils/form"
import { toast } from "react-toastify"
import pb from "@/lib/pocketbase"
import { Dismiss20Filled } from "@fluentui/react-icons"
import { redirect } from "next/navigation"
function ModalEmailChange({authString}:{authString:string}) {
function ModalEmailChange({ authString }) {
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
formState: { errors, isSubmitting }
} = useForm({
resolver: yupResolver(emailValidationSchema),
});
resolver: yupResolver(emailValidationSchema)
})
const onSubmit = async (data: any) => {
console.log(data);
pb.authStore.loadFromCookie(authString);
!pb.authStore.isValid && redirect("/");
const onSubmit = async data => {
console.log(data)
pb.authStore.loadFromCookie(authString)
!pb.authStore.isValid && redirect("/")
try {
if (await pb.collection("user").requestEmailChange(data.email)) {
reset();
document.getElementById("email-change-modal")?.click();
document.getElementById("sign-in-modal")?.click();
reset()
document.getElementById("email-change-modal")?.click()
document.getElementById("sign-in-modal")?.click()
}
} catch (error) {
if (error instanceof Error) {
@ -37,11 +37,11 @@ function ModalEmailChange({authString}:{authString:string}) {
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
});
theme: "colored"
})
}
}
};
}
return (
<>
<input
@ -49,7 +49,7 @@ function ModalEmailChange({authString}:{authString:string}) {
id="email-change-modal"
className="modal-toggle"
onClick={() => {
reset();
reset()
}}
/>
<label htmlFor="email-change-modal" className="modal cursor-pointer">
@ -88,7 +88,7 @@ function ModalEmailChange({authString}:{authString:string}) {
<button
disabled={isSubmitting}
type="submit"
className={isSubmitting ? "btn btn-gray": "btn btn-primary"}
className={isSubmitting ? "btn btn-gray" : "btn btn-primary"}
>
Change Email
{isSubmitting && <div className="loading"></div>}
@ -99,7 +99,7 @@ function ModalEmailChange({authString}:{authString:string}) {
</label>
</label>
</>
);
)
}
export default ModalEmailChange;
export default ModalEmailChange

View File

@ -1,30 +1,29 @@
import React, { useRef } from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { emailValidationSchema } from "@/utils/form";
import { toast } from "react-toastify";
import pb from "@/lib/pocketbase";
import {Dismiss20Filled} from '@fluentui/react-icons'
import { redirect } from "next/navigation";
import React from "react"
import { useForm } from "react-hook-form"
import { yupResolver } from "@hookform/resolvers/yup"
import { emailValidationSchema } from "@/utils/form"
import { toast } from "react-toastify"
import pb from "@/lib/pocketbase"
import { Dismiss20Filled } from "@fluentui/react-icons"
function ModalPasswordReset() {
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
formState: { errors, isSubmitting }
} = useForm({
resolver: yupResolver(emailValidationSchema),
});
resolver: yupResolver(emailValidationSchema)
})
const onSubmit = async (data: any) => {
console.log(data);
const onSubmit = async data => {
console.log(data)
try {
//login user
if (await pb.collection("user").requestPasswordReset(data.email)) {
reset();
document.getElementById("password-reset-modal")?.click();
document.getElementById("sign-in-modal")?.click();
reset()
document.getElementById("password-reset-modal")?.click()
document.getElementById("sign-in-modal")?.click()
}
} catch (error) {
if (error instanceof Error) {
@ -36,11 +35,11 @@ function ModalPasswordReset() {
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
});
theme: "colored"
})
}
}
};
}
return (
<>
<input
@ -48,7 +47,7 @@ function ModalPasswordReset() {
id="password-reset-modal"
className="modal-toggle"
onClick={() => {
reset();
reset()
}}
/>
<label htmlFor="password-reset-modal" className="modal cursor-pointer">
@ -98,7 +97,7 @@ function ModalPasswordReset() {
</label>
</label>
</>
);
)
}
export default ModalPasswordReset;
export default ModalPasswordReset

View File

@ -1,36 +1,34 @@
"use client";
"use client"
import React from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { signInValidationSchema } from "@/utils/form";
import { login } from "@/app/actions";
import { toast } from "react-toastify";
import { useRouter } from "next/navigation";
import {Dismiss20Filled} from '@fluentui/react-icons'
import React from "react"
import { useForm } from "react-hook-form"
import { yupResolver } from "@hookform/resolvers/yup"
import { signInValidationSchema } from "@/utils/form"
import { login } from "@/app/actions"
import { toast } from "react-toastify"
import { useRouter } from "next/navigation"
import { Dismiss20Filled } from "@fluentui/react-icons"
function ModalSignIn() {
const router = useRouter();
const router = useRouter()
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
formState: { errors, isSubmitting }
} = useForm({
resolver: yupResolver(signInValidationSchema),
});
resolver: yupResolver(signInValidationSchema)
})
const onSubmit = async (data: any) => {
const onSubmit = async data => {
try {
//login user
const auth = (await login({ email: data.email, password: data.password }))
if (
auth.success
) {
reset();
document.getElementById("sign-in-modal")?.click();
router.push("/account");
const auth = await login({ email: data.email, password: data.password })
if (auth.success) {
reset()
document.getElementById("sign-in-modal")?.click()
router.push("/account")
} else {
throw new Error(auth.response.message)
}
@ -44,11 +42,11 @@ function ModalSignIn() {
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
});
theme: "colored"
})
}
}
};
}
return (
<>
<input
@ -56,7 +54,7 @@ function ModalSignIn() {
id="sign-in-modal"
className="modal-toggle"
onClick={() => {
reset();
reset()
}}
/>
<label htmlFor="sign-in-modal" className="modal cursor-pointer">
@ -120,8 +118,8 @@ function ModalSignIn() {
<span
onClick={() => {
document.getElementById("sign-in-modal")?.click();
document.getElementById("sign-up-modal")?.click();
document.getElementById("sign-in-modal")?.click()
document.getElementById("sign-up-modal")?.click()
}}
className="text-primary hover:text-primary/60 cursor-pointer "
>
@ -143,7 +141,7 @@ function ModalSignIn() {
</label>
</label>
</>
);
)
}
export default ModalSignIn;
export default ModalSignIn

View 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}&nbsp;
</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}&nbsp;
</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

View File

@ -1,39 +1,33 @@
"use client";
"use client"
import React from "react"
import { useForm } from "react-hook-form"
import { yupResolver } from "@hookform/resolvers/yup"
import { signUpValidationSchema } from "@/utils/form"
import { companySizeList } from "@/constants"
import pb from "@/lib/pocketbase"
import { createCheckoutSession, login } from "@/app/actions"
import { toast } from "react-toastify"
import { useRouter } from "next/navigation"
import { Dismiss20Filled } from "@fluentui/react-icons"
import React, { Dispatch, SetStateAction } from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { signUpValidationSchema } from "@/utils/form";
import { companySizeList } from "@/constants";
import pb from "@/lib/pocketbase";
import { createCheckoutSession, login } from "@/app/actions";
import { toast } from "react-toastify";
import { Price, User } from "@/types";
import { useRouter } from "next/navigation";
import {Dismiss20Filled} from '@fluentui/react-icons'
function ModalSignUp({
setUser,
}: {
setUser: Dispatch<SetStateAction<User | undefined>>;
}) {
function ModalSignUp({ setUser }) {
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
formState: { errors, isSubmitting }
} = useForm({
resolver: yupResolver(signUpValidationSchema),
});
const router = useRouter();
const generateCheckoutPage = async (price: Price, type: string) => {
resolver: yupResolver(signUpValidationSchema)
})
const router = useRouter()
const generateCheckoutPage = async (price, type) => {
try {
const checkoutSessionResponse = await createCheckoutSession(
price.price_id,
type
);
console.log(checkoutSessionResponse);
router.push(checkoutSessionResponse.url);
)
console.log(checkoutSessionResponse)
router.push(checkoutSessionResponse.url)
} catch (error) {
if (error instanceof Error) {
toast.error(error.message, {
@ -44,53 +38,51 @@ function ModalSignUp({
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
});
theme: "colored"
})
}
}
};
const onSubmit = async (data: any) => {
}
const onSubmit = async data => {
data = {
emailVisibility: false,
lastSeen: new Date(),
role: "Admin",
displayName: `${data.firstName} ${data.lastName}`,
passwordConfirm: data.password,
...data,
};
...data
}
try {
//create organisation
const organisation = await pb.collection("organisation").create({
name: data.organisation,
organisationSize: data.organisationSize,
});
organisationSize: data.organisationSize
})
//create user
const user = await pb
.collection("user")
.create({ ...data, organisation: organisation.id });
.create({ ...data, organisation: organisation.id })
//login user
const auth = (await login({ email: data.email, password: data.password }))
if (
auth.success
) {
reset();
document.getElementById("sign-up-modal")?.click();
const price = localStorage.getItem("price");
const type = localStorage.getItem("type");
setUser(user as User);
console.log("price", price);
console.log("type", type);
const auth = await login({ email: data.email, password: data.password })
if (auth.success) {
reset()
document.getElementById("sign-up-modal")?.click()
const price = localStorage.getItem("price")
const type = localStorage.getItem("type")
setUser(user)
console.log("price", price)
console.log("type", type)
price
? generateCheckoutPage(JSON.parse(price), type ?? "")
: router.push("/account");
: router.push("/account")
} else {
throw new Error(auth.response.message)
}
} catch (error) {
if (error instanceof Error) {
toast.error(
Object.values((error as any).data.data)
.map((x: any) => x.message)
Object.values(error.data.data)
.map(x => x.message)
.join(),
{
position: "bottom-left",
@ -100,12 +92,12 @@ function ModalSignUp({
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
theme: "colored"
}
);
)
}
}
};
}
return (
<>
<input
@ -114,7 +106,7 @@ function ModalSignUp({
className="modal-toggle"
name=""
onClick={() => {
reset();
reset()
}}
/>
<label htmlFor="sign-up-modal" className="modal cursor-pointer">
@ -124,7 +116,7 @@ function ModalSignUp({
htmlFor="sign-up-modal"
className="cursor-pointer text-base-content"
onClick={() => {
reset();
reset()
}}
>
<Dismiss20Filled />
@ -225,7 +217,7 @@ function ModalSignUp({
>
{companySizeOption}
</option>
);
)
})}
</select>
<div className="text-start text-sm italic text-error-content">
@ -265,8 +257,8 @@ function ModalSignUp({
<span
onClick={() => {
document.getElementById("sign-up-modal")?.click();
document.getElementById("sign-in-modal")?.click();
document.getElementById("sign-up-modal")?.click()
document.getElementById("sign-in-modal")?.click()
}}
className="text-primary hover:text-primary/60 cursor-pointer "
>
@ -279,7 +271,7 @@ function ModalSignUp({
</label>
</label>
</>
);
)
}
export default ModalSignUp;
export default ModalSignUp

View File

@ -1,15 +1,15 @@
"use client";
"use client"
import Logo from "@/components/Logo";
import React, { useState } from "react";
import Link from "next/link";
import {Navigation24Filled} from "@fluentui/react-icons"
import Logo from "@/components/Logo"
import React, { useState } from "react"
import Link from "next/link"
import { Navigation24Filled } from "@fluentui/react-icons"
function Navigation({ isUserLoggedIn }: { isUserLoggedIn: boolean }) {
const [checked, setChecked] = useState<boolean>();
function Navigation({ isUserLoggedIn }) {
const [checked, setChecked] = useState()
const handleClick = () => {
checked ? setChecked(!checked) : setChecked(checked);
};
checked ? setChecked(!checked) : setChecked(checked)
}
return (
<div className="drawer max-w-fit">
<input
@ -23,10 +23,7 @@ function Navigation({ isUserLoggedIn }: { isUserLoggedIn: boolean }) {
{/* Navbar */}
<div className="flex-grow navbar ">
<div className="flex-none lg:hidden">
<label
htmlFor="my-drawer-3"
className="items-center"
>
<label htmlFor="my-drawer-3" className="items-center">
<Navigation24Filled />
</label>
</div>
@ -107,7 +104,7 @@ function Navigation({ isUserLoggedIn }: { isUserLoggedIn: boolean }) {
</div>
</div>
</div>
);
)
}
export default Navigation;
export default Navigation

View File

@ -1,34 +1,24 @@
"use client";
"use client"
import { createCheckoutSession, isAuthenticated } from "@/app/actions"
import { toast } from "react-toastify"
import { useRouter } from "next/navigation"
import { CheckmarkCircle24Filled } from "@fluentui/react-icons"
import { Price, Product, SourceModal } from "@/types";
import { createCheckoutSession, isAuthenticated } from "@/app/actions";
import { toast } from "react-toastify";
import { useRouter } from "next/navigation";
import {CheckmarkCircle24Filled } from "@fluentui/react-icons"
export default function PriceCard({
product,
isAnnual,
loading,
}: {
product?: Product;
isAnnual?: boolean;
loading?: boolean;
}) {
const router = useRouter();
const openSignUpModalOnPriceClick = (price: Price, type: string) => {
const signUpModal = document.getElementById("sign-up-modal");
if (!signUpModal) return;
signUpModal.click();
};
const generateCheckoutPage = async (price: Price, type: string) => {
export default function PriceCard({ product, isAnnual, loading }) {
const router = useRouter()
const openSignUpModalOnPriceClick = (price, type) => {
const signUpModal = document.getElementById("sign-up-modal")
if (!signUpModal) return
signUpModal.click()
}
const generateCheckoutPage = async (price, type) => {
try {
const checkoutSessionResponse = await createCheckoutSession(
price.price_id,
type
);
console.log(checkoutSessionResponse);
router.push(checkoutSessionResponse.url);
)
console.log(checkoutSessionResponse)
router.push(checkoutSessionResponse.url)
} catch (error) {
if (error instanceof Error) {
toast.error(error.message, {
@ -39,25 +29,25 @@ export default function PriceCard({
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
});
theme: "colored"
})
}
}
};
const submitForm = async (product: Product) => {
const userIsAuthenticated = await isAuthenticated();
console.log("userIsAuthenticated", userIsAuthenticated);
const price = isAnnual ? product.yearlyPrice : product.monthlyPrice;
console.log("price", price);
console.log("product.type", product.type);
localStorage.setItem("price", JSON.stringify(price));
localStorage.setItem("type", product.type);
}
const submitForm = async product => {
const userIsAuthenticated = await isAuthenticated()
console.log("userIsAuthenticated", userIsAuthenticated)
const price = isAnnual ? product.yearlyPrice : product.monthlyPrice
console.log("price", price)
console.log("product.type", product.type)
localStorage.setItem("price", JSON.stringify(price))
localStorage.setItem("type", product.type)
if (userIsAuthenticated) {
await generateCheckoutPage(price, product.type);
await generateCheckoutPage(price, product.type)
} else {
openSignUpModalOnPriceClick(price, product.type);
openSignUpModalOnPriceClick(price, product.type)
}
};
}
return (
<div
className={`relative w-64 sm:w-80 bg-base-200 rounded-lg p-6 shadow-lg border border-primary/40 ${
@ -124,5 +114,5 @@ export default function PriceCard({
</div>
</div>
</div>
);
)
}

View 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>
</>
)
}

View File

@ -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>
</>
);
}

View 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

View File

@ -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;

View 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

View File

@ -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;

View File

@ -0,0 +1,7 @@
import React from "react"
const Spacer = props => {
return <div {...props}></div>
}
export default Spacer

View File

@ -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;

View File

@ -1,20 +1,14 @@
import React, { useState } from "react";
import React from "react"
import { MediaPlayer, MediaProvider } from "@vidstack/react";
import { MediaPlayer, MediaProvider } from "@vidstack/react"
import {
DefaultAudioLayout,
defaultLayoutIcons,
DefaultVideoLayout,
} from "@vidstack/react/player/layouts/default";
import "@vidstack/react/player/styles/base.css";
export type Props = {
videotitle: string;
video: string;
width: number;
height: number;
};
DefaultVideoLayout
} from "@vidstack/react/player/layouts/default"
import "@vidstack/react/player/styles/base.css"
export default function YouTubeFrame({ video, width, height }: Props) {
export default function YouTubeFrame({ video, width, height }) {
return (
<MediaPlayer
className="w-full aspect-video bg-slate-900 bg-base-content font-sans overflow-hidden rounded-xl ring-media-focus data-[focus]:ring-4"
@ -29,5 +23,5 @@ export default function YouTubeFrame({ video, width, height }: Props) {
thumbnails="https://media-files.vidstack.io/thumbnails.vtt"
/>
</MediaPlayer>
);
)
}

View File

@ -4,6 +4,7 @@ export const companySizeList = [
"30-70 employees",
"70-100 employees",
"100+ employees"
]
export const title = "FastPocket"
]
export const title = "FastPocket"

7
Frontend/jsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}

20
Frontend/lib/auth.js Normal file
View 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
}

View File

@ -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