all changes
|
@ -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>
|
||||
)
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
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 (
|
||||
<main
|
||||
id="content"
|
||||
role="main"
|
||||
className="h-full flex-grow flex flex-col bg-base-100"
|
||||
>
|
||||
{/* Page sections */}
|
||||
<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.
|
||||
</p>
|
||||
<p className="text-lg text-base-content font-thin">
|
||||
After reflecting on the challenges that developers face building their
|
||||
applications, I've seen that there will never be one codebase
|
||||
that suits all developers. But I am sure for those who are
|
||||
opensourcing, self-hosting and want to spin up an application quickly
|
||||
they will be able to do it using{" "}
|
||||
<Link
|
||||
href={"https://fastpocket.dev"}
|
||||
className="font-bold text-secondary"
|
||||
>
|
||||
FastPocket
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-lg text-base-content font-thin">
|
||||
I am taking all of the knowledge that I have gained across 20+
|
||||
different React projects and combining it into 1 single codebase. It
|
||||
includes personal philosophies on styling, theming and building and
|
||||
will help indie-hackers, startups make complex technical decisions
|
||||
that have been battle tested in enterprise code
|
||||
</p>
|
||||
<p className="text-lg text-base-content font-thin">
|
||||
So I've committed to building{" "}
|
||||
<Link
|
||||
href={"https://fastpocket.dev"}
|
||||
className="font-bold text-secondary"
|
||||
>
|
||||
FastPocket
|
||||
</Link>{" "}
|
||||
to help you build your projects faster to get paid.
|
||||
</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.
|
||||
</p>
|
||||
<p className="text-lg text-base-content font-thin">
|
||||
I know that your next project will grow exponentially because of the
|
||||
work I have already done.
|
||||
</p>
|
||||
<p className="text-xl text-base-content font-bold">
|
||||
Ready to launch your next product?
|
||||
</p>
|
||||
|
||||
<Image
|
||||
src={"/images/sam.webp"}
|
||||
alt={"samuel wyndham"}
|
||||
width={1200}
|
||||
height={1200}
|
||||
className="w-48 h-48 rounded-full object-cover my-8"
|
||||
/>
|
||||
|
||||
<Link
|
||||
className="btn w-40 btn-primary rounded-full outline-none border-none capitalize"
|
||||
href={"/pricing"}
|
||||
>
|
||||
Let's go!
|
||||
</Link>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
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 }) {
|
||||
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)
|
||||
|
||||
return {
|
||||
title: `${post.title} | FastPocket`,
|
||||
authors: [
|
||||
{
|
||||
name: post.author || "Samuel Wyndham"
|
||||
}
|
||||
],
|
||||
description: post.description,
|
||||
keywords: post.keywords,
|
||||
openGraph: {
|
||||
title: `${post.title} | FastPocket`,
|
||||
description: post.description,
|
||||
type: "article",
|
||||
url: `https://${siteURL}/blogs/${post.slug}`,
|
||||
publishedTime: post.created,
|
||||
modifiedTime: post.updated,
|
||||
authors: [`https://${siteURL}/about`],
|
||||
images: [
|
||||
{
|
||||
url: `${post.imageUrl}`,
|
||||
width: 1024,
|
||||
height: 576,
|
||||
alt: post.title,
|
||||
type: "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "@meinbiz",
|
||||
creator: "@meinbiz",
|
||||
title: `${post.title} | FastPocket`,
|
||||
description: post.description,
|
||||
images: [
|
||||
{
|
||||
url: `${post.imageUrl}`,
|
||||
width: 1024,
|
||||
height: 576,
|
||||
alt: post.title
|
||||
}
|
||||
]
|
||||
},
|
||||
alternates: {
|
||||
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 PostPage = async props => {
|
||||
console.log("params", props.params)
|
||||
const post = await pb
|
||||
.collection("blog")
|
||||
.getFirstListItem(`slug="` + props.params.slug + `"`, {
|
||||
requestKey: "post"
|
||||
})
|
||||
return (
|
||||
<main
|
||||
id="content"
|
||||
role="main"
|
||||
className="h-full flex-grow flex flex-col bg-base-100 max-w-screen"
|
||||
>
|
||||
<Background>
|
||||
<div className="flex flex-col h-full w-full md:max-w-3xl md:mx-auto max-w-screen mb-8">
|
||||
<Spacer className="pt-32" />
|
||||
<BlogContent post={post} />
|
||||
</div>
|
||||
<Footer />
|
||||
</Background>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default PostPage
|
|
@ -0,0 +1,44 @@
|
|||
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()
|
||||
|
||||
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} />)
|
||||
return (
|
||||
<main
|
||||
id="content"
|
||||
role="main"
|
||||
className="h-full flex-grow flex flex-col bg-base-100"
|
||||
>
|
||||
<Background>
|
||||
{/* Page sections */}
|
||||
<PageHeader
|
||||
title="Blogs"
|
||||
subtitle={
|
||||
<>
|
||||
{" "}
|
||||
<h2 className="text-base-content text-2xl font-medium text-center max-w-5xl mx-auto px-6">
|
||||
Take a look as we show you how to make a great PocketBase +
|
||||
React App
|
||||
</h2>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="max-w-6xl mx-auto mb-auto pb-24 h-full w-full py-12 px-8">
|
||||
<div className="w-full flex items-start justify-center flex-row flex-wrap gap-x-8 gap-y-8 md:gap-8 text-base-content ">
|
||||
{postPreviews}
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</Background>
|
||||
</main>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
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 (
|
||||
<div className="flex flex-col h-full w-full bg-base-100 ">
|
||||
<Background>
|
||||
<PageHeader
|
||||
title="Contact Us"
|
||||
subtitle={
|
||||
<>
|
||||
{" "}
|
||||
<h2 className="text-base-content text-2xl font-medium content text-center max-w-6xl mx-auto px-6">
|
||||
We'd love to talk about how we can help you.
|
||||
</h2>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormLeftDescriptionRightContactUs />
|
||||
<Spacer className="mt-auto" />
|
||||
<Footer />
|
||||
</Background>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
|
@ -0,0 +1,11 @@
|
|||
import React from "react";
|
||||
|
||||
function layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default layout;
|
|
@ -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 []
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
"use client"
|
||||
|
||||
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 (
|
||||
<main
|
||||
id="content"
|
||||
role="main"
|
||||
className="h-full flex flex-col min-h-screen mx-auto w-screen overflow-hidden bg-base-100"
|
||||
>
|
||||
<Background>
|
||||
<PageHeader
|
||||
title="Your Fast Pocket Application"
|
||||
subtitle={
|
||||
<>
|
||||
{" "}
|
||||
<h2 className="text-base-content text-2xl font-medium content text-center max-w-4xl mx-auto px-6">
|
||||
Apply now to join us as we build apps fast, together! Be among
|
||||
the first to start using FastPocket and lock in your lifetime
|
||||
discount.
|
||||
</h2>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<h2 className="text-base-content text-4xl mt-12 font-extrabold content text-center max-w-6xl mx-auto px-6">
|
||||
What you will get!
|
||||
</h2>
|
||||
|
||||
<SolidBackgrondIconFeature />
|
||||
|
||||
<Payment type="one_time" />
|
||||
<Spacer className="mt-auto" />
|
||||
<Newsletter/>
|
||||
<Spacer className="mb-8" />
|
||||
<Footer />
|
||||
</Background>
|
||||
</main>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
"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"
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full bg-base-100 ">
|
||||
<Background>
|
||||
<PageHeader
|
||||
title="What's New"
|
||||
subtitle={
|
||||
<>
|
||||
{" "}
|
||||
<h2 className="text-base-content text-2xl font-medium content text-center max-w-6xl mx-auto px-6">
|
||||
Fastpocket gives you boilerplate to help you build.
|
||||
</h2>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<SolidBackgrondIconFeature />
|
||||
<Spacer className="mt-auto" />
|
||||
<Footer />
|
||||
</Background>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 13 KiB |
BIN
app/favicon.ico
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
|
@ -1,33 +1,9 @@
|
|||
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--heading-font);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/* 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";
|
||||
|
||||
const raleway = Arimo({
|
||||
variable: "--body-font",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const arimo = Arimo({
|
||||
variable: "--body-font",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const indieFlower = Indie_Flower({
|
||||
variable: "--accent-font",
|
||||
weight: "400",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}) {
|
||||
const isUserLoggedIn = await isAuthenticated(cookies());
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`h-full ${raleway.variable} ${arimo.variable} ${indieFlower.variable}`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<PHProvider>
|
||||
<GTagProvider />
|
||||
<body className={`${arimo.className} bg-base-100 flex`}>
|
||||
<ThemeProvider>
|
||||
<Header isUserLoggedIn={isUserLoggedIn} authString={cookies().get("pb_auth")?.value || ""} />
|
||||
{children}
|
||||
<ToastContainer
|
||||
position="bottom-left"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme="colored"
|
||||
/>
|
||||
</ThemeProvider>
|
||||
<Script
|
||||
id="promotekit-jq"
|
||||
src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"
|
||||
strategy="beforeInteractive"
|
||||
/>
|
||||
<Script
|
||||
id="promotekit-js"
|
||||
async
|
||||
defer
|
||||
strategy="afterInteractive"
|
||||
src="https://cdn.promotekit.com/promotekit.js"
|
||||
data-promotekit="41c8e339-d2aa-414b-88b8-31b6d6346e2b"
|
||||
/>
|
||||
<Script
|
||||
id="promotekit"
|
||||
async
|
||||
defer
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
$(document).ready(function(){
|
||||
setTimeout(function() {
|
||||
$('a[href^="https://buy.stripe.com/"]').each(function(){
|
||||
const oldBuyUrl = $(this).attr("href");
|
||||
const referralId = window.promotekit_referral;
|
||||
if (!oldBuyUrl.includes("client_reference_id")) {
|
||||
const newBuyUrl = oldBuyUrl + "?client_reference_id=" + referralId;
|
||||
$(this).attr("href", newBuyUrl);
|
||||
}
|
||||
});
|
||||
$("[pricing-table-id]").each(function(){
|
||||
$(this).attr("client-reference-id", window.promotekit_referral);
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
`,
|
||||
}}
|
||||
></Script>
|
||||
</body>
|
||||
</PHProvider>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import Background from "@/components/Utilities/Background"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<>
|
||||
<main
|
||||
id="content"
|
||||
role="main"
|
||||
className="h-screen flex-grow flex flex-col bg-base-100 max-w-screen"
|
||||
>
|
||||
<Background>
|
||||
<div className="self-center justify-self-center my-auto">
|
||||
<div className="max-w-[50rem] flex flex-col mx-auto size-full">
|
||||
<div className="text-center py-10 px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="block text-7xl font-bold text-base-content sm:text-9xl">
|
||||
404
|
||||
</h1>
|
||||
<h1 className="block text-2xl font-bold text-primary-content" />
|
||||
<p className="mt-3 text-base-content/80">
|
||||
Oops, something went wrong.
|
||||
</p>
|
||||
<p className="text-base-content/80">
|
||||
Sorry, we couldn't find your page.
|
||||
</p>
|
||||
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
||||
<a
|
||||
className="w-full sm:w-auto py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-primary text-primary-content hover:br-primary/80 disabled:opacity-50 disabled:pointer-events-none"
|
||||
href="https://github.com/mrwyndham/pocketbase-stripe"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
className="flex-shrink-0 size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
||||
height={16}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
||||
Get the source code
|
||||
</a>
|
||||
<Link
|
||||
className="w-full sm:w-auto py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent text-base-content hover:text-secondary/80 disabled:opacity-50 disabled:pointer-events-none"
|
||||
href="/"
|
||||
>
|
||||
<svg
|
||||
className="flex-shrink-0 size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</svg>
|
||||
Back to home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Background>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
113
app/page.js
|
@ -1,113 +0,0 @@
|
|||
import Image from "next/image";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
|
||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
||||
Get started by editing
|
||||
<code className="font-mono font-bold">app/page.js</code>
|
||||
</p>
|
||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
|
||||
<a
|
||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{" "}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className="dark:invert"
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-full sm:before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full sm:after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
|
||||
<Image
|
||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Docs{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Find in-depth information about Next.js features and API.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800 hover:dark:bg-opacity-30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Learn{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Learn about Next.js in an interactive course with quizzes!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Templates{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
|
||||
Explore starter templates for Next.js.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={`mb-3 text-2xl font-semibold`}>
|
||||
Deploy{" "}
|
||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
||||
->
|
||||
</span>
|
||||
</h2>
|
||||
<p className={`m-0 max-w-[30ch] text-sm opacity-50 text-balance`}>
|
||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
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 Payment from "@/sections/Payment"
|
||||
|
||||
export const metadata = {
|
||||
title: "Pocketbase, Stripe, Next.js, Boilerplate | FastPocket",
|
||||
description:
|
||||
"FastPocket - Is a boilerplate codebase for everyone to build a product quickly.",
|
||||
keywords: [
|
||||
"pocketbase",
|
||||
"stripe",
|
||||
"next.js",
|
||||
"boilerplate",
|
||||
"template",
|
||||
"codebase"
|
||||
],
|
||||
openGraph: {
|
||||
url: "https://fastpocket.dev",
|
||||
type: "website",
|
||||
title: "Pocketbase, Stripe, Next.js, Boilerplate | FastPocket",
|
||||
description:
|
||||
"FastPocket - Is a boilerplate codebase for everyone to build a product quickly.",
|
||||
images: [
|
||||
{
|
||||
url: "https://fastpocket.dev/images/home/thumbnail.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "fastpocket.degv"
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Pocketbase, Stripe, Next.js, Boilerplate | FastPocket",
|
||||
description:
|
||||
"fastpocket.dev - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.",
|
||||
creator: "@meinbiz",
|
||||
site: "@meinbiz",
|
||||
images: [
|
||||
{
|
||||
url: "https://fastpocket.dev/images/home/thumbnail.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "fastpocket.degv"
|
||||
}
|
||||
]
|
||||
},
|
||||
alternates: {
|
||||
canonical: "https://fastpocket.dev"
|
||||
}
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<PageWrapper>
|
||||
<SquaredBackgroundHero />
|
||||
<VerticalTabsFeature />
|
||||
<div className="bg-primary/2">
|
||||
<ContainerImageIconBlocksFeature />
|
||||
</div>
|
||||
<PageHeader
|
||||
title={"You Will Hate FastPocket If..."}
|
||||
className="!pt-16"
|
||||
subtitle={<></>}
|
||||
/>
|
||||
<CardsFeature />
|
||||
<Background>
|
||||
<CardTestemonial />
|
||||
</Background>
|
||||
<PageHeader
|
||||
title={"Want To Start Building Your Apps Faster?"}
|
||||
className="!pt-16"
|
||||
subtitle={
|
||||
<>
|
||||
{" "}
|
||||
<h2 className="text-base-content text-2xl font-medium content text-center max-w-6xl mx-auto px-6">
|
||||
Purchase now to get early access at a discounted price and start
|
||||
building with fly.io and docker compose templates immediately!
|
||||
</h2>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Payment/>
|
||||
<FAQ />
|
||||
<Footer />
|
||||
</PageWrapper>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
"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'
|
||||
|
||||
|
||||
if (typeof window !== 'undefined' && !!process.env.NEXT_PUBLIC_POSTHOG_KEY && !!process.env.NEXT_PUBLIC_POSTHOG_HOST) {
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST
|
||||
})
|
||||
}
|
||||
|
||||
export function PHProvider({ children }) {
|
||||
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
|
||||
}
|
||||
|
||||
export function GTagProvider() {
|
||||
return process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS ? (
|
||||
<GoogleAnalytics ga_id={process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS} />
|
||||
) : <></>
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }) {
|
||||
return <NTThemeProvider defaultTheme = 'light'>{children}</NTThemeProvider>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
function Page() {
|
||||
return (
|
||||
<>
|
||||
Qualquercoisa
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,32 @@
|
|||
import React from "react"
|
||||
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
function BlogCard({ title, slug, subtitle, imageUrl }) {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
href={`/blogs/${slug}`}
|
||||
className="w-52 md:w-64 md:max-w-[300px] h-fit flex flex-col items-center flex-auto bg-base-200 p-10 rounded-xl shadow-lg"
|
||||
>
|
||||
<div className="w-full aspect-[4/3] rounded-lg overflow-hidden relative">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={title}
|
||||
className="w-full h-full object-cover"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full h-2/5 ">
|
||||
<h1 className="pt-4 pb-2 xl:pt-8 text-base-content text-xl font-bold ">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-base-content ">{subtitle}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogCard
|
|
@ -0,0 +1,259 @@
|
|||
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() {
|
||||
return (
|
||||
<footer className="w-full pb-10 pt-14 px-4 sm:px-6 lg:px-8 mx-auto bg-base-200">
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-6 mb-10">
|
||||
<div className="col-span-full hidden lg:col-span-1 lg:flex lg:flex-col lg:items-center">
|
||||
<Link
|
||||
className="flex-none text-xl font-semibold "
|
||||
href="#"
|
||||
aria-label="Brand"
|
||||
>
|
||||
<Logo />
|
||||
</Link>
|
||||
<p className="mt-3 text-xs sm:text-sm text-base-content">
|
||||
© {new Date().getFullYear()} {title + " "}
|
||||
</p>
|
||||
<FastPocketBadge className="h-24 mt-8" />
|
||||
</div>
|
||||
{/* End Col */}
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold text-primary uppercase">
|
||||
Product
|
||||
</h2>
|
||||
<div className="mt-3 grid space-y-3 text-sm">
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="/pricing"
|
||||
aria-label="pricing"
|
||||
>
|
||||
Pricing
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="https://docs.fastpocket.dev"
|
||||
aria-label="documentation"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Col */}
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold text-primary uppercase">
|
||||
Company
|
||||
</h2>
|
||||
<div className="mt-3 grid space-y-3 text-sm">
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="/blogs"
|
||||
aria-label="blog"
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="#"
|
||||
aria-label="customers"
|
||||
>
|
||||
Customers
|
||||
</Link>
|
||||
<span className="inline text-secondary">
|
||||
— Under Construction
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Col */}
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold text-primary uppercase">
|
||||
Resources
|
||||
</h2>
|
||||
<div className="mt-3 grid space-y-3 text-sm">
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="https://discord.gg/YpR5QVAa3Z"
|
||||
aria-label="community"
|
||||
>
|
||||
Community
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="#"
|
||||
aria-label="support"
|
||||
>
|
||||
Help & Support
|
||||
</Link>
|
||||
<span className="inline text-secondary">
|
||||
— Under Construction
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="/whatsnew"
|
||||
aria-label="what's new"
|
||||
>
|
||||
What's New
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Col */}
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold text-primary uppercase">
|
||||
Developers
|
||||
</h2>
|
||||
<div className="mt-3 grid space-y-3 text-sm">
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="#"
|
||||
aria-label="roadmap"
|
||||
>
|
||||
Roadmap
|
||||
</Link>
|
||||
<span className="inline text-secondary">
|
||||
— Under Construction
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="https://github.com/mrwyndham/pocketbase-stripe"
|
||||
aria-label="github"
|
||||
>
|
||||
GitHub
|
||||
</Link>{" "}
|
||||
</p>
|
||||
</div>
|
||||
{/* <h2 className="mt-7 text-xs font-semibold text-primary uppercase">
|
||||
Industries
|
||||
</h2>
|
||||
<div className="mt-3 grid space-y-3 text-sm">
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="#"
|
||||
>
|
||||
Financial Services
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="#"
|
||||
>
|
||||
Education
|
||||
</Link>
|
||||
</p>
|
||||
</div> */}
|
||||
</div>
|
||||
{/* End Col */}
|
||||
</div>
|
||||
{/* End Grid */}
|
||||
<div className="pt-5 mt-5 border-t border-base-content/40 ">
|
||||
<div className="sm:flex sm:justify-between sm:items-center">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<div className="space-x-4 text-sm ms-4">
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="#"
|
||||
aria-label="terms"
|
||||
>
|
||||
Terms
|
||||
</Link>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="#"
|
||||
aria-label="privacy"
|
||||
>
|
||||
Privacy
|
||||
</Link>
|
||||
<Link
|
||||
className="inline-flex gap-x-2 text-base-content hover:text-base-content/80"
|
||||
href="#"
|
||||
aria-label="status"
|
||||
>
|
||||
Status
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="mt-3 sm:hidden">
|
||||
<Link
|
||||
className="flex-none text-xl font-semibold "
|
||||
href="#"
|
||||
aria-label="Brand"
|
||||
>
|
||||
<Logo />
|
||||
</Link>
|
||||
<p className="mt-1 text-xs sm:text-sm text-base-content">
|
||||
© {new Date().getFullYear()} {title + " "}
|
||||
</p>
|
||||
</div>
|
||||
{/* Social Brands */}
|
||||
<div className="space-x-4">
|
||||
<Link
|
||||
className="inline-flex justify-center items-center size-10 text-center text-base-content hover:bg-base-content/80 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white transition "
|
||||
href="https://discord.gg/YpR5QVAa3Z"
|
||||
aria-label="discord"
|
||||
>
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
viewBox="0 0 24 24"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
className="inline-flex justify-center items-center size-10 text-center text-base-content hover:bg-base-content/80 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white transition "
|
||||
href="https://github.com/mrwyndham/pocketbase-stripe"
|
||||
aria-label="github"
|
||||
>
|
||||
<svg
|
||||
className="flex-shrink-0 size-3.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={16}
|
||||
height={16}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
{/* End Social Brands */}
|
||||
</div>
|
||||
{/* End Col */}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
|
@ -0,0 +1,35 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getSubscriptions } from "@/app/actions";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export const GetStartedSectionButton = ({ user }) => {
|
||||
const router = useRouter();
|
||||
const [subscription, setSubscription] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
const subscriptions = await getSubscriptions();
|
||||
if (subscriptions.length > 0) {
|
||||
setSubscription(subscriptions[0]);
|
||||
}
|
||||
})();
|
||||
}, [user]);
|
||||
|
||||
return subscription ? (
|
||||
<></>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => router.push("/pricing")}
|
||||
className="px-10 py-2 text-base capitalize !rounded-3xl bg-base-content bg-gradient-to-r from-primary to-secondary hover:bg-gray-900 w-full sm:w-auto"
|
||||
>
|
||||
Upgrade
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default GetStartedSectionButton;
|
|
@ -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
|
|
@ -0,0 +1,141 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } 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 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";
|
||||
|
||||
export default function Header({ isUserLoggedIn, authString }) {
|
||||
const [user, setUser] = useState();
|
||||
const [statefulUser, setStatefulUser] = useState();
|
||||
const pathName = usePathname();
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const user = await getUser();
|
||||
setUser(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">
|
||||
<label className="swap swap-rotate" aria-label="theme-toggle">
|
||||
<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) => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import React from "react"
|
||||
|
||||
import Image from "next/image"
|
||||
|
||||
function Logo({ imageProps, ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
props.className ?? " flex flex-row items-center justify-center"
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{/* <Image className="absolute hover:rotate-[360deg] transition duration-1000 ease-in-out" src={logoArrow} alt="Logo"/> */}
|
||||
|
||||
<Image
|
||||
src={"/icons/combination-logo.svg"}
|
||||
alt="Follow us on Twitter"
|
||||
width={200}
|
||||
height={40}
|
||||
{...imageProps}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logo
|
|
@ -0,0 +1,105 @@
|
|||
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 }) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm({
|
||||
resolver: yupResolver(emailValidationSchema)
|
||||
})
|
||||
|
||||
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()
|
||||
}
|
||||
} 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="email-change-modal"
|
||||
className="modal-toggle"
|
||||
onClick={() => {
|
||||
reset()
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="email-change-modal" className="modal cursor-pointer">
|
||||
<label className="modal-box relative bg-base-100 max-w-full md:max-w-[450px] py-4 px-3 md:p-6">
|
||||
<div className="flex justify-end pb-2 select-none">
|
||||
<label
|
||||
htmlFor="email-change-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">
|
||||
Change Your Email
|
||||
</h3>
|
||||
<p className="text-sm md:text-base">Enter in your new email</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="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>
|
||||
</label>
|
||||
</label>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalEmailChange
|
|
@ -0,0 +1,103 @@
|
|||
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 }
|
||||
} = useForm({
|
||||
resolver: yupResolver(emailValidationSchema)
|
||||
})
|
||||
|
||||
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()
|
||||
}
|
||||
} 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="password-reset-modal"
|
||||
className="modal-toggle"
|
||||
onClick={() => {
|
||||
reset()
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="password-reset-modal" className="modal cursor-pointer">
|
||||
<label className="modal-box relative bg-base-100 max-w-full md:max-w-[450px] py-4 px-3 md:p-6">
|
||||
<div className="flex justify-end pb-2 select-none">
|
||||
<label
|
||||
htmlFor="password-reset-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">
|
||||
Reset Your Password
|
||||
</h3>
|
||||
<p className="text-sm md:text-base">Enter in your email</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="flex flex-row w-full justify-between">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Reset Password
|
||||
{isSubmitting && <div className="loading"></div>}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalPasswordReset
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,277 @@
|
|||
"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"
|
||||
|
||||
function ModalSignUp({ setUser }) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm({
|
||||
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)
|
||||
} 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const onSubmit = async data => {
|
||||
data = {
|
||||
emailVisibility: false,
|
||||
lastSeen: new Date(),
|
||||
role: "Admin",
|
||||
displayName: `${data.firstName} ${data.lastName}`,
|
||||
passwordConfirm: data.password,
|
||||
...data
|
||||
}
|
||||
try {
|
||||
//create organisation
|
||||
const organisation = await pb.collection("organisation").create({
|
||||
name: data.organisation,
|
||||
organisationSize: data.organisationSize
|
||||
})
|
||||
//create user
|
||||
const user = await pb
|
||||
.collection("user")
|
||||
.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)
|
||||
console.log("price", price)
|
||||
console.log("type", type)
|
||||
price
|
||||
? generateCheckoutPage(JSON.parse(price), type ?? "")
|
||||
: router.push("/account")
|
||||
} else {
|
||||
throw new Error(auth.response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(
|
||||
Object.values(error.data.data)
|
||||
.map(x => x.message)
|
||||
.join(),
|
||||
{
|
||||
position: "bottom-left",
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="sign-up-modal"
|
||||
className="modal-toggle"
|
||||
name=""
|
||||
onClick={() => {
|
||||
reset()
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="sign-up-modal" className="modal cursor-pointer">
|
||||
<label className="modal-box relative 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-up-modal"
|
||||
className="cursor-pointer text-base-content"
|
||||
onClick={() => {
|
||||
reset()
|
||||
}}
|
||||
>
|
||||
<Dismiss20Filled />
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col h-[30rem] lg:h-full overflow-y-scroll">
|
||||
<div className="w-[100%] bg-gradient-to-r from-primary to-secondary px-6 mt-3 pb-6 rounded-lg text-primary-content">
|
||||
<h3 className="pb-1 text-3xl font-bold md:text-3xl pt-6">
|
||||
Want to 10X Your Dev Game?
|
||||
</h3>
|
||||
<p className="text-sm md:text-base">
|
||||
Signup and get started using FastPocket
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="w-full pl-1">
|
||||
<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="Email…"
|
||||
aria-label="Email…"
|
||||
autoComplete="on"
|
||||
{...register("email")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.email?.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="relative mt-1 flex-grow">
|
||||
<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="First Name…"
|
||||
aria-label="First Name…"
|
||||
autoComplete="on"
|
||||
{...register("firstName")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.firstName?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-1 flex-grow">
|
||||
<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="Last Name…"
|
||||
aria-label="Last Name…"
|
||||
{...register("lastName")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.lastName?.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-1 flex-grow">
|
||||
<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="Phone Number…"
|
||||
aria-label="Phone Number…"
|
||||
{...register("phoneNumber")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.phoneNumber?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="relative mt-1 flex-grow">
|
||||
<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="Organisation…"
|
||||
aria-label="Organisation…"
|
||||
{...register("organisation")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.organisation?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-1 flex-grow">
|
||||
<select
|
||||
defaultValue={""}
|
||||
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 "
|
||||
{...register("organisationSize")}
|
||||
>
|
||||
<option className="bg-gray-850" value={""} disabled>
|
||||
Company Size…
|
||||
</option>
|
||||
{companySizeList.map((companySizeOption, i) => {
|
||||
return (
|
||||
<option
|
||||
className="bg-gray-850"
|
||||
value={companySizeOption}
|
||||
key={i}
|
||||
>
|
||||
{companySizeOption}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.organisationSize?.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="relative mt-1 flex-grow">
|
||||
<input
|
||||
id="SignUpPwd"
|
||||
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"
|
||||
{...register("password")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.password?.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-x-4">
|
||||
<button
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Sign Up
|
||||
{isSubmitting && <div className="loading"></div>}
|
||||
</button>
|
||||
<div className=" text-xs w-28 block">
|
||||
<span className="whitespace-normal">
|
||||
Already have an account?{" "}
|
||||
</span>
|
||||
|
||||
<span
|
||||
onClick={() => {
|
||||
document.getElementById("sign-up-modal")?.click()
|
||||
document.getElementById("sign-in-modal")?.click()
|
||||
}}
|
||||
className="text-primary hover:text-primary/60 cursor-pointer "
|
||||
>
|
||||
Sign in
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalSignUp
|
|
@ -0,0 +1,110 @@
|
|||
"use client"
|
||||
|
||||
import Logo from "@/components/Logo"
|
||||
import React, { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { Navigation24Filled } from "@fluentui/react-icons"
|
||||
|
||||
function Navigation({ isUserLoggedIn }) {
|
||||
const [checked, setChecked] = useState()
|
||||
const handleClick = () => {
|
||||
checked ? setChecked(!checked) : setChecked(checked)
|
||||
}
|
||||
return (
|
||||
<div className="drawer max-w-fit">
|
||||
<input
|
||||
id="my-drawer-3"
|
||||
type="checkbox"
|
||||
className="drawer-toggle "
|
||||
aria-label="menu-toggle"
|
||||
checked={checked}
|
||||
/>
|
||||
<div className="drawer-content flex flex-col max-w-fit sm:max-w-full sm:w-full">
|
||||
{/* Navbar */}
|
||||
<div className="flex-grow navbar ">
|
||||
<div className="flex-none lg:hidden">
|
||||
<label htmlFor="my-drawer-3" className="items-center">
|
||||
<Navigation24Filled />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex-none hidden lg:block">
|
||||
<ul className="menu menu-horizontal items-center text-base-content font-medium">
|
||||
{/* Site branding */}
|
||||
<li className="shrink-0 mr-4">
|
||||
{/* Logo */}
|
||||
<Link href="/">
|
||||
<Logo className="group relative cursor-pointer" />
|
||||
</Link>
|
||||
</li>
|
||||
{/* Navbar menu content here */}
|
||||
<li>
|
||||
<Link href="/pricing" onClick={handleClick}>
|
||||
Become A 10x Dev
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/about">Meet Sam</Link>
|
||||
</li>
|
||||
|
||||
{/* <li>
|
||||
<Link href="/contact" onClick={handleClick}>
|
||||
Contact
|
||||
</Link>
|
||||
</li> */}
|
||||
{isUserLoggedIn ? (
|
||||
<li>
|
||||
<Link href="/account" onClick={handleClick}>
|
||||
Account
|
||||
</Link>
|
||||
</li>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/* Page content here */}
|
||||
</div>
|
||||
<div className="drawer-side z-20">
|
||||
<label
|
||||
htmlFor="my-drawer-3"
|
||||
className="drawer-overlay"
|
||||
aria-label="drawer-navigation"
|
||||
></label>
|
||||
|
||||
<div className="menu w-80 h-full bg-base-100 text-base-content">
|
||||
{/* Sidebar content here */}
|
||||
<div className="shrink-0 m-3">
|
||||
{/* Logo */}
|
||||
<Link href="/">
|
||||
<Logo className="group relative cursor-pointer" />
|
||||
</Link>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/pricing">Get Your FastPocket</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/about">Meet Sam</Link>
|
||||
</li>
|
||||
</ul>
|
||||
{/* <li>
|
||||
<Link href="/contact">Contact</Link>
|
||||
</li> */}
|
||||
{isUserLoggedIn ? (
|
||||
<li>
|
||||
<Link href="/account" onClick={handleClick}>
|
||||
Account
|
||||
</Link>
|
||||
</li>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navigation
|
|
@ -0,0 +1,118 @@
|
|||
"use client"
|
||||
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 }) {
|
||||
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)
|
||||
} 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
} else {
|
||||
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 ${
|
||||
loading ? "animate-pulse h-[20rem]" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="mb-12 relative">
|
||||
{!loading && (
|
||||
<p className="absolute top-[-10px] left-[50%] -translate-x-1/2 font-architects-daughter text-sm text-primary text-center">
|
||||
$30 off for the first 5 customers{" "}
|
||||
<span className="text-secondary">(3 left)</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<h1 className="text-center text-3xl font-inter font-bold pt-12 text-base-content ">
|
||||
{product?.name}
|
||||
</h1>
|
||||
<h2 className="text-center pt-4 text-base-content ">
|
||||
{product?.description}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="pb-12">
|
||||
<ul className="flex flex-col gap-y-3 mx-12">
|
||||
{product?.metadata?.benefits?.map((x, i) => (
|
||||
<li key={i} className="flex items-center gap-x-4 flex-nowrap">
|
||||
<CheckmarkCircle24Filled />
|
||||
|
||||
<p className="w-40 text-base-content overflow-clip text-ellipsis">
|
||||
{x}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex flex-col mx-auto mt-auto">
|
||||
{!loading && (
|
||||
<div className="flex flex-row mx-auto gap-x-2 justify-center items-center mb-2">
|
||||
<h1 className="text-base-content text-4xl font-bold">
|
||||
$
|
||||
{isAnnual
|
||||
? (product?.yearlyPrice?.unit_amount ?? 0) / 100
|
||||
: (product?.monthlyPrice?.unit_amount ?? 0) / 100}
|
||||
</h1>
|
||||
{product?.type != "one_time" ? (
|
||||
<p className="w-16 leading-5 text-sm text-base-content ">
|
||||
per user per {isAnnual ? "year" : "month"}
|
||||
</p>
|
||||
) : (
|
||||
<p className="w-16 leading-5 text-sm text-base-content ">
|
||||
One Time
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{product && (
|
||||
<button
|
||||
onClick={() => submitForm(product)}
|
||||
className="btn btn-primary rounded-full bg-gradient-to-r from-primary to-secondary outline-none border-none capitalize"
|
||||
>
|
||||
Let's go!
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
import React from "react"
|
||||
|
||||
const Spacer = props => {
|
||||
return <div {...props}></div>
|
||||
}
|
||||
|
||||
export default Spacer
|
|
@ -0,0 +1,27 @@
|
|||
import React from "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 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"
|
||||
title="Sprite Fight"
|
||||
src="youtube/KCHnP62DWpg"
|
||||
controls
|
||||
>
|
||||
<MediaProvider />
|
||||
<DefaultAudioLayout icons={defaultLayoutIcons} />
|
||||
<DefaultVideoLayout
|
||||
icons={defaultLayoutIcons}
|
||||
thumbnails="https://media-files.vidstack.io/thumbnails.vtt"
|
||||
/>
|
||||
</MediaPlayer>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export const companySizeList = [
|
||||
"1-10 employees",
|
||||
"10-30 employees",
|
||||
"30-70 employees",
|
||||
"70-100 employees",
|
||||
"100+ employees"
|
||||
]
|
||||
|
||||
export const title = "FastPocket"
|
||||
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = (() => {
|
||||
const POCKETBASE_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL + '/';
|
||||
return new PocketBase(POCKETBASE_URL);
|
||||
})();
|
||||
|
||||
export default pb;
|
|
@ -0,0 +1,33 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
productionBrowserSourceMaps: true,
|
||||
|
||||
compiler: {
|
||||
// Enables the styled-components SWC transform
|
||||
styledComponents: true
|
||||
},
|
||||
|
||||
webpack(config) {
|
||||
config.resolve.fallback = {
|
||||
// if you miss it, all the other options in fallback, specified
|
||||
// by next.js will be dropped.
|
||||
...config.resolve.fallback,
|
||||
|
||||
fs: false, // the solution
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "**",
|
||||
port: "",
|
||||
pathname: "**",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
|
@ -1,4 +0,0 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
56
package.json
|
@ -1,23 +1,57 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"name": "fastpocket",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"start": "next build && next start",
|
||||
"lint": "next lint",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.1.0"
|
||||
"@fluentui/react-icons": "2.0.227",
|
||||
"@hookform/resolvers": "^3.2.0",
|
||||
"@netlify/functions": "^2.1.0",
|
||||
"@next/third-parties": "^14.1.0",
|
||||
"@tailwindcss/forms": "^0.5.4",
|
||||
"@vidstack/react": "^1.9.8",
|
||||
"aos": "^2.3.4",
|
||||
"autoprefixer": "10.4.15",
|
||||
"colorcolor": "^3.0.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"daisyui": "^3.5.1",
|
||||
"eslint": "8.47.0",
|
||||
"eslint-config-next": "13.4.17",
|
||||
"gray-matter": "^4.0.3",
|
||||
"next": "^14.1.0",
|
||||
"next-qrcode": "^2.5.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"node-fetch-commonjs": "^3.3.2",
|
||||
"patch-package": "^8.0.0",
|
||||
"pocketbase": "^0.18.0",
|
||||
"postcss": "^8.4.33",
|
||||
"posthog-js": "^1.105.0",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"preline": "^2.0.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
"sharp": "^0.33.3",
|
||||
"tailwindcss": "3.3.3",
|
||||
"theme-change": "^2.5.0",
|
||||
"typescript": "5.1.6",
|
||||
"yup": "^1.2.0",
|
||||
"yup-password": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.0.1",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0"
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/aos": "^3.0.4",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/node": "20.5.0",
|
||||
"@types/react": "18.2.20",
|
||||
"markdown-to-jsx": "^7.3.2"
|
||||
}
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 11 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg width="51" height="65" viewBox="0 0 51 65" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.9483 64.8545C8.70466 64.2966 6.83699 63.4962 5.26039 62.4411C3.87783 61.4951 3.24719 60.8281 2.07081 59.0574C1.17335 57.7234 0.955056 57.2261 0.554842 55.6738C0.215266 54.3397 0.191015 54.0851 0.0818658 50.3861C0.00909952 48.2153 -0.0151603 38.7799 0.00909509 28.9201C0.0576059 9.7583 0.0212228 10.7406 0.736757 8.84872C1.70697 6.30191 3.92635 3.68232 6.267 2.32402C6.57019 2.15423 6.89763 1.93593 6.98253 1.85104C7.06742 1.76614 7.28572 1.657 7.46763 1.62061C7.64955 1.5721 7.90423 1.47508 8.03764 1.39018C8.17104 1.31742 8.60764 1.14763 9.00785 1.02635C9.40807 0.917204 9.97807 0.747419 10.2813 0.650398C10.5845 0.565504 11.1545 0.432096 11.5547 0.35933C11.9549 0.274436 12.5006 0.165289 12.7674 0.104651C13.3981 -0.0287539 37.1684 -0.0408816 37.2533 0.104651C37.2897 0.153162 37.7384 0.238056 38.2356 0.286566C39.8971 0.432099 41.122 0.820181 43.2686 1.85104C43.7416 2.06933 44.4693 2.51806 45.0878 2.96679C45.7184 3.41551 47.4405 5.31956 48.0105 6.18063C48.811 7.41765 49.1142 8.09681 49.6599 9.94022C50.2057 11.7958 50.3755 13.1055 50.1814 13.8938C50.0601 14.3547 50.0116 18.8177 49.9874 33.5892C49.951 51.89 49.8903 54.934 49.575 55.3585C49.5265 55.4312 49.4174 55.7587 49.3325 56.0861C49.2354 56.4257 49.0899 56.8259 49.005 56.9957C48.9201 57.1655 48.8474 57.3838 48.8352 57.4808C48.8352 57.5778 48.7261 57.8325 48.5927 58.0266C48.4714 58.2327 48.2652 58.5844 48.1439 58.8149C47.8165 59.4698 46.6765 60.8402 45.5608 61.9074C44.2267 63.1808 42.4561 64.1026 40.3095 64.6362L38.8784 65L25.1498 64.9879C16.5149 64.9757 11.2515 64.9272 10.9483 64.8545ZM26.5688 46.008C27.7209 45.5229 28.0362 45.2561 32.475 40.9386C33.5058 39.932 35.992 37.5429 37.9931 35.6267C41.3282 32.4492 43.1473 30.5816 43.1473 30.3633C43.1473 30.2905 40.5641 27.5011 39.1452 26.0701C37.5201 24.4086 36.2709 23.4747 34.4882 22.5894L33.6271 22.1528L27.9877 22.0437C24.4222 21.9588 21.2205 21.9588 19.2436 22.0315L16.1389 22.1528L15.0717 22.7107C14.4896 23.026 13.6649 23.5596 13.2404 23.8992C12.3672 24.5905 6.76423 30.2905 6.76423 30.4846C6.76423 30.618 11.3728 35.2144 12.8523 36.5605C14.4896 38.0522 19.0132 42.2969 20.5656 43.8008C22.4332 45.6078 22.9183 45.9595 23.8522 46.1535C24.98 46.3961 25.732 46.3597 26.5688 46.008ZM7.38274 22.9047C10.4632 19.8364 11.0453 19.3392 12.4036 18.6479C13.3981 18.1385 13.5194 18.09 14.7443 17.8839C15.2657 17.799 15.8964 17.6413 16.1632 17.5443C16.5755 17.3866 17.9581 17.3624 24.7132 17.326C31.7473 17.3017 32.9358 17.326 34.0273 17.4958C36.8531 17.9445 38.5631 18.8298 40.7218 20.9279C41.4858 21.6798 42.5531 22.6864 43.0867 23.1715C43.6203 23.6566 44.6027 24.6026 45.2697 25.2818L46.4825 26.5188L47.0767 26.0216C47.4042 25.7547 47.865 25.3545 48.1076 25.1362L48.5442 24.7603L47.8893 24.0447C46.6765 22.7471 43.7901 19.8485 42.0073 18.1385C39.3999 15.6524 37.8233 14.6458 35.8465 14.2334C34.5609 13.9545 15.8115 14.0636 14.4047 14.3426C12.2338 14.7913 11.5668 15.1915 9.12913 17.5564C8.13466 18.5145 6.49742 20.0426 5.49082 20.94C4.49635 21.8496 3.19868 23.0139 2.61655 23.5354L1.57357 24.4935L2.55591 25.5001L3.55039 26.5188L4.01124 26.155C4.26592 25.9609 5.78188 24.4935 7.38274 22.9047ZM45.6942 14.5245C45.6942 13.5179 45.3788 11.8685 45.0029 10.9104C44.6391 10.0009 43.8507 8.72744 43.208 8.01191C41.7648 6.42318 39.1452 5.14977 36.7075 4.85871C35.325 4.68892 17.3517 4.57977 15.1081 4.7253C12.5734 4.88296 11.494 5.1619 9.57786 6.10787C8.52275 6.61723 7.41912 7.35701 6.98253 7.82999C4.92082 10.0372 4.26592 11.7472 4.53273 14.1485L4.64188 15.143L5.67273 14.1607C9.16551 10.8255 10.6693 10.0009 14.1136 9.50362C14.914 9.39447 15.6296 9.23681 15.7266 9.16404C15.9813 8.95787 33.3603 8.84872 33.8939 9.04276C34.1001 9.12766 34.8035 9.28532 35.4462 9.39447C36.9743 9.64915 38.4418 10.1343 39.9335 10.8619C40.6005 11.1894 41.2069 11.4926 41.2675 11.5411C41.3403 11.6017 41.7405 11.8928 42.165 12.2081C42.6016 12.5234 43.5233 13.3481 44.2267 14.0273C44.9301 14.7064 45.5486 15.2643 45.6093 15.2764C45.6578 15.2764 45.6942 14.9368 45.6942 14.5245Z" fill="#FD5469"/>
|
||||
<path d="M23.5368 37.2154C22.9304 36.9122 22.3483 36.2816 22.033 35.6024C21.7662 35.0203 21.7176 33.2739 21.9481 32.6069C22.1542 31.9884 22.7242 31.2365 23.1002 31.0909C23.9734 30.7392 24.4949 30.6422 25.2832 30.7028C26.7264 30.812 27.7451 31.4184 28.3394 32.5584C28.8609 33.5528 28.5698 35.6146 27.7572 36.5363C27.1994 37.179 26.3868 37.4701 25.1377 37.4701C24.2766 37.4701 23.937 37.4216 23.5368 37.2154Z" fill="#FD5469"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 880 B |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 47 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,9 @@
|
|||
# *
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Host
|
||||
Host: https://fastpocket.dev
|
||||
|
||||
# Sitemaps
|
||||
Sitemap: https://fastpocket.dev/sitemap.xml
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
Before Width: | Height: | Size: 629 B |
|
@ -0,0 +1,123 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQRCode } from "next-qrcode";
|
||||
import { createManagementSubscriptionSession, getSubscriptions } from "@/app/actions";
|
||||
|
||||
|
||||
function AccountContent({ user }) {
|
||||
const router = useRouter();
|
||||
const { Canvas } = useQRCode();
|
||||
|
||||
const [subscription, setSubscription] = useState();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!user) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
const subscriptions = await getSubscriptions();
|
||||
if (subscriptions.length > 0) {
|
||||
setSubscription(subscriptions[0]);
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [user]);
|
||||
|
||||
const manageSubscription = async () => {
|
||||
try {
|
||||
const managementSession = await createManagementSubscriptionSession();
|
||||
router.push(managementSession.url);
|
||||
} 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 loading ? (
|
||||
<div className="flex flex-col"></div>
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
<div className="max-w-6xl mx-auto mb-12 h-full w-full px-6">
|
||||
<div className="w-full bg-base-200 p-8 rounded text-base-content">
|
||||
<h3 className="font-accent text-xl text-secondary mb-2">
|
||||
Your Subscription
|
||||
</h3>
|
||||
{subscription && subscription.status !== "canceled" ? (
|
||||
<>
|
||||
<h2 className="text-3xl mb-3 font-heading font-bold">
|
||||
{subscription?.product?.name}
|
||||
</h2>
|
||||
<p className="text-lg mb-4">
|
||||
{subscription.product?.description}
|
||||
</p>
|
||||
<button
|
||||
onClick={manageSubscription}
|
||||
className="btn btn-sm text-base-content capitalize !rounded-md bg-secondary hover:bg-secondary/60 w-full sm:w-auto mt-8"
|
||||
>
|
||||
Manage Subscription
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-lg text-base-content mb-4">
|
||||
{"You haven’t upgraded your workflow yet"}
|
||||
</p>
|
||||
<div className="flex flex-row gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => router.push("/pricing")}
|
||||
className="btn btn-sm btn-neutral text-primary-content capitalize bg-gradient-to-r from-primary to-secondary border-none"
|
||||
>
|
||||
Upgrade
|
||||
</button>
|
||||
<button
|
||||
onClick={manageSubscription}
|
||||
className="btn btn-sm btn-secondary text-primary-content capitalize border-none"
|
||||
>
|
||||
Manage Purchases
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
document.getElementById("password-reset-modal")?.click()
|
||||
}
|
||||
className="btn btn-sm btn-secondary md:ml-auto text-primary-content capitalize border-none"
|
||||
>
|
||||
Reset Password
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
document.getElementById("email-change-modal")?.click()
|
||||
}
|
||||
className="btn btn-sm btn-secondary text-primary-content capitalize border-none"
|
||||
>
|
||||
Change Email
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountContent;
|
|
@ -0,0 +1,46 @@
|
|||
import React from "react"
|
||||
import Image from "next/image"
|
||||
|
||||
function BlogContent({ post }) {
|
||||
return (
|
||||
<div className="lg:mx-0 px-4 lg:px-8 py-12 lg:rounded-lg lg:shadow-lg lg:bg-base-200">
|
||||
<h1 className="font-heading text-4xl font-bold text-base-content">
|
||||
{post.title}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Not sure if name is required */}
|
||||
{post?.author && (
|
||||
<p className="text-base-content">{post.author ?? ""}</p>
|
||||
)}
|
||||
{post?.author && <span className="text-base-content">|</span>}
|
||||
<p className="text-base-content">
|
||||
Last modified {new Date(post.updated).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<article className="prose prose-img:rounded-xl max-w-none prose-p:text-base-content prose-a:text-primary prose-h2:text-base-content prose-li:text-base-content prose-strong:text-base-content prose-blockquote:pr-2 prose-blockquote:font-normal">
|
||||
<div className="h-full w-full">
|
||||
<Image
|
||||
src={post.imageUrl}
|
||||
alt="post-image"
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100%"
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="w-full max-w-[92vw] overflow-hidden"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
></div>
|
||||
</article>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogContent
|
|
@ -0,0 +1,250 @@
|
|||
"use client"
|
||||
|
||||
import pb from "@/lib/pocketbase"
|
||||
import { contactUsValidationSchema } from "@/utils/form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "react-toastify"
|
||||
|
||||
const FormLeftDescriptionRightContactUs = () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm({
|
||||
resolver: yupResolver(contactUsValidationSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async data => {
|
||||
try {
|
||||
await pb.collection("contact").create({ source: "contactus", ...data })
|
||||
localStorage.setItem("contactus", JSON.stringify(data))
|
||||
} 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 (
|
||||
<>
|
||||
{/* Contact Us */}
|
||||
<div className="max-w-[85rem] px-4 sm:px-6 lg:px-8 mx-auto pb-10">
|
||||
<div className="max-w-2xl lg:max-w-5xl mx-auto">
|
||||
<div className="mt-12 grid items-start lg:grid-cols-2 gap-6 lg:gap-16">
|
||||
{/* Card */}
|
||||
<div className="flex flex-col rounded-xl p-4 sm:p-6 lg:p-8 bg-base-200 min-h-[20rem] shadow-lg">
|
||||
<h2 className="mb-8 text-xl font-semibold text-base-content ">
|
||||
Fill in the form
|
||||
</h2>
|
||||
{typeof window !== "undefined" &&
|
||||
!localStorage.getItem("contactus") ? (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="grid gap-4 gap-y-0">
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-0">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="hs-firstname-contacts-1"
|
||||
className="sr-only"
|
||||
>
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="hs-firstname-contacts-1"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-white rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="First Name"
|
||||
{...register("firstName")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.firstName?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="hs-lastname-contacts-1"
|
||||
className="sr-only"
|
||||
>
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="hs-lastname-contacts-1"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-white rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Last Name"
|
||||
{...register("lastName")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.lastName?.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Grid */}
|
||||
<div>
|
||||
<label htmlFor="hs-email-contacts-1" className="sr-only">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="hs-email-contacts-1"
|
||||
autoComplete="email"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-white rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Email"
|
||||
{...register("email")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.email?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="hs-phone-number-1" className="sr-only">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="hs-phone-number-1"
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-white rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Phone Number"
|
||||
{...register("phoneNumber")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.phoneNumber?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="hs-about-contacts-1" className="sr-only">
|
||||
Details
|
||||
</label>
|
||||
<textarea
|
||||
id="hs-about-contacts-1"
|
||||
rows={4}
|
||||
className="py-3 px-4 block w-full bg-base-200 text-base-content border-white rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Details"
|
||||
defaultValue={""}
|
||||
{...register("note")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.note?.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Grid */}
|
||||
<div className="mt-4 grid">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Send inquiry
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 text-center">
|
||||
<p className="text-sm text-base-content ">
|
||||
We'll get back to you in 1-2 business days.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<p className="text-sm text-base-content">
|
||||
We have received your details and will be in touch!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* End Card */}
|
||||
<div className="divide-y divide-white ">
|
||||
{/* Icon Block */}
|
||||
<div className="flex gap-x-7 py-6">
|
||||
<svg
|
||||
className="flex-shrink-0 size-6 mt-1.5 text-base-content "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2v5Z" />
|
||||
<path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1" />
|
||||
</svg>
|
||||
<div className="grow">
|
||||
<h3 className="font-semibold text-base-content ">FAQ</h3>
|
||||
<p className="mt-1 text-sm text-base-content ">
|
||||
Search our FAQ for answers to anything you might ask.
|
||||
</p>
|
||||
<a
|
||||
className="mt-2 inline-flex items-center gap-x-2 text-sm font-medium text-base-content hover:text-base-content "
|
||||
href="/"
|
||||
>
|
||||
Visit FAQ
|
||||
<svg
|
||||
className="flex-shrink-0 size-2.5 transition ease-in-out group-hover:translate-x-1"
|
||||
width={16}
|
||||
height={16}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.975821 6.92249C0.43689 6.92249 -3.50468e-07 7.34222 -3.27835e-07 7.85999C-3.05203e-07 8.37775 0.43689 8.79749 0.975821 8.79749L12.7694 8.79748L7.60447 13.7596C7.22339 14.1257 7.22339 14.7193 7.60447 15.0854C7.98555 15.4515 8.60341 15.4515 8.98449 15.0854L15.6427 8.68862C16.1191 8.23098 16.1191 7.48899 15.6427 7.03134L8.98449 0.634573C8.60341 0.268455 7.98555 0.268456 7.60447 0.634573C7.22339 1.00069 7.22339 1.59428 7.60447 1.9604L12.7694 6.92248L0.975821 6.92249Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Icon Block */}
|
||||
|
||||
{/* Icon Block */}
|
||||
<div className=" flex gap-x-7 py-6">
|
||||
<svg
|
||||
className="flex-shrink-0 size-6 mt-1.5 text-base-content "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z" />
|
||||
<path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10" />
|
||||
</svg>
|
||||
<div className="grow">
|
||||
<h3 className="font-semibold text-base-content ">
|
||||
Contact us by email
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-base-content ">
|
||||
If you wish to write us an email instead please use
|
||||
</p>
|
||||
<a
|
||||
className="mt-2 inline-flex items-center gap-x-2 text-sm font-medium text-base-content hover:text-base-content "
|
||||
href="mailto:developer@biz365.com.au"
|
||||
>
|
||||
developer@biz365.com.au
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Icon Block */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Contact Us */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormLeftDescriptionRightContactUs
|
|
@ -0,0 +1,161 @@
|
|||
import React from "react"
|
||||
|
||||
function RightAlignedBorderBottomFAQ() {
|
||||
return (
|
||||
<>
|
||||
{/* FAQ */}
|
||||
<div className="max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
|
||||
{/* Grid */}
|
||||
<div className="grid md:grid-cols-5 gap-10">
|
||||
<div className="md:col-span-2">
|
||||
<div className="max-w-xs">
|
||||
<h2 className="text-2xl font-bold md:text-4xl md:leading-tight text-base-content">
|
||||
Frequently
|
||||
<br />
|
||||
asked questions
|
||||
</h2>
|
||||
<p className="mt-1 hidden md:block text-base-content/80 ">
|
||||
Answers to the most frequently asked questions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Col */}
|
||||
{/* Accordion */}
|
||||
<div className="md:col-span-3 space-y-6">
|
||||
{/* My tech stack is different can I still use it? */}
|
||||
<div className="collapse collapse-plus bg-base-300">
|
||||
<input
|
||||
type="radio"
|
||||
id="my-tech-stack-is"
|
||||
name="my-accordion-3"
|
||||
className="w-auto h-auto"
|
||||
defaultChecked
|
||||
/>
|
||||
<label
|
||||
htmlFor="my-tech-stack-is"
|
||||
className="collapse-title text-xl font-medium"
|
||||
>
|
||||
My tech stack is different can I still use it?
|
||||
</label>
|
||||
<div className="collapse-content">
|
||||
<p>
|
||||
Yes, as long as you're comfortable with React & NextJS.
|
||||
Libraries are independent. You can use SendGrid instead of
|
||||
Mailer, LemonSqueezy instead of Stripe, or Supabase instead of
|
||||
Pocketbase, for instance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* What do I get exactly? */}
|
||||
<div className="collapse collapse-plus bg-base-300">
|
||||
<input
|
||||
type="radio"
|
||||
id="what-do-i-get"
|
||||
name="my-accordion-3"
|
||||
className="w-auto h-auto"
|
||||
/>
|
||||
<label
|
||||
htmlFor="what-do-i-get"
|
||||
className="collapse-title text-xl font-medium"
|
||||
>
|
||||
What do I get exactly?
|
||||
</label>
|
||||
<div className="collapse-content">
|
||||
<p>
|
||||
1.The NextJS starter with all the boilerplate code you need to
|
||||
run an online business: a payment system, a database, login, a
|
||||
blog, UI components, and much more.
|
||||
</p>
|
||||
<p>
|
||||
2. Documentation that helps you set up your app from scratch
|
||||
</p>
|
||||
<p>
|
||||
3. Access to deployment templates to self host your app for
|
||||
free
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Is it a website template? */}
|
||||
<div className="collapse collapse-plus bg-base-300">
|
||||
<input
|
||||
type="radio"
|
||||
id="is-it-a-website"
|
||||
name="my-accordion-3"
|
||||
className="w-auto h-auto"
|
||||
/>
|
||||
<label
|
||||
htmlFor="is-it-a-website"
|
||||
className="collapse-title text-xl font-medium"
|
||||
>
|
||||
Is it a website template?
|
||||
</label>
|
||||
<div className="collapse-content">
|
||||
<p>
|
||||
It's more than just a template. You can copy/paste
|
||||
sections to build your site quickly, like a pricing section,
|
||||
an FAQ, and even an entire blog. You also get a bunch of UI
|
||||
components like buttons, modals, popovers, etc. The NextJS
|
||||
starter also comes with handy tools you need to run an online
|
||||
business—payment processing, emails, SEO, etc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* How is FastPocket different from other boilerplates */}
|
||||
<div className="collapse collapse-plus bg-base-300">
|
||||
<input
|
||||
type="radio"
|
||||
id="are-there-any-other"
|
||||
name="my-accordion-3"
|
||||
className="w-auto h-auto"
|
||||
/>
|
||||
<label
|
||||
htmlFor="are-there-any-other"
|
||||
className="collapse-title text-xl font-medium"
|
||||
>
|
||||
Are there any other costs associated
|
||||
</label>
|
||||
<div className="collapse-content">
|
||||
<p>
|
||||
Many hosting platforms, like Vercel, let you host a project
|
||||
for free (front-end + back-end) and we give you Pocketbase
|
||||
templates to host your backend for free on Fly.io or
|
||||
PocketHost — so you can launch for first app for $0/month.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* How is FastPocket different from other boilerplates */}
|
||||
<div className="collapse collapse-plus bg-base-300">
|
||||
<input
|
||||
type="radio"
|
||||
id="how-is-fastpocket-different"
|
||||
name="my-accordion-3"
|
||||
className="w-auto h-auto"
|
||||
/>
|
||||
<label
|
||||
htmlFor="how-is-fastpocket-different"
|
||||
className="collapse-title text-xl font-medium"
|
||||
>
|
||||
How is FastPocket different from other boilerplates
|
||||
</label>
|
||||
<div className="collapse-content">
|
||||
<p>
|
||||
FastPocket is all about giving you control over how you host
|
||||
your app. You won't be locked into vendors when you scale
|
||||
and you get to choose how much or how little you spend. We
|
||||
give you deployment templates and hosting options (that you
|
||||
can migrate from when you grow)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Accordion */}
|
||||
{/* End Col */}
|
||||
</div>
|
||||
{/* End Grid */}
|
||||
</div>
|
||||
\{/* End FAQ */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RightAlignedBorderBottomFAQ
|
|
@ -0,0 +1,218 @@
|
|||
import React from "react"
|
||||
|
||||
function CardsFeature() {
|
||||
return (
|
||||
<>
|
||||
{/* Icon Blocks */}
|
||||
<div className="max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 items-center gap-6 md:gap-10">
|
||||
{/* Card */}
|
||||
<div className="size-full bg-neutral shadow-lg rounded-lg p-5">
|
||||
<div className="flex items-center gap-x-4 mb-3">
|
||||
<div className="inline-flex justify-center items-center size-[62px] rounded-full border-4 border-primary/10 bg-primary/10">
|
||||
<svg
|
||||
className="flex-shrink-0 size-6 text-primary dark:text-primary/80"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="13.5" cy="6.5" r=".5" />
|
||||
<circle cx="17.5" cy="10.5" r=".5" />
|
||||
<circle cx="8.5" cy="7.5" r=".5" />
|
||||
<circle cx="6.5" cy="12.5" r=".5" />
|
||||
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<h2 className="block text-lg font-semibold capitalize text-primary-content">
|
||||
You Like Useless Features
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-primary-content/80">
|
||||
When every feature is a treasure, even the ones that don't
|
||||
seem to do much.
|
||||
</p>
|
||||
</div>
|
||||
{/* End Card */}
|
||||
{/* Card */}
|
||||
<div className="size-full bg-neutral shadow-lg rounded-lg p-5">
|
||||
<div className="flex items-center gap-x-4 mb-3">
|
||||
<div className="inline-flex justify-center items-center size-[62px] rounded-full border-4 border-primary/10 bg-primary/10">
|
||||
<svg
|
||||
className="flex-shrink-0 size-6 text-primary dark:text-primary/80"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M2 3h20" />
|
||||
<path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3" />
|
||||
<path d="m7 21 5-5 5 5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<h2 className="block text-lg font-semibold capitalize text-primary-content">
|
||||
You Like Debugging
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-primary-content/80">
|
||||
In the realm of code, where only the time rich heroes are the ones
|
||||
who can fix the bugs.
|
||||
</p>
|
||||
</div>
|
||||
{/* End Card */}
|
||||
{/* Card */}
|
||||
<div className="size-full bg-neutral shadow-lg rounded-lg p-5">
|
||||
<div className="flex items-center gap-x-4 mb-3">
|
||||
<div className="inline-flex justify-center items-center size-[62px] rounded-full border-4 border-primary/10 bg-primary/10">
|
||||
<svg
|
||||
className="flex-shrink-0 size-6 text-primary dark:text-primary/80"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7" />
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
|
||||
<path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4" />
|
||||
<path d="M2 7h20" />
|
||||
<path d="M22 7v3a2 2 0 0 1-2 2v0a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12v0a2 2 0 0 1-2-2V7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<h2 className="block text-lg font-semibold capitalize text-primary-content">
|
||||
You love solving solved problems
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-primary-content/80">
|
||||
Where every challenge is a puzzle, and the key is already in your
|
||||
hand.
|
||||
</p>
|
||||
</div>
|
||||
{/* End Card */}
|
||||
{/* Card */}
|
||||
<div className="size-full bg-neutral shadow-lg rounded-lg p-5">
|
||||
<div className="flex items-center gap-x-4 mb-3">
|
||||
<div className="inline-flex justify-center items-center size-[62px] rounded-full border-4 border-primary/10 bg-primary/10">
|
||||
<svg
|
||||
className="flex-shrink-0 size-6 text-primary dark:text-primary/80"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M5.5 8.5 9 12l-3.5 3.5L2 12l3.5-3.5Z" />
|
||||
<path d="m12 2 3.5 3.5L12 9 8.5 5.5 12 2Z" />
|
||||
<path d="M18.5 8.5 22 12l-3.5 3.5L15 12l3.5-3.5Z" />
|
||||
<path d="m12 15 3.5 3.5L12 22l-3.5-3.5L12 15Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<h2 className="block text-lg font-semibold capitalize text-primary-content">
|
||||
You hate shipping fast
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-primary-content/80">
|
||||
In the race of development, where the finish line is not the
|
||||
destination.
|
||||
</p>
|
||||
</div>
|
||||
{/* End Card */}
|
||||
{/* Card */}
|
||||
<div className="size-full bg-neutral shadow-lg rounded-lg p-5">
|
||||
<div className="flex items-center gap-x-4 mb-3">
|
||||
<div className="inline-flex justify-center items-center size-[62px] rounded-full border-4 border-primary/10 bg-primary/10">
|
||||
<svg
|
||||
className="flex-shrink-0 size-6 text-primary dark:text-primary/80"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M16.466 7.5C15.643 4.237 13.952 2 12 2 9.239 2 7 6.477 7 12s2.239 10 5 10c.342 0 .677-.069 1-.2" />
|
||||
<path d="m15.194 13.707 3.814 1.86-1.86 3.814" />
|
||||
<path d="M19 15.57c-1.804.885-4.274 1.43-7 1.43-5.523 0-10-2.239-10-5s4.477-5 10-5c4.838 0 8.873 1.718 9.8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<h2 className="block text-lg font-semibold capitalize text-primary-content">
|
||||
You love process more than dev
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-primary-content/80">
|
||||
Where the journey is more important than the destination.
|
||||
</p>
|
||||
</div>
|
||||
{/* End Card */}
|
||||
{/* Card */}
|
||||
<div className="size-full bg-neutral shadow-lg rounded-lg p-5">
|
||||
<div className="flex items-center gap-x-4 mb-3">
|
||||
<div className="inline-flex justify-center items-center size-[62px] rounded-full border-4 border-primary/10 bg-primary/10">
|
||||
<svg
|
||||
className="flex-shrink-0 size-6 text-primary dark:text-primary/80"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z" />
|
||||
<rect x={3} y={14} width={7} height={7} rx={1} />
|
||||
<circle cx="17.5" cy="17.5" r="3.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<h2 className="block text-lg font-semibold capitalize text-primary-content">
|
||||
You hate fast profits
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-primary-content/80">
|
||||
In the world of finance, where the race to the bottom is not the
|
||||
goal.
|
||||
</p>
|
||||
</div>
|
||||
{/* End Card */}
|
||||
</div>
|
||||
</div>
|
||||
{/* End Icon Blocks */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardsFeature
|
|
@ -0,0 +1,170 @@
|
|||
import React from "react"
|
||||
import Image from "next/image"
|
||||
|
||||
function ContainerImageIconBlocksFeature() {
|
||||
return (
|
||||
<>
|
||||
{/* Features */}
|
||||
<div className="max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
|
||||
{/* HIDDEN */}
|
||||
<div className="hidden aspect-w-16 aspect-h-7 rounded-xl">
|
||||
<Image
|
||||
className="w-full object-contain max-h-[40rem] rounded-xl"
|
||||
width="1000"
|
||||
height="1000"
|
||||
src="/images/fastpocket-diagram_713x640.webp"
|
||||
alt="Image Description"
|
||||
/>
|
||||
</div>
|
||||
{/* Grid */}
|
||||
<div className="mt-5 lg:mt-16 grid lg:grid-cols-3 gap-8 lg:gap-12">
|
||||
<div className="lg:col-span-1">
|
||||
<h2 className="font-bold text-2xl md:text-3xl text-base-content ">
|
||||
Want To Build Fast?
|
||||
</h2>
|
||||
<p className="mt-2 md:mt-4 text-base-content/80">
|
||||
There is so much to do in order to get an application developed.
|
||||
Just getting the essentials together like payments and emails is
|
||||
an enormous undertaking. That's why FastPocket exists. To
|
||||
give you the essentials so you can focus on what makes your app
|
||||
unique.
|
||||
</p>
|
||||
</div>
|
||||
{/* End Col */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="grid sm:grid-cols-2 gap-8 md:gap-12">
|
||||
{/* Icon Block */}
|
||||
<div className="flex gap-x-5">
|
||||
<svg
|
||||
className="flex-shrink-0 mt-1 size-6 text-primary "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect width={18} height={10} x={3} y={11} rx={2} />
|
||||
<circle cx={12} cy={5} r={2} />
|
||||
<path d="M12 7v4" />
|
||||
<line x1={8} x2={8} y1={16} y2={16} />
|
||||
<line x1={16} x2={16} y1={16} y2={16} />
|
||||
</svg>
|
||||
<div className="grow">
|
||||
<h3 className="text-lg font-semibold text-base-content">
|
||||
Payments
|
||||
</h3>
|
||||
<p className="mt-1 text-base-content/80 ">
|
||||
FastPocket includes Stripe for simple payment so that you
|
||||
can get profitable quickly. All your products in Stripe
|
||||
automatically syncronize with Pocketbase meaning no
|
||||
additional work for you
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Icon Block */}
|
||||
{/* Icon Block */}
|
||||
<div className="flex gap-x-5">
|
||||
<svg
|
||||
className="flex-shrink-0 mt-1 size-6 text-primary "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M7 10v12" />
|
||||
<path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z" />
|
||||
</svg>
|
||||
<div className="grow">
|
||||
<h3 className="text-lg font-semibold text-base-content">
|
||||
Style
|
||||
</h3>
|
||||
<p className="mt-1 text-base-content/80 ">
|
||||
FastPocket includes components to help you get styling your
|
||||
codebase really quickly. We use TailwindCSS, DaisyUI and
|
||||
Modified FastPocket components in order to fit the look and
|
||||
feel of your brand
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Icon Block */}
|
||||
{/* Icon Block */}
|
||||
<div className="flex gap-x-5">
|
||||
<svg
|
||||
className="flex-shrink-0 mt-1 size-6 text-primary "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
|
||||
</svg>
|
||||
<div className="grow">
|
||||
<h3 className="text-lg font-semibold text-base-content">
|
||||
Email
|
||||
</h3>
|
||||
<p className="mt-1 text-base-content/80 ">
|
||||
We know how annoying it is to setup email and that is why we
|
||||
provide email templates as well as frameworks for building
|
||||
your own emails for signup verifications and more
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Icon Block */}
|
||||
{/* Icon Block */}
|
||||
<div className="flex gap-x-5">
|
||||
<svg
|
||||
className="flex-shrink-0 mt-1 size-6 text-primary "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||
<circle cx={9} cy={7} r={4} />
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
<div className="grow">
|
||||
<h3 className="text-lg font-semibold text-base-content">
|
||||
Backend
|
||||
</h3>
|
||||
<p className="mt-1 text-base-content/80 ">
|
||||
We provide Pocketbase + stripe build templates to ship your
|
||||
product with no hassle. This includes login and payment for
|
||||
reoccuring and one time transactions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Icon Block */}
|
||||
</div>
|
||||
</div>
|
||||
{/* End Col */}
|
||||
</div>
|
||||
{/* End Grid */}
|
||||
</div>
|
||||
{/* End Features */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContainerImageIconBlocksFeature
|
|
@ -0,0 +1,155 @@
|
|||
import React from "react"
|
||||
import Image from "next/image"
|
||||
import { Library32Filled } from "@fluentui/react-icons"
|
||||
|
||||
function SolidBackgrondIconFeature() {
|
||||
return (
|
||||
<>
|
||||
{/* Icon Blocks */}
|
||||
<div className="max-w-5xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 items-center gap-2">
|
||||
{/* Icon Block */}
|
||||
<a
|
||||
className="group flex flex-col justify-center bg-base-200 shadow-lg rounded-xl p-4 md:p-7 border-primary/40 border "
|
||||
href="#"
|
||||
>
|
||||
<div className="flex justify-center items-center size-12 bg-primary rounded-xl py-2">
|
||||
<Image
|
||||
src={"/images/stripe-icon.webp"}
|
||||
alt={""}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<h3 className="group-hover:text-base-content text-lg font-semibold text-base-content ">
|
||||
Stripe Payments
|
||||
</h3>
|
||||
<p className="mt-1 text-base-content ">
|
||||
We have built stripe payments for 1 off and reoccuring payments
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/* End Icon Block */}
|
||||
{/* Icon Block */}
|
||||
<a
|
||||
className="group flex flex-col justify-center bg-base-200 shadow-lg rounded-xl p-4 md:p-7 border-primary/40 border "
|
||||
href="#"
|
||||
>
|
||||
<div className="flex justify-center items-center size-12 bg-primary rounded-xl py-2">
|
||||
<Image
|
||||
src={"/images/daisyui-icon.webp"}
|
||||
alt={""}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<h3 className="group-hover:text-base-content text-lg font-semibold text-base-content ">
|
||||
Theming
|
||||
</h3>
|
||||
<p className="mt-1 text-base-content ">
|
||||
All our components include intuative theming with a js utility
|
||||
class that uses your themes colors programmatically
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/* End Icon Block */}
|
||||
{/* Icon Block */}
|
||||
<a
|
||||
className="group flex flex-col justify-center bg-base-200 shadow-lg rounded-xl p-4 md:p-7 border-primary/40 border "
|
||||
href="#"
|
||||
>
|
||||
<div className="flex justify-center items-center size-12 bg-primary rounded-xl py-2">
|
||||
<Library32Filled style={{ height: 32, width: 32 }} />
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<h3 className="group-hover:text-base-content text-lg font-semibold text-base-content ">
|
||||
Documentation
|
||||
</h3>
|
||||
<p className="mt-1 text-base-content ">
|
||||
Every component and plugin is well documented
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/* End Icon Block */}
|
||||
{/* Icon Block */}
|
||||
<a
|
||||
className="group flex flex-col justify-center bg-base-200 shadow-lg rounded-xl p-4 md:p-7 border-primary/40 border "
|
||||
href="#"
|
||||
>
|
||||
<div className="flex justify-center items-center size-12 bg-primary rounded-xl py-2">
|
||||
<Image
|
||||
src={"/images/pocketbase-icon.webp"}
|
||||
alt={""}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<h3 className="group-hover:text-base-content text-lg font-semibold text-base-content ">
|
||||
Deployment Templates
|
||||
</h3>
|
||||
<p className="mt-1 text-base-content ">
|
||||
For fly.io, raleway, and more so you can get running with
|
||||
PocketBase as quickly as possible
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/* End Icon Block */}
|
||||
{/* Icon Block */}
|
||||
<a
|
||||
className="group flex flex-col justify-center bg-base-200 shadow-lg rounded-xl p-4 md:p-7 border-primary/40 border "
|
||||
href="#"
|
||||
>
|
||||
<div className="flex justify-center items-center size-12 bg-primary rounded-xl py-2">
|
||||
<Image
|
||||
src={"/images/daisyui-icon.png"}
|
||||
alt={""}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<h3 className="group-hover:text-base-content text-lg font-semibold text-base-content ">
|
||||
Component Library
|
||||
</h3>
|
||||
<p className="mt-1 text-base-content ">
|
||||
We provide fully customisable sections and components that you
|
||||
can drop in
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/* End Icon Block */}
|
||||
{/* Icon Block */}
|
||||
<a
|
||||
className="group flex flex-col justify-center bg-base-200 shadow-lg rounded-xl p-4 md:p-7 border-primary/40 border "
|
||||
href="#"
|
||||
>
|
||||
<div className="flex justify-center items-center size-12 bg-primary rounded-xl py-2">
|
||||
<Image
|
||||
src={"/images/nextjs-icon.webp"}
|
||||
alt={""}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<h3 className="group-hover:text-base-content text-lg font-semibold text-base-content ">
|
||||
Resources
|
||||
</h3>
|
||||
<p className="mt-1 text-base-content ">
|
||||
A growing list of resources to help you build your products
|
||||
faster
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/* End Icon Block */}
|
||||
</div>
|
||||
</div>
|
||||
{/* End Icon Blocks */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SolidBackgrondIconFeature
|
|
@ -0,0 +1,245 @@
|
|||
"use client"
|
||||
import Image from "next/image"
|
||||
import React, { useState } from "react"
|
||||
|
||||
function VerticalTabsFeature() {
|
||||
const [tab, setTab] = useState("1")
|
||||
return (
|
||||
<>
|
||||
{/* Features */}
|
||||
<div className="max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
|
||||
<div className="relative p-6 md:p-16">
|
||||
{/* Grid */}
|
||||
<div className="relative z-10 lg:grid lg:grid-cols-12 lg:gap-16 lg:items-center">
|
||||
<div className="mb-10 lg:mb-0 lg:col-span-6 lg:col-start-8 lg:order-2">
|
||||
<h2 className="text-2xl text-secondary-content font-bold sm:text-3xl ">
|
||||
Save 20+ hours of app development with FastPocket
|
||||
</h2>
|
||||
{/* Tab Navs */}
|
||||
<nav
|
||||
className="grid gap-4 mt-5 md:mt-10"
|
||||
aria-label="Tabs"
|
||||
role="tablist"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`hover:bg-base-content/5 ${tab === "1" &&
|
||||
"bg-base-300 shadow-lg hover:bg-base-300"} text-start p-4 md:p-5 rounded-xl`}
|
||||
id="tabs-with-card-item-1"
|
||||
aria-controls="tabs-with-card-item-1"
|
||||
role="tab"
|
||||
onClick={() => setTab("1")}
|
||||
>
|
||||
<span className="flex">
|
||||
<svg
|
||||
className="flex-shrink-0 mt-2 size-6 md:size-7 text-secondary-content "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z" />
|
||||
<path d="M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z" />
|
||||
<path d="M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z" />
|
||||
<path d="M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z" />
|
||||
<path d="M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z" />
|
||||
</svg>
|
||||
<span className="grow ms-6">
|
||||
<span className="block text-lg font-semibold text-secondary-content ">
|
||||
Simple Setup
|
||||
</span>
|
||||
<span className="block mt-1 text-secondary-content ">
|
||||
FastPocket already has a codebase with all of the
|
||||
necessary integrations to start an online business out
|
||||
of the box AND excellent documentation
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`hover:bg-base-content/5 ${tab === "2" &&
|
||||
"bg-base-300 shadow-lg hover:bg-base-300"} text-start p-4 md:p-5 rounded-xl `}
|
||||
id="tabs-with-card-item-2"
|
||||
aria-controls="tabs-with-card-item-2"
|
||||
role="tab"
|
||||
onClick={() => setTab("2")}
|
||||
>
|
||||
<span className="flex">
|
||||
<svg
|
||||
className="flex-shrink-0 mt-2 size-6 md:size-7 text-secondary-content "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m12 14 4-4" />
|
||||
<path d="M3.34 19a10 10 0 1 1 17.32 0" />
|
||||
</svg>
|
||||
<span className="grow ms-6">
|
||||
<span className="block text-lg font-semibold text-secondary-content ">
|
||||
Copy Paste Components
|
||||
</span>
|
||||
<span className="block mt-1 text-secondary-content ">
|
||||
We give you cookie cutter copy paste setup for your SaaS
|
||||
with unopinionated SaaS styling that can be modified to
|
||||
suite whatever app you are building
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`hover:bg-base-content/5 ${tab === "3" &&
|
||||
"bg-base-300 shadow-lg hover:bg-base-300"} text-start p-4 md:p-5 rounded-xl `}
|
||||
id="tabs-with-card-item-3"
|
||||
aria-controls="tabs-with-card-item-3"
|
||||
role="tab"
|
||||
onClick={() => setTab("3")}
|
||||
>
|
||||
<span className="flex">
|
||||
<svg
|
||||
className="flex-shrink-0 mt-2 size-6 md:size-7 text-secondary-content "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
|
||||
<path d="M5 3v4" />
|
||||
<path d="M19 17v4" />
|
||||
<path d="M3 5h4" />
|
||||
<path d="M17 19h4" />
|
||||
</svg>
|
||||
<span className="grow ms-6">
|
||||
<span className="block text-lg font-semibold text-secondary-content ">
|
||||
Simple Bring Your Own Backend
|
||||
</span>
|
||||
<span className="block mt-1 text-secondary-content ">
|
||||
We offer deployment templates that allow you to deploy
|
||||
to Digital Ocean, Fly.io, Pockethost or any other
|
||||
backend you want to use. You have the control over your
|
||||
hosting
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
{/* End Tab Navs */}
|
||||
</div>
|
||||
{/* End Col */}
|
||||
<div className="lg:col-span-6">
|
||||
<div className="relative">
|
||||
{/* Tab Content */}
|
||||
<div>
|
||||
{tab == "1" && (
|
||||
<div
|
||||
id="tabs-with-card-1"
|
||||
role="tabpanel"
|
||||
aria-labelledby="tabs-with-card-item-1"
|
||||
>
|
||||
<Image
|
||||
className="shadow-xl shadow-base-200 rounded-xl bg-base-300"
|
||||
src="/images/vertical-tabs-feature-1_520x643.webp"
|
||||
alt="Image Description"
|
||||
width="987"
|
||||
height="1220"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{tab == "2" && (
|
||||
<div
|
||||
id="tabs-with-card-2"
|
||||
role="tabpanel"
|
||||
aria-labelledby="tabs-with-card-item-2"
|
||||
>
|
||||
<Image
|
||||
className="shadow-xl shadow-base-200 rounded-xl bg-base-300"
|
||||
src="/images/vertical-tabs-feature-2.webp"
|
||||
alt="Image Description"
|
||||
width="987"
|
||||
height="1220"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{tab == "3" && (
|
||||
<div
|
||||
id="tabs-with-card-3"
|
||||
role="tabpanel"
|
||||
aria-labelledby="tabs-with-card-item-3"
|
||||
>
|
||||
<Image
|
||||
className="shadow-xl shadow-base-200 rounded-xl bg-base-300"
|
||||
src="/images/vertical-tabs-feature-3.webp"
|
||||
alt="Image Description"
|
||||
width="987"
|
||||
height="1220"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* End Tab Content */}
|
||||
{/* SVG Element */}
|
||||
<div className="hidden absolute top-0 end-0 translate-x-20 md:block lg:translate-x-20">
|
||||
<svg
|
||||
className="w-16 h-auto text-secondary"
|
||||
width={121}
|
||||
height={135}
|
||||
viewBox="0 0 121 135"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 16.4754C11.7688 27.4499 21.2452 57.3224 5 89.0164"
|
||||
stroke="currentColor"
|
||||
strokeWidth={10}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M33.6761 112.104C44.6984 98.1239 74.2618 57.6776 83.4821 5"
|
||||
stroke="currentColor"
|
||||
strokeWidth={10}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M50.5525 130C68.2064 127.495 110.731 117.541 116 78.0874"
|
||||
stroke="currentColor"
|
||||
strokeWidth={10}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/* End SVG Element */}
|
||||
</div>
|
||||
</div>
|
||||
{/* End Col */}
|
||||
</div>
|
||||
{/* End Grid */}
|
||||
{/* Background Color */}
|
||||
<div className="absolute inset-0 grid grid-cols-12 size-full">
|
||||
<div className="col-span-full lg:col-span-7 lg:col-start-6 bg-base-300 w-full h-5/6 rounded-xl sm:h-3/4 lg:h-full" />
|
||||
</div>
|
||||
{/* End Background Color */}
|
||||
</div>
|
||||
</div>
|
||||
{/* End Features */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default VerticalTabsFeature
|
|
@ -0,0 +1,36 @@
|
|||
import React from "react"
|
||||
import Video from "../Video"
|
||||
|
||||
const CenterAllignedWithVideoHero = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Hero */}
|
||||
<div className="h-screen w-screen flex flex-col">
|
||||
<div className="relative overflow-hidden my-auto">
|
||||
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8 py-10">
|
||||
<div className="max-w-2xl text-center mx-auto">
|
||||
<h1 className="block text-3xl font-bold bg-base-content sm:text-4xl md:text-5xl">
|
||||
Fast <span className="text-primary">Pocket</span>
|
||||
</h1>
|
||||
<p className="mt-3 text-lg bg-base-content">
|
||||
Build your startup here. Launch it anywhere.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-10 relative max-w-5xl mx-auto">
|
||||
<Video />
|
||||
<div className="absolute bottom-12 -start-20 -z-[1] size-48 bg-gradient-to-b from-primary to-base-100 p-px rounded-lg">
|
||||
<div className="bg-base-100 size-48 rounded-lg" />
|
||||
</div>
|
||||
<div className="absolute -top-12 -end-20 -z-[1] size-48 bg-gradient-to-t from-secondary to-accent p-px rounded-full">
|
||||
<div className="bg-base-100 size-48 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Hero */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CenterAllignedWithVideoHero
|
|
@ -0,0 +1,41 @@
|
|||
import React from "react"
|
||||
|
||||
const SimpleHero = () => {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center">
|
||||
<div className="text-center py-10 px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="block text-2xl font-bold bg-base-content sm:text-4xl">
|
||||
Cover Page
|
||||
</h1>
|
||||
<p className="mt-3 text-lg text-gray-300">
|
||||
Cover is a one-page template for building simple and beautiful home
|
||||
pages using Tailwind CSS.
|
||||
</p>
|
||||
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
||||
<a
|
||||
className="w-full sm:w-auto py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-base-100 text-gray-800 hover:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600"
|
||||
href="#"
|
||||
>
|
||||
<svg
|
||||
className="flex-shrink-0 size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</svg>
|
||||
Back to examples
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SimpleHero
|
|
@ -0,0 +1,78 @@
|
|||
import Background from "@/components/Utilities/Background"
|
||||
import { title } from "@/constants"
|
||||
import Image from "next/image"
|
||||
import React from "react"
|
||||
|
||||
const SquaredBackgroundHero = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Hero */}
|
||||
<Background>
|
||||
<div className="h-screen w-screen flex items-center">
|
||||
<div className="pt-12 md:max-w-[50rem] xl:max-w-[60rem] mx-auto px-6 sm:px-6 lg:px-8 md:pt-0">
|
||||
{/* Grid */}
|
||||
<div className="grid md:grid-cols-2 md:gap-0 xl:gap-20 md:items-center grid-flow-dense">
|
||||
<div className="order-1">
|
||||
<h1 className="block text-4xl font-extrabold sm:text-4xl lg:text-6xl leading-tight">
|
||||
Build your app fast with{" "}
|
||||
<span className="text-primary">{title}</span>
|
||||
</h1>
|
||||
<p className="mt-3 text-lg text-secondary-content">
|
||||
Join in a tight knit community of like minded makers as we
|
||||
build apps using Pocketbase and React
|
||||
</p>
|
||||
{/* Buttons */}
|
||||
<div className="mt-7 grid gap-3 w-full sm:inline-flex">
|
||||
<a
|
||||
className="py-3 px-4 inline-flex justify-center items-center gap-x-1 text-sm font-semibold rounded-lg border border-transparent bg-primary text-primary-content hover:bg-opacity-60 disabled:opacity-50 disabled:pointer-events-none dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600"
|
||||
href="https://buy.stripe.com/4gwaGQa9egxb5rO9AA"
|
||||
>
|
||||
<Image
|
||||
alt="fastpocket-icon"
|
||||
src="/images/icon.webp"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
Get FastPocket
|
||||
<svg
|
||||
className="flex-shrink-0 size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{/* End Buttons */}
|
||||
</div>
|
||||
{/* End Col */}
|
||||
<div className="-order-1 relative mr-4 sm:mr-0 sm:ms-4 sm:order-1">
|
||||
<Image
|
||||
priority
|
||||
// className="w-full rounded-md"
|
||||
width="352"
|
||||
height="316"
|
||||
src="/images/fastpocket-diagram_352x316.webp"
|
||||
alt="FastPocket Diagram"
|
||||
/>
|
||||
<div className="absolute inset-0 -z-[1] size-full rounded-md mt-4 -mb-4 me-4 -ms-4 lg:mt-6 lg:-mb-6 lg:me-6 lg:-ms-6" />
|
||||
</div>
|
||||
{/* End Col */}
|
||||
</div>
|
||||
{/* End Grid */}
|
||||
</div>
|
||||
</div>
|
||||
</Background>
|
||||
{/* End Hero */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SquaredBackgroundHero
|
|
@ -0,0 +1,209 @@
|
|||
"use client"
|
||||
import pb from "@/lib/pocketbase"
|
||||
import { waitinglistValidationSchema } from "@/utils/form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
|
||||
import Background from "@/components/Utilities/Background"
|
||||
import { toast } from "react-toastify"
|
||||
|
||||
const WaitingListWithImageHero = () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm({
|
||||
resolver: yupResolver(waitinglistValidationSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async data => {
|
||||
try {
|
||||
await pb.collection("contact").create({ source: "waitinglist", ...data })
|
||||
localStorage.setItem("waitinglist", JSON.stringify(data))
|
||||
} 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 (
|
||||
<Background>
|
||||
<div className="h-screen w-full bg-clip flex items-center justify-center pt-20">
|
||||
<div className="text-center py-8 px-4 m-4 border border-primary/40 sm:px-6 lg:px-8 bg-base-200 rounded-xl shadow-lg">
|
||||
{typeof window !== "undefined" &&
|
||||
!localStorage.getItem("waitinglist") ? (
|
||||
<>
|
||||
<h2 className="text-4xl text-base-content font-heading font-black flex-wrap max-w-sm leading-[3.2rem] lg:leading-normal ">
|
||||
<span className="bg-primary p-1 text-primary-content">
|
||||
Finish
|
||||
</span>{" "}
|
||||
Your React + PocketBase project, With A{" "}
|
||||
<span className="bg-primary p-1 text-primary-content">
|
||||
Head Start
|
||||
</span>
|
||||
</h2>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="mt-8 space-y-0">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="hs-cover-with-gradient-form-name-1"
|
||||
className="sr-only"
|
||||
>
|
||||
First name
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register("firstName")}
|
||||
type="text"
|
||||
id="hs-cover-with-gradient-form-name-1"
|
||||
className=" py-3 ps-11 pe-4 block w-full bg-base-100/[.03] border-primary/40 placeholder:text-base-content rounded-lg text-sm focus:border-secondary focus:ring-secondary sm:p-4 sm:ps-11"
|
||||
placeholder="First name"
|
||||
/>
|
||||
<div className="absolute inset-y-0 start-0 flex items-center pointer-events-none z-20 ps-4">
|
||||
<svg
|
||||
className="flex-shrink-0 size-4 text-base-content "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
||||
<circle cx={12} cy={7} r={4} />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-start text-sm italic text-error">
|
||||
{errors.firstName?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="hs-cover-with-gradient-form-name-1"
|
||||
className="sr-only"
|
||||
>
|
||||
Last name
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register("lastName")}
|
||||
type="text"
|
||||
id="hs-cover-with-gradient-form-name-1"
|
||||
className=" py-3 ps-11 pe-4 block w-full bg-base-100/[.03] border-primary/40 dark:placeholder:bg-base-contentntent placeholder:text-base-content rounded-lg text-sm focus:border-secondary focus:ring-secondary sm:p-4 sm:ps-11"
|
||||
placeholder="Last name"
|
||||
/>
|
||||
<div className="absolute inset-y-0 start-0 flex items-center pointer-events-none z-20 ps-4">
|
||||
<svg
|
||||
className="flex-shrink-0 size-4 text-base-content"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
||||
<circle cx={12} cy={7} r={4} />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-start text-sm italic text-error">
|
||||
{errors.lastName?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="hs-cover-with-gradient-form-email-1"
|
||||
className="sr-only"
|
||||
>
|
||||
Email address
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register("email")}
|
||||
type="email"
|
||||
id="hs-cover-with-gradient-form-email-1"
|
||||
className=" py-3 ps-11 pe-4 block w-full bg-base-100/[.03] border-primary/40 text-base-content placeholder:text-base-content rounded-lg text-sm focus:border-secondary focus:ring-secondary sm:p-4 sm:ps-11"
|
||||
placeholder="Email address"
|
||||
/>
|
||||
<div className="absolute inset-y-0 start-0 flex items-center pointer-events-none z-20 ps-4">
|
||||
<svg
|
||||
className="flex-shrink-0 size-4 text-base-content "
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect width={20} height={16} x={2} y={4} rx={2} />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-start text-sm italic text-error">
|
||||
{errors.email?.message}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid">
|
||||
<button
|
||||
className="btn btn-primary text-primary-content"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Join the waitlist
|
||||
{isSubmitting ? (
|
||||
<div className="loading"></div>
|
||||
) : (
|
||||
<svg
|
||||
className="flex-shrink-0 size-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<h2 className="sm:text-3xl font-body text-base-content">
|
||||
You are on our waiting list. We will be in touch!
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Background>
|
||||
)
|
||||
}
|
||||
|
||||
export default WaitingListWithImageHero
|
|
@ -0,0 +1,8 @@
|
|||
import SquaredBackgroundHero from "./SquaredBackgroundHero"
|
||||
import CenterAllignedWithVideoHero from "./CenterAllignedWithVideoHero"
|
||||
import WaitingListWithImageHero from "./WaitingListWithImageHero"
|
||||
export {
|
||||
SquaredBackgroundHero,
|
||||
CenterAllignedWithVideoHero,
|
||||
WaitingListWithImageHero
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
"use client"
|
||||
|
||||
import pb from "@/lib/pocketbase"
|
||||
import { emailValidationSchema } from "@/utils/form"
|
||||
import { yupResolver } from "@hookform/resolvers/yup"
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "react-toastify"
|
||||
|
||||
function Newsletter() {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm({
|
||||
resolver: yupResolver(emailValidationSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async data => {
|
||||
try {
|
||||
await pb.collection("contact").create({ source: "newsletter", ...data })
|
||||
localStorage.setItem("newsletter", JSON.stringify(data))
|
||||
} 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 (
|
||||
<section>
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
{/* CTA box */}
|
||||
<div
|
||||
className="relative bg-gradient-to-r from-primary to-secondary py-10 px-8 md:py-16 md:px-12"
|
||||
data-aos="fade-up"
|
||||
>
|
||||
{typeof window !== "undefined" &&
|
||||
!localStorage.getItem("newsletter") ? (
|
||||
<div className="relative flex flex-col lg:flex-row justify-between items-center">
|
||||
<div className="mb-6 lg:mr-16 lg:mb-0 text-center lg:text-left lg:w-1/2 text-primary-content">
|
||||
<h3 className=" mb-2 text-3xl font-black">
|
||||
Stay Ahead of the Curve
|
||||
</h3>
|
||||
<p className=" text-lg">
|
||||
Join our newsletter to get top news before anyone else.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-1/2">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col sm:flex-row justify-center max-w-xs mx-auto sm:max-w-md lg:max-w-none gap-x-2"
|
||||
>
|
||||
<div className="w-full">
|
||||
<input
|
||||
id="NewsletterEmail"
|
||||
type="text"
|
||||
className="py-3 px-4 block w-full text-base-content border-white rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none "
|
||||
placeholder="Email…"
|
||||
aria-label="Email…"
|
||||
{...register("email")}
|
||||
/>
|
||||
<div className="text-start text-sm italic text-error-content">
|
||||
{errors.email?.message}
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn text-primary-content btn-neutral">
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex flex-col lg:flex-row justify-between items-center text-primary-content">
|
||||
<h3 className=" mb-2 text-3xl font-black">
|
||||
Thanks for subscribing. You won't regret it!
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Newsletter
|
|
@ -0,0 +1,14 @@
|
|||
import React from "react"
|
||||
|
||||
function PageHeader({ title, subtitle, className }) {
|
||||
return (
|
||||
<div className={` pt-48 max-w-screen flex flex-col ${className}`}>
|
||||
<h1 className="text-4xl md:text-5xl text-center font-bold text-base-content mb-6 mx-auto px-4">
|
||||
{title}
|
||||
</h1>
|
||||
{subtitle}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageHeader
|
|
@ -0,0 +1,58 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PriceCard from "@/components/PriceCard";
|
||||
import PriceToggle from "@/components/PriceToggle";
|
||||
import { apiPrices } from "../app/(public)/pricing/action";
|
||||
|
||||
const Payment = ({
|
||||
type = "one_time",
|
||||
}) => {
|
||||
const [isAnnual, setIsAnnual] = useState(false);
|
||||
const [products, setProducts] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const handleToggle = () => {
|
||||
setIsAnnual((prev) => !prev);
|
||||
};
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
(async () => {
|
||||
const resposeProducts = await apiPrices();
|
||||
setProducts(resposeProducts);
|
||||
setIsLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{!(type == "one_time") && (
|
||||
<div className="flex items-center justify-center mt-10">
|
||||
<PriceToggle isAnnual={isAnnual} onChange={handleToggle} />
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w-6xl mx-auto mb-24 h-full">
|
||||
<div
|
||||
className={`w-screen lg:w-full pb-4 flex gap-x-4 lg:justify-center gap-y-8 px-6 pt-12 overflow-x-scroll`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<PriceCard loading={isLoading} product={undefined} isAnnual={undefined} />
|
||||
<PriceCard loading={isLoading} product={undefined} isAnnual={undefined} />
|
||||
<PriceCard loading={isLoading} product={undefined} isAnnual={undefined} />
|
||||
</>
|
||||
) : (
|
||||
(products || [])
|
||||
.filter((x) =>
|
||||
type == "one_time" ? x.type == "one_time" : x.type != "one_time"
|
||||
)
|
||||
.map((x, i) => (
|
||||
<PriceCard key={i} product={x} isAnnual={isAnnual} loading={undefined} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Payment;
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import React from "react"
|
||||
import Image from "next/image"
|
||||
|
||||
function CardTestemonial() {
|
||||
return (
|
||||
<>
|
||||
{/* Testimonials */}
|
||||
<div className="max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-6">
|
||||
{/* Card */}
|
||||
<div className="flex flex-col bg-base-100 /40 shadow-sm rounded-xl ">
|
||||
<div className="flex-auto p-4 md:p-6">
|
||||
<Image
|
||||
width="500"
|
||||
height="500"
|
||||
className="h-9 w-9 rounded-full"
|
||||
src={
|
||||
"https://pbs.twimg.com/profile_images/1432517677864607745/spXCJfPY_400x400.jpg"
|
||||
}
|
||||
alt={"Timmy"}
|
||||
/>
|
||||
<p className="mt-3 sm:mt-6 text-base text-base-content md:text-xl ">
|
||||
<em>I think its a good idea</em>
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-b-xl md:px-6">
|
||||
<h3 className="text-sm font-semibold text-base-content sm:text-base ">
|
||||
Mustafa Hanif
|
||||
</h3>
|
||||
<p className="text-sm text-base-content/80">Indie Hacker</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Card */}
|
||||
{/* Card */}
|
||||
<div className="flex flex-col bg-base-100 border border-primary/40 shadow-sm rounded-xl ">
|
||||
<div className="flex-auto p-4 md:p-6">
|
||||
<Image
|
||||
width="500"
|
||||
height="500"
|
||||
className="h-9 w-9 rounded-full"
|
||||
src={
|
||||
"https://pbs.twimg.com/profile_images/1647540692837597184/SyEB8Ehg_400x400.jpg"
|
||||
}
|
||||
alt={"Timmy"}
|
||||
/>
|
||||
<p className="mt-3 sm:mt-6 text-base text-base-content md:text-xl ">
|
||||
<em>I know it's worth it and very good</em>
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-b-xl md:px-6">
|
||||
<h3 className="text-sm font-semibold text-base-content sm:text-base ">
|
||||
Timmy D Turner
|
||||
</h3>
|
||||
<p className="text-sm text-base-content/80">Indie Hacker</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Card */}
|
||||
</div>
|
||||
{/* End Grid */}
|
||||
</div>
|
||||
{/* End Testimonials */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardTestemonial
|
|
@ -0,0 +1,40 @@
|
|||
import React from "react"
|
||||
|
||||
export const Testimonial = () => {
|
||||
return (
|
||||
<div className="relative max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
|
||||
<blockquote className="text-center lg:mx-auto lg:w-3/5">
|
||||
<div className="mt-6 lg:mt-10">
|
||||
<p className="relative text-xl sm:text-2xl md:text-3xl md:leading-normal font-medium text-gray-800">
|
||||
<svg
|
||||
className="absolute top-0 start-0 transform -translate-x-8 -translate-y-8 h-16 w-16 text-gray-200 sm:h-24 sm:w-24 dark:text-gray-700"
|
||||
width="16"
|
||||
height="13"
|
||||
viewBox="0 0 16 13"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M7.18079 9.25611C7.18079 10.0101 6.93759 10.6211 6.45119 11.0891C5.96479 11.5311 5.35039 11.7521 4.60799 11.7521C3.71199 11.7521 2.96958 11.4531 2.38078 10.8551C1.81758 10.2571 1.53598 9.39911 1.53598 8.28111C1.53598 7.08511 1.86878 5.91511 2.53438 4.77111C3.22559 3.60111 4.18559 2.67811 5.41439 2.00211L6.29759 3.36711C5.63199 3.83511 5.09439 4.35511 4.68479 4.92711C4.30079 5.49911 4.04479 6.16211 3.91679 6.91611C4.14719 6.81211 4.41599 6.76011 4.72319 6.76011C5.43999 6.76011 6.02879 6.99411 6.48959 7.46211C6.95039 7.93011 7.18079 8.52811 7.18079 9.25611ZM14.2464 9.25611C14.2464 10.0101 14.0032 10.6211 13.5168 11.0891C13.0304 11.5311 12.416 11.7521 11.6736 11.7521C10.7776 11.7521 10.0352 11.4531 9.44639 10.8551C8.88319 10.2571 8.60159 9.39911 8.60159 8.28111C8.60159 7.08511 8.93439 5.91511 9.59999 4.77111C10.2912 3.60111 11.2512 2.67811 12.48 2.00211L13.3632 3.36711C12.6976 3.83511 12.16 4.35511 11.7504 4.92711C11.3664 5.49911 11.1104 6.16211 10.9824 6.91611C11.2128 6.81211 11.4816 6.76011 11.7888 6.76011C12.5056 6.76011 13.0944 6.99411 13.5552 7.46211C14.016 7.93011 14.2464 8.52811 14.2464 9.25611Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="relative z-10 italic text-gray-800 dark:text-gray-200">
|
||||
Taken our charity{"'"}s data automation to the next level.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer className="mt-6">
|
||||
<div className="font-semibold text-gray-800 dark:text-gray-200">
|
||||
Debby Goetschalackx
|
||||
</div>
|
||||
<div className="text-sm dark:text-gray-200">
|
||||
Cheif Financial Officer
|
||||
</div>
|
||||
</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
"use client"
|
||||
|
||||
import YouTubeFrame from "@/components/Utilities/YoutubeEmbed"
|
||||
import React from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
function Video() {
|
||||
const [windowWidth, setWindowWidth] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setWindowWidth(window.innerWidth)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 relative">
|
||||
{/* Hero image */}
|
||||
<div
|
||||
className="relative flex justify-center items-center"
|
||||
data-aos="fade-up"
|
||||
data-aos-delay="200"
|
||||
>
|
||||
<YouTubeFrame
|
||||
video="KCHnP62DWpg"
|
||||
videotitle="Sign365 - Fill and Sign Once"
|
||||
width={
|
||||
!!windowWidth && windowWidth > 425
|
||||
? windowWidth > 800
|
||||
? 800
|
||||
: 400
|
||||
: 280
|
||||
}
|
||||
height={!!windowWidth && windowWidth > 425 ? 500 : 240}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Video
|
|
@ -1,18 +1,112 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
// const defaultTheme = require("tailwindcss/defaultTheme");
|
||||
|
||||
const config = {
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./sections/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./node_modules/preline/preline.js"
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||
fontFamily: {
|
||||
heading: "var(--heading-font)",
|
||||
body: "var(--heading-font)",
|
||||
accent: "var(--accent-font)"
|
||||
},
|
||||
},
|
||||
fontSize: {
|
||||
xs: "0.75rem",
|
||||
sm: "0.875rem",
|
||||
base: "1rem",
|
||||
lg: "1.125rem",
|
||||
xl: "1.25rem",
|
||||
"2xl": "1.5rem",
|
||||
"3xl": "2rem",
|
||||
"4xl": "2.5rem",
|
||||
"5xl": "3.25rem",
|
||||
"6xl": "4rem"
|
||||
},
|
||||
letterSpacing: {
|
||||
tighter: "-0.02em",
|
||||
tight: "-0.01em",
|
||||
normal: "0",
|
||||
wide: "0.01em",
|
||||
wider: "0.02em",
|
||||
widest: "0.4em"
|
||||
},
|
||||
typography: {
|
||||
DEFAULT: {
|
||||
css: {
|
||||
p: {
|
||||
fontFamily: "var(--body-font)"
|
||||
},
|
||||
h1: {
|
||||
fontFamily: "var(--heading-font)"
|
||||
},
|
||||
h2: {
|
||||
fontFamily: "var(--heading-font)"
|
||||
},
|
||||
h3: {
|
||||
fontFamily: "var(--heading-font)"
|
||||
},
|
||||
h4: {
|
||||
fontFamily: "var(--heading-font)"
|
||||
},
|
||||
h5: {
|
||||
fontFamily: "var(--heading-font)"
|
||||
},
|
||||
h6: {
|
||||
fontFamily: "var(--heading-font)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
light: {
|
||||
primary: "#FD5469",
|
||||
"primary-content": "#F5F5F5",
|
||||
secondary: "#7082FF",
|
||||
"secondary-content": "#000",
|
||||
accent: "#fd0000",
|
||||
neutral: "#28282e",
|
||||
"base-100": "#FBFAFA",
|
||||
"base-200": "#F9F8F8",
|
||||
"base-300": "#F4F3F4",
|
||||
"base-content": "#000",
|
||||
info: "#00f5ff",
|
||||
success: "#00ff8b",
|
||||
warning: "#ff5100",
|
||||
error: "#ff0051"
|
||||
},
|
||||
dark: {
|
||||
primary: "#FD5469",
|
||||
"primary-content": "#F5F5F5",
|
||||
secondary: "#7082FF",
|
||||
"secondary-content": "#fff",
|
||||
accent: "#006cff",
|
||||
neutral: "#060206",
|
||||
"base-100": "#2a3130",
|
||||
"base-200": "#2a2d2a",
|
||||
"base-300": "#292924",
|
||||
"base-content": "#fff",
|
||||
info: "#009ae0",
|
||||
success: "#76e200",
|
||||
warning: "#eb9400",
|
||||
error: "#be2133"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
require("@tailwindcss/forms"),
|
||||
require("@tailwindcss/typography"),
|
||||
require("daisyui"),
|
||||
require("preline/plugin")
|
||||
]
|
||||
}
|
||||
export default config
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import tw from "../tailwind.config"
|
||||
|
||||
export default tw.daisyui.themes[0]
|
||||
|
||||
export function hexToRgb(hex) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
}
|
||||
: null
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
import * as Yup from "yup"
|
||||
import YupPassword from "yup-password"
|
||||
YupPassword(Yup)
|
||||
|
||||
const isValidMobileNumber = mobileNumber => {
|
||||
if (mobileNumber.length < 8 || mobileNumber.length > 12) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Mobile Number should of 9 to 11 length"
|
||||
}
|
||||
} else if (isNaN(Number(mobileNumber))) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Mobile Number should only contain numbers"
|
||||
}
|
||||
}
|
||||
return { success: true, message: "Valid Mobile Number" }
|
||||
}
|
||||
|
||||
Yup.addMethod(Yup.string, "mobileNumberValidation", function(errorMessage) {
|
||||
return this.test(`test-mobile-number`, errorMessage, function(value) {
|
||||
const { path, createError } = this
|
||||
if (!value) {
|
||||
return createError({ path, message: errorMessage })
|
||||
}
|
||||
const validation = isValidMobileNumber(value)
|
||||
return (
|
||||
(value && validation.success) ||
|
||||
createError({ path, message: validation.message })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const signUpValidationSchema = Yup.object().shape({
|
||||
firstName: Yup.string().required("First Name is required"),
|
||||
lastName: Yup.string().required("Last Name is required"),
|
||||
email: Yup.string()
|
||||
.email()
|
||||
.required("E-mail is required"),
|
||||
phoneNumber: Yup.string()
|
||||
.required("Phone Number is required")
|
||||
.mobileNumberValidation("Phone Number is not valid"),
|
||||
organisation: Yup.string().required("Organisation name is required"),
|
||||
organisationSize: Yup.string().required("Company Size is required"),
|
||||
password: Yup.string()
|
||||
.password()
|
||||
.required("Password is required.")
|
||||
.min(8, "Password is too short - should be 8 characters minimum.")
|
||||
})
|
||||
|
||||
const passwordValidationSchema = Yup.object().shape({
|
||||
newPassword: Yup.string()
|
||||
.password()
|
||||
.required("Password is required.")
|
||||
.min(8, "Password is too short - should be 8 characters minimum."),
|
||||
newPasswordConfirm: Yup.string()
|
||||
.password()
|
||||
.required("Password is required.")
|
||||
.min(8, "Password is too short - should be 8 characters minimum.")
|
||||
})
|
||||
const changeEmailValidationSchema = Yup.object().shape({
|
||||
password: Yup.string()
|
||||
.password()
|
||||
.required("Password is required.")
|
||||
.min(8, "Password is too short - should be 8 characters minimum.")
|
||||
})
|
||||
const waitinglistValidationSchema = Yup.object().shape({
|
||||
firstName: Yup.string().required("First Name is required"),
|
||||
lastName: Yup.string().required("Last Name is required"),
|
||||
email: Yup.string()
|
||||
.email()
|
||||
.required("E-mail is required")
|
||||
})
|
||||
const contactUsValidationSchema = Yup.object().shape({
|
||||
firstName: Yup.string().required("First Name is required"),
|
||||
lastName: Yup.string().required("Last Name is required"),
|
||||
note: Yup.string(),
|
||||
phoneNumber: Yup.string()
|
||||
.required("Phone Number is required")
|
||||
.mobileNumberValidation("Phone Number is not valid"),
|
||||
email: Yup.string()
|
||||
.email()
|
||||
.required("E-mail is required")
|
||||
})
|
||||
|
||||
const emailValidationSchema = Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.email()
|
||||
.required("E-mail is required")
|
||||
})
|
||||
|
||||
const signInValidationSchema = Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.email()
|
||||
.required("E-mail is required"),
|
||||
password: Yup.string()
|
||||
.password()
|
||||
.required("Password is required.")
|
||||
.min(8, "Password is too short - should be 8 characters minimum.")
|
||||
.minUppercase(1, "password must contain at least 1 upper case letter")
|
||||
})
|
||||
|
||||
export {
|
||||
emailValidationSchema,
|
||||
changeEmailValidationSchema,
|
||||
passwordValidationSchema,
|
||||
signUpValidationSchema,
|
||||
signInValidationSchema,
|
||||
waitinglistValidationSchema,
|
||||
contactUsValidationSchema
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import pb from "@/lib/pocketbase"
|
||||
|
||||
const getPostMetadata = async () => {
|
||||
try {
|
||||
return await pb.collection("blog").getFullList({ requestKey: "blogs" })
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export default getPostMetadata
|