├── .commitlintrc.json
├── .eslintrc.json
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .nvmrc
├── .prettierignore
├── README.md
├── actions
├── generate-user-stripe.ts
└── update-user-name.ts
├── app
├── (auth)
│ ├── layout.tsx
│ ├── login
│ │ └── page.tsx
│ └── register
│ │ └── page.tsx
├── (dashboard)
│ ├── dashboard
│ │ ├── actions.ts
│ │ ├── activity
│ │ │ ├── activity-log.tsx
│ │ │ ├── campaign-selector.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── billing
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── campaigns
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── integrations
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ ├── notifications
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ └── settings
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ └── playground
│ │ ├── form.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── search-results.tsx
├── (marketing)
│ ├── components
│ │ ├── 3DDemo.tsx
│ │ ├── CallToAction.tsx
│ │ ├── FeatureSection.tsx
│ │ ├── HeroSection.tsx
│ │ ├── HowItWorksSection.tsx
│ │ ├── MakerContent.tsx
│ │ ├── MobileSection.tsx
│ │ ├── PricingSection.tsx
│ │ ├── ProofSuccess.tsx
│ │ ├── TabButtons.tsx
│ │ └── page.tsx
│ ├── error.tsx
│ ├── layout.tsx
│ ├── page.tsx
│ └── pricing
│ │ ├── loading.tsx
│ │ └── page.tsx
├── (project)
│ └── project
│ │ ├── _components
│ │ ├── client-tab-wrapper.tsx
│ │ ├── delete-post-modal.tsx
│ │ ├── edit-comment.tsx
│ │ ├── find-content-button.tsx
│ │ ├── generate-comment.tsx
│ │ └── post-comment.tsx
│ │ ├── activity
│ │ ├── activity-log.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
│ │ ├── explorer
│ │ ├── components
│ │ │ ├── campaign-selector.tsx
│ │ │ ├── keyword-management-modal.tsx
│ │ │ ├── playground-results-display.tsx
│ │ │ └── playground-settings-form.tsx
│ │ ├── data
│ │ │ ├── models.ts
│ │ │ └── presets.ts
│ │ └── page.tsx
│ │ ├── keywords
│ │ ├── loading.tsx
│ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ ├── page.tsx
│ │ ├── playground
│ │ ├── components
│ │ │ └── form.tsx
│ │ └── page.tsx
│ │ ├── preferences
│ │ ├── loading.tsx
│ │ └── page.tsx
│ │ └── settings
│ │ ├── loading.tsx
│ │ └── page.tsx
├── actions
│ ├── activity-log.tsx
│ ├── ai.ts
│ ├── apify.ts
│ ├── campaign.ts
│ ├── comment.ts
│ ├── email.ts
│ ├── fetch-linkedin.ts
│ ├── index.ts
│ ├── keywords.ts
│ ├── notification.ts
│ ├── post-preferences.ts
│ ├── post.ts
│ └── twitter.ts
├── api
│ ├── auth
│ │ ├── [...nextauth]
│ │ │ └── route.ts
│ │ └── linkedin
│ │ │ └── callback
│ │ │ └── route.ts
│ ├── callback
│ │ ├── linkedin
│ │ │ └── route.ts
│ │ ├── route.ts
│ │ └── twitter
│ │ │ └── route.ts
│ ├── cron
│ │ └── campaign
│ │ │ ├── comment
│ │ │ └── route.ts
│ │ │ └── post
│ │ │ └── route.ts
│ ├── linkedin
│ │ └── route.ts
│ ├── og
│ │ └── route.tsx
│ ├── openai
│ │ ├── openai.ts
│ │ └── route.ts
│ ├── reddit
│ │ └── route.ts
│ ├── search
│ │ └── route.ts
│ ├── webhooks
│ │ └── stripe
│ │ │ └── route.ts
│ └── x
│ │ └── route.ts
├── layout.tsx
├── opengraph-image.jpg
└── robots.ts
├── assets
└── fonts
│ ├── CalSans-SemiBold.ttf
│ ├── CalSans-SemiBold.woff2
│ ├── Inter-Bold.ttf
│ ├── Inter-Regular.ttf
│ └── index.ts
├── components.json
├── components
├── 3d-pin.tsx
├── analytics.tsx
├── bentogrid.tsx
├── billing-info.tsx
├── blog-posts.tsx
├── browser.tsx
├── connect-linkedin-button.tsx
├── connect-linkedin-card.tsx
├── connect-twitter-button.tsx
├── connect-twitter-card.tsx
├── content
│ ├── mdx-card.tsx
│ └── mdx-components.tsx
├── dashboard
│ ├── activity-log
│ │ ├── columns.tsx
│ │ ├── data-table.tsx
│ │ └── index.tsx
│ ├── add-campaign-modal.tsx
│ ├── add-keywords-modal.tsx
│ ├── add-post-preferences-moda.tsx
│ ├── autopilot-switch.tsx
│ ├── campaign-menu.tsx
│ ├── campaigns.tsx
│ ├── collapsible-server.tsx
│ ├── empty-campaign-card.tsx
│ ├── header.tsx
│ ├── keyword-select-form.tsx
│ ├── post-preferences-form.tsx
│ ├── shell.tsx
│ └── subscription-modal.tsx
├── disconnect-linkedin-button.tsx
├── docs
│ ├── page-header.tsx
│ ├── search.tsx
│ └── sidebar-nav.tsx
├── features.tsx
├── feedback.tsx
├── forms
│ ├── apidojo-twitter-scrapper-form.tsx
│ ├── billing-form-button.tsx
│ ├── invite-user.tsx
│ ├── microworlds-twitter-scrapper-form.tsx
│ ├── repo.tsx
│ ├── user-auth-form.tsx
│ └── user-name-form.tsx
├── gif-card.tsx
├── howitworks.tsx
├── info-landing.tsx
├── layout
│ ├── collapsible-demo.tsx
│ ├── landing-steps.tsx
│ ├── main-nav.tsx
│ ├── marketing-section.tsx
│ ├── mobile-nav.tsx
│ ├── mode-toggle.tsx
│ ├── nav.tsx
│ ├── navbar.tsx
│ ├── preview-select.tsx
│ ├── project-requirements.tsx
│ ├── select-worktype.tsx
│ ├── sign-in-modal.tsx
│ ├── site-footer.tsx
│ ├── sticky-scroll.tsx
│ ├── user-account-nav.tsx
│ └── user-explain.tsx
├── linkedin-card.tsx
├── magicui
│ └── sparkles-text.tsx
├── modal-provider.tsx
├── pricing-cards.tsx
├── pricing-faq.tsx
├── project
│ ├── form-modal.tsx
│ ├── repo-dropdown.tsx
│ └── submit-modal.tsx
├── providers.tsx
├── query-provider.tsx
├── radar
│ ├── IconContainer.tsx
│ ├── Preview.tsx
│ └── Radar.tsx
├── shared
│ ├── callout.tsx
│ ├── card-skeleton.tsx
│ ├── empty-placeholder.tsx
│ ├── header-section.tsx
│ ├── icons.tsx
│ ├── modal.tsx
│ ├── toc.tsx
│ └── user-avatar.tsx
├── tailwind-indicator.tsx
├── timeline.tsx
├── twitter-card.tsx
├── ugc-text.tsx
└── ui
│ ├── 3d-card.tsx
│ ├── 3d-pin.tsx
│ ├── accordion.tsx
│ ├── alert-dialog.tsx
│ ├── alert.tsx
│ ├── animated-tooltip.tsx
│ ├── aspect-ratio.tsx
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── bento-grid.tsx
│ ├── button.tsx
│ ├── calendar.tsx
│ ├── card.tsx
│ ├── checkbox.tsx
│ ├── collapsible.tsx
│ ├── command.tsx
│ ├── container-scroll-animation.tsx
│ ├── context-menu.tsx
│ ├── dialog.tsx
│ ├── drawer.tsx
│ ├── dropdown-menu.tsx
│ ├── fancy-tabs.tsx
│ ├── form.tsx
│ ├── hero-highlight.tsx
│ ├── hero-parallax.tsx
│ ├── hover-card.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── macbook-scroll.tsx
│ ├── menubar.tsx
│ ├── moving-border.tsx
│ ├── navigation-menu.tsx
│ ├── popover.tsx
│ ├── progress.tsx
│ ├── radio-group.tsx
│ ├── resizable.tsx
│ ├── scroll-area.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── sheet.tsx
│ ├── skeleton.tsx
│ ├── slider.tsx
│ ├── sonner.tsx
│ ├── stepper
│ ├── context.tsx
│ ├── horizontal-step.tsx
│ ├── index.tsx
│ ├── step-button-container.tsx
│ ├── step-icon.tsx
│ ├── step-label.tsx
│ ├── step.tsx
│ ├── types.ts
│ ├── use-media-query.tsx
│ ├── use-stepper.ts
│ └── vertical-step.tsx
│ ├── sticky-scroll-reveal.tsx
│ ├── switch.tsx
│ ├── table.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ ├── toggle.tsx
│ ├── tooltip.tsx
│ ├── tracing-beam.tsx
│ ├── tweet-card.tsx
│ └── use-toast.ts
├── config
├── dashboard.ts
├── docs.ts
├── landing.ts
├── marketing.ts
├── site.ts
└── subscriptions.ts
├── content
├── authors
│ └── shadcn.mdx
├── blog
│ ├── deploying-next-apps.mdx
│ ├── dynamic-routing-static-regeneration.mdx
│ ├── preview-mode-headless-cms.mdx
│ └── server-client-components.mdx
├── docs
│ ├── documentation
│ │ ├── code-blocks.mdx
│ │ ├── components.mdx
│ │ ├── index.mdx
│ │ └── style-guide.mdx
│ ├── in-progress.mdx
│ └── index.mdx
├── guides
│ ├── build-blog-using-contentlayer-mdx.mdx
│ └── using-next-auth-next-13.mdx
└── pages
│ ├── privacy.mdx
│ └── terms.mdx
├── emails
├── autopilot-on.tsx
├── magic-link-email.tsx
├── weekly-summary.tsx
└── welcome-email.tsx
├── env.mjs
├── hooks
├── use-intersection-observer.ts
├── use-keywords-modal.tsx
├── use-local-storage.ts
├── use-lock-body.ts
├── use-media-query.ts
├── use-mounted.ts
├── use-mutation-observer.ts
├── use-post-preferences.tsx
├── use-scroll.ts
├── use-signin-modal.ts
└── use-tab-store.ts
├── lib
├── apify.ts
├── auth.ts
├── config
│ └── index.ts
├── db.ts
├── email.ts
├── encryption.ts
├── exceptions.ts
├── linkedin-api
│ ├── access_token.json
│ └── index.ts
├── linkedin.ts
├── queries.ts
├── reddit.ts
├── reddit
│ └── reddit.ts
├── session.ts
├── stripe.ts
├── subscription.ts
├── toc.ts
├── twitter.ts
├── utils.ts
└── validations
│ ├── auth.ts
│ ├── og.ts
│ ├── twitter.ts
│ └── user.ts
├── middleware.ts
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── prisma
├── migrations
│ └── 0_init
│ │ └── migration.sql
├── schema.prisma
└── seed.ts
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── cases
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ └── 5.png
├── creates.gif
├── demo.gif
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── images
│ ├── 400x400.jpg
│ ├── avatars
│ │ └── shadcn.png
│ ├── blog
│ │ ├── blog-post-1.jpg
│ │ ├── blog-post-2.jpg
│ │ ├── blog-post-3.jpg
│ │ └── blog-post-4.jpg
│ ├── hero.png
│ ├── user-step1.gif
│ ├── user-step2.gif
│ └── user-step3.gif
├── og.jpg
├── site.webmanifest
├── steps
│ ├── step1.gif
│ ├── step2.gif
│ └── step3.gif
└── vercel.svg
├── schemas
├── apify.ts
├── campaign.ts
├── content-interaction.ts
├── logging-notification.ts
├── notification.ts
└── playground.ts
├── styles
├── globals.css
└── mdx.css
├── tailwind.config.js
├── tsconfig.json
├── types
├── env.d.ts
├── index.d.ts
├── next-auth.d.ts
└── reddit.ts
├── utils
└── cn.ts
└── vercel.json
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "prettier",
7 | "plugin:tailwindcss/recommended"
8 | ],
9 | "plugins": ["tailwindcss"],
10 | "rules": {
11 | "@next/next/no-html-link-for-pages": "off",
12 | "react/jsx-key": "off",
13 | "tailwindcss/no-custom-classname": "off",
14 | "tailwindcss/classnames-order": "off"
15 | },
16 | "settings": {
17 | "tailwindcss": {
18 | "callees": ["cn"],
19 | "config": "tailwind.config.js"
20 | },
21 | "next": {
22 | "rootDir": true
23 | }
24 | },
25 | "overrides": [
26 | {
27 | "files": ["*.ts", "*.tsx"],
28 | "parser": "@typescript-eslint/parser"
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | # email
40 | /.react-email/
41 |
42 | .vscode
43 | .contentlayer
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx pretty-quick --staged
5 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v16.18.0
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .next
4 | build
5 | .contentlayer
--------------------------------------------------------------------------------
/actions/update-user-name.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { authOptions } from "@/lib/auth";
4 | import { prisma } from "@/lib/db";
5 | import { userNameSchema } from "@/lib/validations/user";
6 | import { getServerSession } from "next-auth";
7 | import { revalidatePath } from "next/cache";
8 |
9 | export type FormData = {
10 | name: string;
11 | };
12 |
13 | export async function updateUserName(userId: string, data: FormData) {
14 | try {
15 | const session = await getServerSession(authOptions)
16 |
17 | if (!session?.user || session?.user.id !== userId) {
18 | throw new Error("Unauthorized");
19 | }
20 |
21 | const { name } = userNameSchema.parse(data);
22 |
23 | // Update the user name.
24 | await prisma.user.update({
25 | where: {
26 | id: userId,
27 | },
28 | data: {
29 | name: name,
30 | },
31 | })
32 |
33 | revalidatePath('/dashboard/settings');
34 | return { status: "success" };
35 | } catch (error) {
36 | console.log(error)
37 | return { status: "error" }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | interface AuthLayoutProps {
2 | children: React.ReactNode
3 | }
4 |
5 | export default function AuthLayout({ children }: AuthLayoutProps) {
6 | return
{children}
7 | }
8 |
--------------------------------------------------------------------------------
/app/(auth)/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next"
2 | import Link from "next/link"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 | import { Icons } from "@/components/shared/icons"
7 | import { UserAuthForm } from "@/components/forms/user-auth-form"
8 | import { Suspense } from "react"
9 |
10 | export const metadata: Metadata = {
11 | title: "Login",
12 | description: "Login to your account",
13 | }
14 |
15 | export default function LoginPage() {
16 | return (
17 |
18 |
25 | <>
26 |
27 | Back
28 | >
29 |
30 |
31 |
32 |
33 |
34 | Welcome back
35 |
36 |
37 | Enter your email to sign in to your account
38 |
39 |
40 |
41 |
42 |
43 |
44 |
48 | Don't have an account? Sign Up
49 |
50 |
51 |
52 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/app/(auth)/register/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { cn } from "@/lib/utils"
4 | import { buttonVariants } from "@/components/ui/button"
5 | import { Icons } from "@/components/shared/icons"
6 | import { UserAuthForm } from "@/components/forms/user-auth-form"
7 | import { Suspense } from "react"
8 |
9 | export const metadata = {
10 | title: "Create an account",
11 | description: "Create an account to get started.",
12 | }
13 |
14 | export default function RegisterPage() {
15 | return (
16 |
17 |
24 | Login
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Create an account
33 |
34 |
35 | Enter your email below to create your account
36 |
37 |
38 |
39 |
40 |
41 |
42 | By clicking continue, you agree to our{" "}
43 |
47 | Terms of Service
48 | {" "}
49 | and{" "}
50 |
54 | Privacy Policy
55 |
56 | .
57 |
58 |
59 |
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/activity/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/shared/card-skeleton"
2 | import { DashboardHeader } from "@/components/dashboard/header"
3 | import { DashboardShell } from "@/components/dashboard/shell"
4 |
5 | export default function DashboardBillingLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/activity/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 | import { Campaign } from "@prisma/client";
3 |
4 | import { authOptions } from "@/lib/auth";
5 | import { getCurrentUser } from "@/lib/session";
6 | import { DashboardHeader } from "@/components/dashboard/header";
7 | import { DashboardShell } from "@/components/dashboard/shell";
8 | import { getAllCampaigns } from "@/app/actions/campaign";
9 |
10 | import { ActivityLogTable } from "./activity-log";
11 |
12 | export const metadata = {
13 | title: "BuzzDaddy Overview",
14 | description: "Manage account and website settings.",
15 | };
16 |
17 | export default async function SettingsPage() {
18 | const user = await getCurrentUser();
19 |
20 | if (!user) {
21 | redirect(authOptions?.pages?.signIn || "/login");
22 | }
23 |
24 | const campaignsResult = await getAllCampaigns(user.id);
25 |
26 | if (campaignsResult.type === "error") {
27 | throw new Error(campaignsResult.message);
28 | }
29 |
30 | const campaigns: Campaign[] = campaignsResult.data || [];
31 |
32 | return (
33 |
34 |
38 |
39 |
({
41 | id: campaign.id,
42 | name: campaign.name,
43 | }))}
44 | />
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/billing/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/shared/card-skeleton"
2 | import { DashboardHeader } from "@/components/dashboard/header"
3 | import { DashboardShell } from "@/components/dashboard/shell"
4 |
5 | export default function DashboardBillingLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/billing/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | import { authOptions } from "@/lib/auth";
4 | import { getCurrentUser } from "@/lib/session";
5 | import { getUserSubscriptionPlan } from "@/lib/subscription";
6 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
7 | import { BillingInfo } from "@/components/billing-info";
8 | import { DashboardHeader } from "@/components/dashboard/header";
9 | import { DashboardShell } from "@/components/dashboard/shell";
10 | import { Icons } from "@/components/shared/icons";
11 |
12 | export const metadata = {
13 | title: "Billing",
14 | description: "Manage billing and your subscription plan.",
15 | };
16 |
17 | export default async function BillingPage() {
18 | const user = await getCurrentUser();
19 |
20 | if (!user) {
21 | redirect(authOptions?.pages?.signIn || "/login");
22 | }
23 |
24 | const subscriptionPlan = await getUserSubscriptionPlan(user.id);
25 |
26 | console.log("user", user);
27 | console.log("subscriptionPlan", subscriptionPlan);
28 |
29 | return (
30 |
31 |
35 |
36 |
37 |
38 | This is a demo app.
39 |
40 | BuzzDaddy is a demo app using a Stripe test environment. You can
41 | find a list of test card numbers on the{" "}
42 |
48 | Stripe docs
49 |
50 | .
51 |
52 |
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/campaigns/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/shared/card-skeleton"
2 | import { DashboardHeader } from "@/components/dashboard/header"
3 | import { DashboardShell } from "@/components/dashboard/shell"
4 |
5 | export default function DashboardSettingsLoading() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/campaigns/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | import { authOptions } from "@/lib/auth";
4 | import { getCurrentUser } from "@/lib/session";
5 | import { Campaigns } from "@/components/dashboard/campaigns";
6 | import { DashboardHeader } from "@/components/dashboard/header";
7 | import { DashboardShell } from "@/components/dashboard/shell";
8 | // import { AddCampaignModal } from "@/components/dashboard/add-campaign-modal";
9 | import { getAllCampaigns } from "@/app/actions/campaign";
10 |
11 | export const metadata = {
12 | title: "BuzzDaddy Overview",
13 | description: "Manage account and website settings.",
14 | };
15 |
16 | export default async function SettingsPage() {
17 | const user = await getCurrentUser();
18 |
19 | if (!user) {
20 | redirect(authOptions?.pages?.signIn || "/login");
21 | }
22 |
23 | const result = await getAllCampaigns(user.id);
24 |
25 | if (result.type === "error" || !result.data) {
26 | console.error(result.message);
27 | return null;
28 | }
29 |
30 | return (
31 |
32 |
33 | {/* */}
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/integrations/loading.tsx:
--------------------------------------------------------------------------------
1 | import { DashboardHeader } from "@/components/dashboard/header";
2 | import { DashboardShell } from "@/components/dashboard/shell";
3 | import { CardSkeleton } from "@/components/shared/card-skeleton";
4 |
5 | export default function DashboardBillingLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/integrations/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | import { authOptions } from "@/lib/auth";
4 | import { getLinkedInToken, getTwitterToken } from "@/lib/queries";
5 | import { getCurrentUser } from "@/lib/session";
6 | import { ConnectLinkedInCard } from "@/components/connect-linkedin-card";
7 | import { ConnectTwitterCard } from "@/components/connect-twitter-card";
8 | import { DashboardHeader } from "@/components/dashboard/header";
9 | import { DashboardShell } from "@/components/dashboard/shell";
10 | import { LinkedInCard } from "@/components/linkedin-card";
11 | import { TwitterCard } from "@/components/twitter-card";
12 |
13 | export const metadata = {
14 | title: "Integrations",
15 | description: "Manage your app integrations.",
16 | };
17 |
18 | export default async function IntegrationsPage() {
19 | const user = await getCurrentUser();
20 |
21 | if (!user) {
22 | redirect(authOptions?.pages?.signIn || "/login");
23 | }
24 |
25 | const twitterToken = await getTwitterToken(user.id);
26 | const twitterAccessToken = twitterToken?.accessToken
27 | ? twitterToken.accessToken
28 | : "";
29 |
30 | const linkedToken = await getLinkedInToken(user.id);
31 | const linkedinAccessToken = linkedToken?.accessToken
32 | ? linkedToken.accessToken
33 | : "";
34 |
35 | return (
36 |
37 |
41 |
42 | {twitterAccessToken ? (
43 |
44 | ) : (
45 | <>
46 |
47 | >
48 | )}
49 |
50 | {linkedinAccessToken ? (
51 |
52 | ) : (
53 |
54 | )}
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 |
3 | import { dashboardConfig } from "@/config/dashboard";
4 | import { getCurrentUser } from "@/lib/session";
5 | import { NestedNav } from "@/components/layout/nav";
6 | import { NavBar } from "@/components/layout/navbar";
7 | import { SiteFooter } from "@/components/layout/site-footer";
8 |
9 | interface DashboardLayoutProps {
10 | children?: React.ReactNode;
11 | }
12 |
13 | export default async function DashboardLayout({
14 | children,
15 | }: DashboardLayoutProps) {
16 | const user = await getCurrentUser();
17 |
18 | if (!user) {
19 | return notFound();
20 | }
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
30 |
31 | {children}
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/shared/card-skeleton"
2 | import { DashboardHeader } from "@/components/dashboard/header"
3 | import { DashboardShell } from "@/components/dashboard/shell"
4 | import { Button } from "@/components/ui/button"
5 |
6 | export default function DashboardLoading() {
7 | return (
8 |
9 |
10 | Add campaign
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/notifications/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation"
2 |
3 | import { BillingInfo } from "@/components/billing-info"
4 | import { DashboardHeader } from "@/components/dashboard/header"
5 | import { DashboardShell } from "@/components/dashboard/shell"
6 | import { Icons } from "@/components/shared/icons"
7 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
8 | import { authOptions } from "@/lib/auth"
9 | import { getCurrentUser } from "@/lib/session"
10 | import { getUserSubscriptionPlan } from "@/lib/subscription"
11 |
12 | export const metadata = {
13 | title: "Activity Log",
14 | description: "Your activity log and notifications.",
15 | }
16 |
17 | export default async function BillingPage() {
18 | const user = await getCurrentUser()
19 |
20 | if (!user) {
21 | redirect(authOptions?.pages?.signIn || "/login")
22 | }
23 |
24 | const subscriptionPlan = await getUserSubscriptionPlan(user.id)
25 |
26 | return (
27 |
28 |
32 |
33 |
34 |
35 | This is a demo app.
36 |
37 | BuzzDaddy is a demo app using a Stripe test environment. You can
38 | find a list of test card numbers on the{" "}
39 |
45 | Stripe docs
46 |
47 | .
48 |
49 |
50 |
53 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/settings/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/shared/card-skeleton"
2 | import { DashboardHeader } from "@/components/dashboard/header"
3 | import { DashboardShell } from "@/components/dashboard/shell"
4 |
5 | export default function DashboardSettingsLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation"
2 |
3 | import { authOptions } from "@/lib/auth"
4 | import { getCurrentUser } from "@/lib/session"
5 | import { DashboardHeader } from "@/components/dashboard/header"
6 | import { DashboardShell } from "@/components/dashboard/shell"
7 | import { UserNameForm } from "@/components/forms/user-name-form"
8 | import { InviteUserForm } from "@/components/forms/invite-user"
9 |
10 | export const metadata = {
11 | title: "Settings",
12 | description: "Manage account and website settings.",
13 | }
14 |
15 | export default async function SettingsPage() {
16 | const user = await getCurrentUser()
17 |
18 | if (!user) {
19 | redirect(authOptions?.pages?.signIn || "/login")
20 | }
21 |
22 | return (
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/app/(dashboard)/playground/layout.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 |
3 | import { dashboardConfig } from "@/config/dashboard";
4 | import { getCurrentUser } from "@/lib/session";
5 | import { NavBar } from "@/components/layout/navbar";
6 | import { SiteFooter } from "@/components/layout/site-footer";
7 |
8 | interface PlaygroundLayoutProps {
9 | children?: React.ReactNode;
10 | }
11 |
12 | export default async function PlaygroundLayout({
13 | children,
14 | }: PlaygroundLayoutProps) {
15 | const user = await getCurrentUser();
16 |
17 | if (!user) {
18 | return notFound();
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 | {children}
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/app/(dashboard)/playground/search-results.tsx:
--------------------------------------------------------------------------------
1 | export const dynamic = "force-dynamic";
2 |
3 | interface SearchResult {
4 | title: string;
5 | link: string;
6 | snippet: string;
7 | }
8 |
9 | const SearchResults = ({
10 | searchParams,
11 | results,
12 | }: {
13 | searchParams: { [key: string]: string };
14 | results: SearchResult[];
15 | }) => {
16 | return (
17 |
18 | {results.length > 0 ? (
19 |
20 |
Search Results:
21 |
36 |
37 | ) : (
38 |
39 | Try searching for something using using different keywords on the
40 | left.
41 |
42 | )}
43 |
44 | );
45 | };
46 |
47 | export default SearchResults;
48 |
--------------------------------------------------------------------------------
/app/(marketing)/components/CallToAction.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button } from "@/components/ui/moving-border";
3 |
4 | const CallToAction = ({ onClick }) => (
5 |
9 |
14 | Get started
15 |
16 |
17 | );
18 |
19 | export default CallToAction;
20 |
--------------------------------------------------------------------------------
/app/(marketing)/components/FeatureSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BentoGrid } from "@/components/bentogrid";
3 |
4 | const FeatureSection = () => (
5 |
9 |
10 |
11 | Features
12 |
13 |
14 | Leverage our low-cost automation tool to promote your business 24/7. BuzzDaddy will mention you naturally in recent & relevant posts.
15 |
16 |
17 |
18 |
19 | );
20 |
21 | export default FeatureSection;
22 |
--------------------------------------------------------------------------------
/app/(marketing)/components/HeroSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const HeroSection = ({ signInModal }) => (
4 |
5 |
6 |
10 | Name-drop your business across social media 24/7 with BuzzDaddy to boost sales
11 |
12 |
16 | Meet your customizable bot that replies to public X, LinkedIn & Reddit posts on your behalf - generating organic buzz for your business.
17 |
18 |
19 |
20 | );
21 |
22 | export default HeroSection;
23 |
--------------------------------------------------------------------------------
/app/(marketing)/components/MakerContent.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSigninModal } from "@/hooks/use-signin-modal";
3 | import HeroSection from "./HeroSection";
4 | import CallToAction from "./CallToAction";
5 | import HowItWorksSection from "./HowItWorksSection";
6 | import FeatureSection from "./FeatureSection";
7 | import PricingSection from "./PricingSection";
8 | import PreviewRadar from "@/components/radar/Preview";
9 | import YoutubeSection from "./ProofSuccess";
10 | import ThreeDComponent from "./3DDemo";
11 | import MobileSection from "./MobileSection";
12 |
13 | const MakerContent = () => {
14 | const signInModal = useSigninModal();
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default MakerContent;
31 |
--------------------------------------------------------------------------------
/app/(marketing)/components/MobileSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { InfoLanding } from "@/components/info-landing";
3 | import { infos } from "@/config/landing"; // Assuming infos is defined here
4 |
5 | const MobileSection = () => {
6 | const gridPatternStyle = {
7 | backgroundImage: `
8 | linear-gradient(to top right, rgba(9,9,11,255) 37%, rgba(0,0,0,0) 100%),
9 | linear-gradient(rgba(255,255,255,0.3) 1px, transparent 1px),
10 | linear-gradient(90deg, rgba(255,255,255,0.3) 1px, transparent 1px)
11 | `,
12 | backgroundSize: "cover, 110px 60px, 70px 40px",
13 | backgroundBlendMode: "normal, normal, normal",
14 | };
15 |
16 | return (
17 |
30 | );
31 | };
32 |
33 | export default MobileSection;
34 |
--------------------------------------------------------------------------------
/app/(marketing)/components/PricingSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { PricingFaq } from "@/components/pricing-faq";
3 |
4 | const PricingSection = () => (
5 |
8 | );
9 |
10 | export default PricingSection;
11 |
--------------------------------------------------------------------------------
/app/(marketing)/components/ProofSuccess.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ContainerScroll } from "@/components/ui/container-scroll-animation";
3 | import { UGCText } from "@/components/ugc-text";
4 | import Image from "next/image";
5 |
6 | const images = [
7 | '/cases/1.png',
8 | '/cases/2.png',
9 | '/cases/3.png',
10 | '/cases/4.png'
11 | ];
12 |
13 | const YoutubeSection = () => {
14 | const gridPatternStyle = {
15 | backgroundImage: `
16 | linear-gradient(to top right, rgba(9,9,11,255) 37%, rgba(0,0,0,0) 100%),
17 | linear-gradient(rgba(255,255,255,0.3) 1px, transparent 1px),
18 | linear-gradient(90deg, rgba(255,255,255,0.3) 1px, transparent 1px)
19 | `,
20 | backgroundSize: "cover, 110px 60px, 70px 40px",
21 | backgroundBlendMode: "normal, normal, normal",
22 | };
23 |
24 | return (
25 |
35 |
36 |
39 |
40 |
41 | People love our replies 😁
42 |
43 | >
44 | }
45 | >
46 |
47 | {images.map((image, index) => (
48 |
49 |
50 |
51 | ))}
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default YoutubeSection;
60 |
--------------------------------------------------------------------------------
/app/(marketing)/components/TabButtons.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const TabButtons = ({ activeTab, setActiveTab }) => {
4 | const buttonClass = (tab) =>
5 | `px-4 py-2 text-sm font-semibold transition duration-300 ${
6 | activeTab === tab
7 | ? "text-secondary bg-primary"
8 | : "text-gray-300 bg-transparent"
9 | }`;
10 |
11 | return (
12 |
13 | setActiveTab("makers")}
15 | className={buttonClass("makers")}
16 | style={{ animationDelay: "0.321s", animationFillMode: "forwards" }}
17 | >
18 | 🎉 Made for Creators, Startups, E-Commerce
19 |
20 | {/* Uncomment if you want the Developers tab */}
21 | {/* setActiveTab("developers")}
23 | className={buttonClass("developers")}
24 | style={{ animationDelay: "0.1s", animationFillMode: "forwards" }}
25 | >
26 | For Developers
27 | */}
28 |
29 | );
30 | };
31 |
32 | export default TabButtons;
33 |
--------------------------------------------------------------------------------
/app/(marketing)/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Button } from '@/components/ui/button';
4 |
5 | export default function Error({
6 | reset,
7 | }: {
8 | reset: () => void;
9 | }) {
10 |
11 | return (
12 |
13 |
Something went wrong!
14 | reset()}
18 | >
19 | Try again
20 |
21 |
22 | );
23 | }
--------------------------------------------------------------------------------
/app/(marketing)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { NavBar } from "@/components/layout/navbar"
2 | import { SiteFooter } from "@/components/layout/site-footer"
3 | import { marketingConfig } from "@/config/marketing"
4 | import { getCurrentUser } from "@/lib/session"
5 | import { Suspense } from "react"
6 |
7 | interface MarketingLayoutProps {
8 | children: React.ReactNode
9 | }
10 |
11 | export default async function MarketingLayout({
12 | children,
13 | }: MarketingLayoutProps) {
14 | const user = await getCurrentUser()
15 |
16 | return (
17 |
18 |
19 |
20 |
21 | {children}
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/app/(marketing)/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState } from "react";
4 | import TabButtons from "./components/TabButtons";
5 | import MakerContent from "./components/MakerContent";
6 |
7 | export default function IndexPage() {
8 | const [activeTab, setActiveTab] = useState("makers");
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
21 | {activeTab === "makers" && }
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/(marketing)/pricing/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton"
2 |
3 | export default function Loading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
--------------------------------------------------------------------------------
/app/(marketing)/pricing/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { PricingCards } from '@/components/pricing-cards';
3 | import { PricingFaq } from '@/components/pricing-faq';
4 | import { Skeleton } from '@/components/ui/skeleton';
5 | import { getCurrentUser } from '@/lib/session';
6 | import { getUserSubscriptionPlan } from '@/lib/subscription';
7 |
8 | export const metadata = {
9 | title: "Pricing",
10 | }
11 |
12 | export default async function PricingPage() {
13 | const user = await getCurrentUser()
14 | let subscriptionPlan;
15 |
16 | if (user) {
17 | subscriptionPlan = await getUserSubscriptionPlan(user.id)
18 | }
19 |
20 | return (
21 |
26 | )
27 | }
--------------------------------------------------------------------------------
/app/(project)/project/_components/client-tab-wrapper.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Platform } from "@/schemas/content-interaction";
5 | import { FaLinkedinIn, FaRedditAlien, FaXTwitter } from "react-icons/fa6";
6 |
7 | import { useTabStore } from "@/hooks/use-tab-store";
8 | import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
9 |
10 | export function ClientTabWrapper({ children }: { children: React.ReactNode }) {
11 | const { setActiveTab, activeTab } = useTabStore();
12 |
13 | return (
14 | {
17 | // console.log("Tab changed to:", value);
18 | setActiveTab(value as Platform);
19 | }}
20 | className="w-full"
21 | >
22 |
23 |
24 |
25 | Twitter
26 |
27 |
28 |
29 | LinkedIn
30 |
31 |
32 |
33 | Reddit
34 |
35 |
36 | {children}
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/app/(project)/project/_components/generate-comment.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { Campaign, Post } from "@prisma/client";
5 | import { useMutation } from "@tanstack/react-query";
6 | import { BsRobot } from "react-icons/bs";
7 | import { toast } from "sonner";
8 |
9 | import { Button } from "@/components/ui/button";
10 | import { generatePostComment } from "@/app/actions/ai";
11 |
12 | interface GenerateCommentProps {
13 | post: Post;
14 | campaign: Campaign;
15 | }
16 |
17 | export const GenerateComment = ({ post, campaign }: GenerateCommentProps) => {
18 | const router = useRouter();
19 |
20 | const createNewComment = async () => {
21 | const result = await generatePostComment(post, campaign);
22 |
23 | if (result.type === "success") {
24 | return result.comment;
25 | }
26 |
27 | throw new Error("Failed to generate comment");
28 | };
29 |
30 | const { mutate, isPending } = useMutation({
31 | mutationFn: createNewComment,
32 | onSuccess(data, variables, context) {
33 | console.log(data);
34 | toast.success("Comment generated successfully");
35 | router.refresh();
36 | },
37 | onError(error, variables, context) {
38 | console.error(error);
39 | toast.error("Failed to generate comment");
40 | },
41 | });
42 |
43 | return (
44 | mutate()}
50 | >
51 |
52 | Generate Reply
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/app/(project)/project/activity/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/shared/card-skeleton"
2 | import { DashboardHeader } from "@/components/dashboard/header"
3 | import { DashboardShell } from "@/components/dashboard/shell"
4 |
5 | export default function DashboardBillingLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/(project)/project/activity/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | import { authOptions } from "@/lib/auth";
4 | import { getCurrentUser } from "@/lib/session";
5 | import { DashboardHeader } from "@/components/dashboard/header";
6 | import { DashboardShell } from "@/components/dashboard/shell";
7 |
8 | import { ActivityLogTable } from "./activity-log";
9 |
10 | export const metadata = {
11 | title: "BuzzDaddy Overview",
12 | description: "Manage account and website settings.",
13 | };
14 |
15 | export default async function SettingsPage() {
16 | const user = await getCurrentUser();
17 |
18 | if (!user) {
19 | redirect(authOptions?.pages?.signIn || "/login");
20 | }
21 |
22 | return (
23 |
24 |
28 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/(project)/project/explorer/data/presets.ts:
--------------------------------------------------------------------------------
1 | export interface Preset {
2 | id: string
3 | name: string
4 | }
5 |
6 | export const presets: Preset[] = [
7 | {
8 | id: "9cb0e66a-9937-465d-a188-2c4c4ae2401f",
9 | name: "Twitter / X",
10 | },
11 | {
12 | id: "61eb0e32-2391-4cd3-adc3-66efe09bc0b7",
13 | name: "Reddit",
14 | },
15 | {
16 | id: "a4e1fa51-f4ce-4e45-892c-224030a00bdd",
17 | name: "Linkedin",
18 | },
19 | // {
20 | // id: "cc198b13-4933-43aa-977e-dcd95fa30770",
21 | // name: "Q&A",
22 | // },
23 | // {
24 | // id: "adfa95be-a575-45fd-a9ef-ea45386c64de",
25 | // name: "English to other languages",
26 | // },
27 | // {
28 | // id: "c569a06a-0bd6-43a7-adf9-bf68c09e7a79",
29 | // name: "Parse unstructured data",
30 | // },
31 | // {
32 | // id: "15ccc0d7-f37a-4f0a-8163-a37e162877dc",
33 | // name: "Classification",
34 | // },
35 | // {
36 | // id: "4641ef41-1c0f-421d-b4b2-70fe431081f3",
37 | // name: "Natural language to Python",
38 | // },
39 | // {
40 | // id: "48d34082-72f3-4a1b-a14d-f15aca4f57a0",
41 | // name: "Explain code",
42 | // },
43 | // {
44 | // id: "dfd42fd5-0394-4810-92c6-cc907d3bfd1a",
45 | // name: "Chat",
46 | // },
47 | ]
48 |
--------------------------------------------------------------------------------
/app/(project)/project/keywords/loading.tsx:
--------------------------------------------------------------------------------
1 | import { DashboardHeader } from "@/components/dashboard/header";
2 | import { DashboardShell } from "@/components/dashboard/shell";
3 | import { CardSkeleton } from "@/components/shared/card-skeleton";
4 |
5 | export default function DashboardBillingLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/app/(project)/project/keywords/page.tsx:
--------------------------------------------------------------------------------
1 | import { SearchParams } from "@/types";
2 | import { redirect } from "next/navigation";
3 |
4 | import { DashboardHeader } from "@/components/dashboard/header";
5 | import { KeywordSelectForm } from "@/components/dashboard/keyword-select-form";
6 | import { DashboardShell } from "@/components/dashboard/shell";
7 | import { authOptions } from "@/lib/auth";
8 | import { getCurrentUser } from "@/lib/session";
9 |
10 | export const metadata = {
11 | title: "BuzzDaddy Keywords Configuration",
12 | description: "Manage your keywords.",
13 | };
14 |
15 | interface KeywordPageProps {
16 | searchParams: SearchParams<"id">;
17 | }
18 |
19 | export default async function SettingsPage({ searchParams }: KeywordPageProps) {
20 | const user = await getCurrentUser();
21 |
22 | if (!user) {
23 | redirect(authOptions?.pages?.signIn || "/login");
24 | }
25 |
26 | return (
27 |
28 |
32 |
33 |
34 |
Keywords
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/(project)/project/layout.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 |
3 | import { dashboardConfig } from "@/config/dashboard";
4 | import { getCurrentUser } from "@/lib/session";
5 | import { DashboardNav, NestedNav } from "@/components/layout/nav";
6 | import { NavBar } from "@/components/layout/navbar";
7 | import { SiteFooter } from "@/components/layout/site-footer";
8 |
9 | interface DashboardLayoutProps {
10 | children?: React.ReactNode;
11 | }
12 |
13 | export default async function DashboardLayout({
14 | children,
15 | }: DashboardLayoutProps) {
16 | const user = await getCurrentUser();
17 |
18 | if (!user) {
19 | return notFound();
20 | }
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
30 |
31 | {children}
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/app/(project)/project/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/shared/card-skeleton"
2 | import { DashboardHeader } from "@/components/dashboard/header"
3 | import { DashboardShell } from "@/components/dashboard/shell"
4 | import { Button } from "@/components/ui/button"
5 |
6 | export default function DashboardLoading() {
7 | return (
8 |
9 |
10 | Add new campaign
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/(project)/project/playground/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import { redirect } from "next/navigation";
3 | import { SearchParams } from "@/types";
4 |
5 | import { getCurrentUser } from "@/lib/session";
6 | import { Card, CardContent } from "@/components/ui/card";
7 | import { DashboardHeader } from "@/components/dashboard/header";
8 | import { getCampaignById } from "@/app/actions/campaign";
9 |
10 | import { VoiceTonePersonalityForm } from "./components/form";
11 |
12 | export const metadata: Metadata = {
13 | title: "Playground",
14 | description: "Experiment real-time with keywords.",
15 | };
16 |
17 | export default async function PlaygroundPage({
18 | searchParams,
19 | }: {
20 | searchParams: SearchParams<"id">;
21 | }) {
22 | const user = await getCurrentUser();
23 | if (!user?.id) {
24 | redirect("/login");
25 | }
26 |
27 | const campaignId = searchParams.id as string;
28 | if (!campaignId) {
29 | redirect("/dashboard/campaigns");
30 | }
31 |
32 | const result = await getCampaignById(campaignId);
33 | if (!result.data) {
34 | redirect("/dashboard/campaigns");
35 | }
36 |
37 | return (
38 | <>
39 |
43 |
44 |
45 |
51 |
52 |
53 | >
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/app/(project)/project/preferences/loading.tsx:
--------------------------------------------------------------------------------
1 | import { DashboardHeader } from "@/components/dashboard/header";
2 | import { DashboardShell } from "@/components/dashboard/shell";
3 | import { CardSkeleton } from "@/components/shared/card-skeleton";
4 |
5 | export default function DashboardBillingLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/app/(project)/project/preferences/page.tsx:
--------------------------------------------------------------------------------
1 | import { SearchParams } from "@/types";
2 | import { redirect } from "next/navigation";
3 |
4 | import { getPostPreferencesByCampaign } from "@/app/actions/post-preferences";
5 | import { AddPostPreferencesModal } from "@/components/dashboard/add-post-preferences-moda";
6 | import { DashboardHeader } from "@/components/dashboard/header";
7 | import { PostPreferencesForm } from "@/components/dashboard/post-preferences-form";
8 | import { DashboardShell } from "@/components/dashboard/shell";
9 | import { authOptions } from "@/lib/auth";
10 | import { getCurrentUser } from "@/lib/session";
11 |
12 | export const metadata = {
13 | title: "BuzzDaddy Autopilot",
14 | description: "Manage post frequency, target platforms and interaction types.",
15 | };
16 |
17 | interface PreferencesPageProps {
18 | searchParams: SearchParams<"id">;
19 | }
20 |
21 | export default async function PreferencesPage({
22 | searchParams,
23 | }: PreferencesPageProps) {
24 | const user = await getCurrentUser();
25 |
26 | if (!user) {
27 | redirect(authOptions?.pages?.signIn || "/login");
28 | }
29 |
30 | const preferences = await getPostPreferencesByCampaign(
31 | searchParams.id as string,
32 | );
33 |
34 | if (!preferences.data) {
35 | return ;
36 | }
37 |
38 | return (
39 | <>
40 |
41 |
45 |
48 |
49 | >
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/app/(project)/project/settings/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/shared/card-skeleton"
2 | import { DashboardHeader } from "@/components/dashboard/header"
3 | import { DashboardShell } from "@/components/dashboard/shell"
4 |
5 | export default function DashboardSettingsLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/(project)/project/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation"
2 |
3 | import { authOptions } from "@/lib/auth"
4 | import { getCurrentUser } from "@/lib/session"
5 | import { DashboardHeader } from "@/components/dashboard/header"
6 | import { DashboardShell } from "@/components/dashboard/shell"
7 | import { UserNameForm } from "@/components/forms/user-name-form"
8 | import { InviteUserForm } from "@/components/forms/invite-user"
9 |
10 | export const metadata = {
11 | title: "Settings",
12 | description: "Manage account and website settings.",
13 | }
14 |
15 | export default async function SettingsPage() {
16 | const user = await getCurrentUser()
17 |
18 | if (!user) {
19 | redirect(authOptions?.pages?.signIn || "/login")
20 | }
21 |
22 | return (
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/app/actions/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ai";
2 | export * from "./apify";
3 | export * from "./campaign";
4 | export * from "./keywords";
5 | export * from "./notification";
6 | export * from "./post-preferences";
7 | export * from "./twitter";
8 |
--------------------------------------------------------------------------------
/app/actions/twitter.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { Message } from "@/types";
4 |
5 | import { client } from "@/lib/twitter";
6 |
7 | export const postComment = async (
8 | tweetId: string,
9 | comment: string,
10 | ): Promise => {
11 | console.log("Function postComment started");
12 | console.log(`Received tweetId: ${tweetId}, comment: ${comment}`);
13 |
14 | try {
15 | console.log("Initializing Twitter client");
16 | const c = client();
17 |
18 | console.log("Posting comment to Twitter");
19 | const postComment = await c.v2.reply(comment, tweetId);
20 |
21 | console.log("POSTING COMMENT RESPONSE: ", postComment);
22 |
23 | if (postComment.errors) {
24 | console.error("Errors in posting comment: ", postComment.errors);
25 | return {
26 | type: "error",
27 | message: "Errors in posting comment",
28 | };
29 | }
30 |
31 | console.log("Comment posted successfully, data: ", postComment.data);
32 |
33 | return {
34 | type: "success",
35 | message: "Comment posted successfully",
36 | };
37 | } catch (error) {
38 | if (error instanceof Error) {
39 | console.error("Error caught in postComment: ", error);
40 | return {
41 | type: "error",
42 | message: "Error caught in postComment",
43 | };
44 | } else {
45 | console.error("An unexpected error occurred in postComment");
46 | return {
47 | type: "error",
48 | message: "An unexpected error occurred in postComment",
49 | };
50 | }
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 |
3 | import { authOptions } from "@/lib/auth"
4 |
5 | const handler = NextAuth(authOptions)
6 |
7 | export { handler as GET, handler as POST }
8 |
--------------------------------------------------------------------------------
/app/api/openai/openai.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from "openai";
2 |
3 | const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
4 |
5 | export const callOpenAI = async (text: string): Promise => {
6 | const completion = await openai.chat.completions.create({
7 | messages: [
8 | {
9 | role: "system",
10 | content: `TASK: Craft a message as if you are traversing social media. You are an average American who happened to see this post and feel connected to it because you are a happy user of OUR STARTUP /PRODUCT NAME HERE
11 | INSTRUCTIONS: Upsell our startup / product organically, it should sound natural not an ad. Weave it in with a joke or review or state your connection, however it connects naturally to the subject matter of the post. No more than 3 sentences. Use placeholder "StartupNameHere" to replace with our startup / product name.`,
12 | },
13 | { role: "user", content: text },
14 | ],
15 | model: "gpt-4o",
16 | });
17 |
18 | const messageContent = completion.choices[0]?.message.content ?? "";
19 | console.log(messageContent);
20 | return messageContent;
21 | };
--------------------------------------------------------------------------------
/app/api/openai/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { callOpenAI } from "./openai";
3 |
4 | export const runtime="edge";
5 |
6 | export async function POST(req: NextRequest) {
7 | try {
8 | const body = await req.json();
9 | const completion = await callOpenAI(body.text);
10 | return NextResponse.json({ result: completion }, { status: 200 });
11 | } catch (error: unknown) {
12 | const message =
13 | error instanceof Error ? error.message : "Unknown error occurred";
14 | return NextResponse.json({ error: message }, { status: 400 });
15 | }
16 | }
--------------------------------------------------------------------------------
/app/opengraph-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/app/opengraph-image.jpg
--------------------------------------------------------------------------------
/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from "next"
2 |
3 | export default function robots(): MetadataRoute.Robots {
4 | return {
5 | rules: {
6 | userAgent: "*",
7 | allow: "/",
8 | },
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/assets/fonts/CalSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/assets/fonts/CalSans-SemiBold.ttf
--------------------------------------------------------------------------------
/assets/fonts/CalSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/assets/fonts/CalSans-SemiBold.woff2
--------------------------------------------------------------------------------
/assets/fonts/Inter-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/assets/fonts/Inter-Bold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Inter-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/assets/fonts/Inter-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/index.ts:
--------------------------------------------------------------------------------
1 | import localFont from "next/font/local";
2 | import { Inter as FontSans, Urbanist } from "next/font/google";
3 |
4 | export const fontSans = FontSans({
5 | subsets: ["latin"],
6 | variable: "--font-sans",
7 | })
8 |
9 | export const fontUrban = Urbanist({
10 | subsets: ["latin"],
11 | variable: "--font-urban",
12 | })
13 |
14 | export const fontHeading = localFont({
15 | src: "./CalSans-SemiBold.woff2",
16 | variable: "--font-heading",
17 | })
18 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "styles/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/analytics.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Analytics as VercelAnalytics } from "@vercel/analytics/react"
4 |
5 | export function Analytics() {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/components/billing-info.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 |
5 | import { buttonVariants } from "@/components/ui/button"
6 | import {
7 | Card,
8 | CardContent,
9 | CardDescription,
10 | CardFooter,
11 | CardHeader,
12 | CardTitle,
13 | } from "@/components/ui/card"
14 | import { cn, formatDate } from "@/lib/utils"
15 | import Link from "next/link"
16 | import { UserSubscriptionPlan } from "types"
17 |
18 | interface BillingInfoProps extends React.HTMLAttributes {
19 | subscriptionPlan: UserSubscriptionPlan;
20 | }
21 |
22 | export function BillingInfo({
23 | subscriptionPlan
24 | }: BillingInfoProps) {
25 |
26 | return (
27 |
28 |
29 | Subscription Plan
30 |
31 | You are currently on the {subscriptionPlan.title} {" "}
32 | plan.
33 |
34 |
35 | {subscriptionPlan.description}
36 |
37 |
41 | {subscriptionPlan.isPaid ? "Manage Subscription" : "Upgrade now"}
42 |
43 |
44 | {subscriptionPlan.isPaid ? (
45 |
46 | {subscriptionPlan.isCanceled
47 | ? "Your plan will be canceled on "
48 | : "Your plan renews on "}
49 | {formatDate(subscriptionPlan.stripeCurrentPeriodEnd)}.
50 |
51 | ) : null}
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/components/browser.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/utils/cn'
2 |
3 | export const BrowserComponent: React.FC<{ children?: React.ReactNode; className?: string }> = ({
4 | className,
5 | children
6 | }) => (
7 |
13 |
18 |
23 |
28 |
39 |
40 |
41 |
42 |
43 | https://buzzdaddy.ai
44 |
45 |
46 |
47 |
48 |
{children}
49 |
50 | )
51 |
--------------------------------------------------------------------------------
/components/connect-linkedin-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { LinkIcon } from "lucide-react";
5 | import { toast } from "sonner";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import { connectLinkedInAccount } from "@/app/(dashboard)/dashboard/actions";
9 |
10 | export function ConnectLinkedInButton() {
11 | const router = useRouter();
12 |
13 | const onSubmit = async () => {
14 | const result = await connectLinkedInAccount();
15 | if (result.type === "error") {
16 | toast.error(result.message);
17 | }
18 | // else {
19 | // toast.success(result.message);
20 | // router.push(result.data);
21 | // }
22 |
23 | if (result.data) {
24 | toast.success(result.message);
25 | if (result.data) {
26 | router.push(result.data);
27 | } else {
28 | toast.error("Unexpected error: No URL returned.");
29 | }
30 | }
31 | };
32 |
33 | return (
34 |
40 |
41 | Connect LinkedIn
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/components/connect-linkedin-card.tsx:
--------------------------------------------------------------------------------
1 | import { LinkedinIcon } from "lucide-react";
2 |
3 | import { ConnectLinkedInButton } from "@/components/connect-linkedin-button";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 |
13 | export const ConnectLinkedInCard = () => {
14 |
15 |
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Connect your LinkedIn account to share your posts.
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/components/connect-twitter-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { LinkIcon } from "lucide-react";
5 | import { toast } from "sonner";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import { connectTwitterAccount } from "@/app/(dashboard)/dashboard/actions";
9 |
10 | export function ConnectTwitterButton() {
11 | const router = useRouter();
12 | const onSubmit = async () => {
13 | const result = await connectTwitterAccount();
14 | if (result.type === "error") {
15 | toast.error(result.message);
16 | }
17 | // else {
18 | // toast.success(result.message);
19 | // router.push(result.data);
20 | // }
21 | if (result.data) {
22 | toast.success(result.message);
23 | router.push(result.data);
24 | }
25 | };
26 |
27 | return (
28 | <>
29 |
35 |
36 | Connect X
37 |
38 | >
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/components/connect-twitter-card.tsx:
--------------------------------------------------------------------------------
1 | import { FaXTwitter } from "react-icons/fa6";
2 |
3 | import { ConnectTwitterButton } from "@/components/connect-twitter-button";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 |
13 | export const ConnectTwitterCard = () => {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Connect your X account to share your posts.
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/components/content/mdx-card.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | interface CardProps extends React.HTMLAttributes {
6 | href?: string
7 | disabled?: boolean
8 | }
9 |
10 | export function MdxCard({
11 | href,
12 | className,
13 | children,
14 | disabled,
15 | ...props
16 | }: CardProps) {
17 | return (
18 |
26 |
27 |
28 | {children}
29 |
30 |
31 | {href && (
32 |
33 |
View
34 |
35 | )}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/components/dashboard/activity-log/columns.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ActivityLog } from "@prisma/client";
4 | import { ColumnDef } from "@tanstack/react-table";
5 | import { ArrowUpDown } from "lucide-react";
6 |
7 | import { Button } from "@/components/ui/button";
8 |
9 | export const columns: ColumnDef[] = [
10 | {
11 | accessorKey: "type",
12 | header: ({ column }) => {
13 | return (
14 | column.toggleSorting(column.getIsSorted() === "asc")}
17 | >
18 | Type
19 |
20 |
21 | );
22 | },
23 | },
24 | {
25 | accessorKey: "message",
26 | header: "Message",
27 | },
28 | {
29 | accessorKey: "createdAt",
30 | header: ({ column }) => {
31 | return (
32 |
33 |
column.toggleSorting(column.getIsSorted() === "asc")}
36 | >
37 | DateTime
38 |
39 |
40 |
41 | );
42 | },
43 |
44 | cell: ({ row }) => {
45 | const date = row.original.createdAt;
46 |
47 | const formatDate = (date: string | Date): string => {
48 | const parsedDate = new Date(date);
49 | return parsedDate.toLocaleString(); // Adjust the format as needed
50 | };
51 | const formattedDate = formatDate(date);
52 |
53 | return {formattedDate}
;
54 | },
55 | },
56 | ];
57 |
--------------------------------------------------------------------------------
/components/dashboard/activity-log/index.tsx:
--------------------------------------------------------------------------------
1 | import { ActivityLog as TActivityLog } from "@prisma/client";
2 |
3 | import { getActivityLog } from "@/app/actions/activity-log";
4 |
5 | import { columns } from "./columns";
6 | import { DataTable } from "./data-table";
7 |
8 | export const ActivityLog = async () => {
9 | const result = await getActivityLog();
10 |
11 | let data: TActivityLog[] = [];
12 |
13 | if (result.type === "error" || !result.data) {
14 | data = [];
15 | } else if (result.data) {
16 | data = result.data;
17 | }
18 |
19 | return (
20 |
21 | {/*
22 | Activity Log
23 | */}
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/components/dashboard/add-keywords-modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useKeywordsModal } from "@/hooks/use-keywords-modal";
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogDescription,
8 | DialogHeader,
9 | DialogTitle,
10 | } from "@/components/ui/dialog";
11 |
12 | import { KeywordSelectForm } from "./keyword-select-form";
13 |
14 | export function AddKeywordsModal() {
15 | const { isOpen, onChange, campaignId, onClose } = useKeywordsModal();
16 |
17 | return (
18 |
19 |
20 |
21 | Keyword Preferences Configuration
22 |
23 | Add keywords for your campaign to target specific audiences and
24 | finding relevant posts and comments.
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/components/dashboard/empty-campaign-card.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from "@/components/ui/card";
2 |
3 | interface CampaignProps {
4 | children: React.ReactNode;
5 | }
6 |
7 | export function EmptyCampaignCard({ children }: CampaignProps) {
8 | return (
9 |
10 |
11 |
12 | {children}
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/components/dashboard/header.tsx:
--------------------------------------------------------------------------------
1 | interface DashboardHeaderProps {
2 | heading: string
3 | text?: string
4 | children?: React.ReactNode
5 | }
6 |
7 | export function DashboardHeader({
8 | heading,
9 | text,
10 | children,
11 | }: DashboardHeaderProps) {
12 | return (
13 |
14 |
15 |
{heading}
16 | {text &&
{text}
}
17 |
18 | {children}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/components/dashboard/shell.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | interface DashboardShellProps extends React.HTMLAttributes {}
6 |
7 | export function DashboardShell({
8 | children,
9 | className,
10 | ...props
11 | }: DashboardShellProps) {
12 | return (
13 |
14 | {children}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/components/disconnect-linkedin-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { UnlinkIcon } from "lucide-react";
5 | import { useSession } from "next-auth/react";
6 | import { toast } from "sonner";
7 |
8 | import { Button } from "@/components/ui/button";
9 | import { deleteLinkedin } from "@/app/(dashboard)/dashboard/actions";
10 |
11 | export const DisconnectLinkedInAccount = () => {
12 | const { data: session, status } = useSession();
13 | const router = useRouter();
14 |
15 | const handleClick = async () => {
16 | try {
17 | if (status === "unauthenticated" || !session?.user) {
18 | toast.error("You are not logged in");
19 | return;
20 | }
21 | const result = await deleteLinkedin(session.user.id);
22 | console.log(result);
23 | if (result.status === "success") {
24 | toast.success(result.message);
25 | router.refresh();
26 | } else if (result.status === "error") {
27 | toast.error(result.message);
28 | }
29 | } catch (error) {
30 | console.error("Error disconnecting LinkedIn account:", error);
31 | toast.error("Failed to disconnect LinkedIn account");
32 | }
33 | };
34 |
35 | return (
36 |
40 |
41 | Disconnect
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/components/docs/page-header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | interface DocsPageHeaderProps extends React.HTMLAttributes {
4 | heading: string
5 | text?: string
6 | }
7 |
8 | export function DocsPageHeader({
9 | heading,
10 | text,
11 | className,
12 | ...props
13 | }: DocsPageHeaderProps) {
14 | return (
15 | <>
16 |
17 |
18 | {heading}
19 |
20 | {text &&
{text}
}
21 |
22 |
23 | >
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/components/docs/search.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { Input } from "@/components/ui/input"
7 | import { toast } from "@/components/ui/use-toast"
8 |
9 | interface DocsSearchProps extends React.HTMLAttributes {}
10 |
11 | export function DocsSearch({ className, ...props }: DocsSearchProps) {
12 | function onSubmit(event: React.SyntheticEvent) {
13 | event.preventDefault()
14 |
15 | return toast({
16 | title: "Not implemented",
17 | description: "We're still working on the search.",
18 | })
19 | }
20 |
21 | return (
22 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/components/docs/sidebar-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { usePathname } from "next/navigation"
5 |
6 | import { SidebarNavItem } from "types"
7 | import { cn } from "@/lib/utils"
8 |
9 | export interface DocsSidebarNavProps {
10 | items: SidebarNavItem[]
11 | }
12 |
13 | export function DocsSidebarNav({ items }: DocsSidebarNavProps) {
14 | const pathname = usePathname()
15 |
16 | return items.length ? (
17 |
18 | {items.map((item) => (
19 |
20 |
21 | {item.title}
22 |
23 | {item.items ? (
24 |
25 | ) : null}
26 |
27 | ))}
28 |
29 | ) : null
30 | }
31 |
32 | interface DocsSidebarNavItemsProps {
33 | items: SidebarNavItem[]
34 | pathname: string | null
35 | }
36 |
37 | export function DocsSidebarNavItems({
38 | items,
39 | pathname,
40 | }: DocsSidebarNavItemsProps) {
41 | return items?.length ? (
42 |
43 | {items.map((item, index) =>
44 | !item.disabled && item.href ? (
45 |
57 | {item.title}
58 |
59 | ) : (
60 |
64 | {item.title}
65 |
66 | )
67 | )}
68 |
69 | ) : null
70 | }
71 |
--------------------------------------------------------------------------------
/components/forms/billing-form-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { generateUserStripe } from '@/actions/generate-user-stripe'
4 | import { Icons } from "@/components/shared/icons"
5 | import { Button } from "@/components/ui/button"
6 | import { SubscriptionPlan, UserSubscriptionPlan } from "@/types"
7 | import { useTransition } from 'react'
8 |
9 | interface BillingFormButtonProps {
10 | offer: SubscriptionPlan;
11 | subscriptionPlan: UserSubscriptionPlan;
12 | year: boolean;
13 | }
14 |
15 | export function BillingFormButton({ year, offer, subscriptionPlan }: BillingFormButtonProps) {
16 | let [isPending, startTransition] = useTransition();
17 | const generateUserStripeSession = generateUserStripe.bind(
18 | null,
19 | offer.stripeIds[year ? "yearly" : "monthly"]
20 | );
21 |
22 | const stripeSessionAction = () => startTransition(async () => await generateUserStripeSession());
23 |
24 | return (
25 |
31 | {isPending ? (
32 | <>
33 | Loading...
34 | >
35 | ) : (
36 | <>
37 | {subscriptionPlan.stripePriceId === offer.stripeIds[year ? "yearly" : "monthly"]
38 | ? "Manage Subscription"
39 | : "Upgrade"}
40 | >
41 | )}
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/components/gif-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { cn } from "@/lib/utils";
3 | import { Badge } from "@/components/ui/badge";
4 |
5 | export interface GifCardProps {
6 | url: string;
7 | title: string;
8 | description: string;
9 | }
10 |
11 | export function GifCard({ url, title, description }: GifCardProps) {
12 | return (
13 |
14 |
25 |
26 |
27 | {description}
28 |
29 |
30 | {title}
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | export const GifGrid = ({ gifs }) => (
39 |
40 |
41 | {gifs.map((gif, index) => (
42 |
43 | ))}
44 |
45 |
46 | );
--------------------------------------------------------------------------------
/components/layout/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import Link from "next/link"
3 |
4 | import { MainNavItem } from "types"
5 | import { siteConfig } from "@/config/site"
6 | import { cn } from "@/lib/utils"
7 | import { useLockBody } from "@/hooks/use-lock-body"
8 | import { Icons } from "@/components/shared/icons"
9 |
10 | interface MobileNavProps {
11 | items: MainNavItem[]
12 | children?: React.ReactNode
13 | }
14 |
15 | export function MobileNav({ items, children }: MobileNavProps) {
16 | useLockBody()
17 |
18 | return (
19 |
24 |
25 |
26 |
27 | {siteConfig.name}
28 |
29 |
30 | {items.map((item, index) => (
31 |
39 | {item.title}
40 |
41 | ))}
42 |
43 | {children}
44 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/components/layout/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { useTheme } from "next-themes"
5 |
6 | import { Button } from "@/components/ui/button"
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu"
13 | import { Icons } from "@/components/shared/icons"
14 |
15 | export function ModeToggle() {
16 | const { setTheme } = useTheme()
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | Toggle theme
25 |
26 |
27 |
28 | setTheme("light")}>
29 |
30 | Light
31 |
32 | setTheme("dark")}>
33 |
34 | Dark
35 |
36 | setTheme("system")}>
37 |
38 | System
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/components/layout/navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import useScroll from "@/hooks/use-scroll";
4 | import { MainNavItem } from "@/types";
5 | import { User } from "next-auth";
6 | import { MainNav } from "./main-nav";
7 | import { UserAccountNav } from "./user-account-nav";
8 | import { Button, buttonVariants } from "@/components/ui/button";
9 | import Link from "next/link";
10 | import { cn } from "@/lib/utils";
11 | import { useSigninModal } from "@/hooks/use-signin-modal";
12 |
13 |
14 | interface NavBarProps {
15 | user: Pick | undefined
16 | items?: MainNavItem[]
17 | children?: React.ReactNode
18 | rightElements?: React.ReactNode
19 | scroll?: boolean
20 | }
21 |
22 | export function NavBar({ user, items, children, rightElements, scroll = false }: NavBarProps) {
23 | const scrolled = useScroll(50);
24 | const signInModal = useSigninModal();
25 |
26 | return (
27 |
58 | );
59 | }
--------------------------------------------------------------------------------
/components/layout/preview-select.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import {
3 | DropdownMenu,
4 | DropdownMenuContent,
5 | DropdownMenuGroup,
6 | DropdownMenuItem,
7 | DropdownMenuLabel,
8 | DropdownMenuPortal,
9 | DropdownMenuSeparator,
10 | DropdownMenuShortcut,
11 | DropdownMenuSub,
12 | DropdownMenuSubContent,
13 | DropdownMenuSubTrigger,
14 | DropdownMenuTrigger,
15 | } from "@/components/ui/dropdown-menu"
16 |
17 | export function PreviewDropdown() {
18 | return (
19 |
20 |
21 | Preview
22 |
23 |
24 | Project links
25 |
26 |
27 |
28 | qashboard.com
29 | ↗
30 |
31 |
32 | https://turbo-computing-machine-pgp4rxpr669hrw-3000.app.github.dev/
33 | ↗
34 |
35 |
36 |
37 |
38 | )
39 | }
--------------------------------------------------------------------------------
/components/layout/select-worktype.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import * as React from "react"
3 | import { CaretSortIcon } from "@radix-ui/react-icons"
4 | import { ShieldAlertIcon, BellIcon } from "lucide-react";
5 | import { Button } from "@/components/ui/button"
6 | import {
7 | Select,
8 | SelectContent,
9 | SelectGroup,
10 | SelectItem,
11 | SelectLabel,
12 | SelectTrigger,
13 | SelectValue,
14 | } from "@/components/ui/select"
15 |
16 | export function CreateDropdownMenu() {
17 | const [isOpen, setIsOpen] = React.useState(true)
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 | Create
27 | Ticket
28 | using Chat
29 | using Template
30 | Import
31 | From Notion
32 | Github Issues
33 |
34 |
35 |
36 | )
37 | }
--------------------------------------------------------------------------------
/components/layout/site-footer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { Feedback } from "@/components/feedback";
4 | import { cn } from "@/lib/utils"
5 | import { Icons } from "@/components/shared/icons"
6 | import { ModeToggle } from "@/components/layout/mode-toggle"
7 |
8 | export function SiteFooter({ className }: React.HTMLAttributes) {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | All Rights Reserved{" "}
16 |
22 | BuzzDaddy
23 |
24 | {" "}| Copyright 2024
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/components/layout/sticky-scroll.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { StickyScroll } from "../ui/sticky-scroll-reveal";
4 |
5 | const content = [
6 | {
7 | title: "Collaborative Editing",
8 | description:
9 | "Work together in real time with your team, clients, and stakeholders. Collaborate on documents, share ideas, and make decisions quickly. With our platform, you can streamline your workflow and increase productivity.",
10 | },
11 | {
12 | title: "Real time changes",
13 | description:
14 | "See changes as they happen. With our platform, you can track every modification in real time. No more confusion about the latest version of your project. Say goodbye to the chaos of version control and embrace the simplicity of real-time updates.",
15 | },
16 | {
17 | title: "Version control",
18 | description:
19 | "Experience real-time updates and never stress about version control again. Our platform ensures that you're always working on the most recent version of your project, eliminating the need for constant manual updates. Stay in the loop, keep your team aligned, and maintain the flow of your work without any interruptions.",
20 | },
21 | {
22 | title: "Running out of content",
23 | description:
24 | "Experience real-time updates and never stress about version control again. Our platform ensures that you're always working on the most recent version of your project, eliminating the need for constant manual updates. Stay in the loop, keep your team aligned, and maintain the flow of your work without any interruptions.",
25 | },
26 | ];
27 | export function StickyScrollRevealDemo() {
28 | return (
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/components/modal-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SignInModal } from "@/components/layout/sign-in-modal";
4 | import { useMounted } from "@/hooks/use-mounted";
5 |
6 | export const ModalProvider = () => {
7 | const mounted = useMounted()
8 |
9 | if (!mounted) {
10 | return null;
11 | }
12 |
13 | return (
14 | <>
15 |
16 | {/* add your own modals here... */}
17 | >
18 | );
19 | };
--------------------------------------------------------------------------------
/components/providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import { ThemeProviderProps } from "next-themes/dist/types";
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children} ;
9 | }
10 |
11 | export function SessionProvider({ children }: { children: React.ReactNode }) {
12 | return {children} ;
13 | }
14 |
--------------------------------------------------------------------------------
/components/query-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4 |
5 | const client = new QueryClient();
6 |
7 | export function QueryProvider({ children }: { children: React.ReactNode }) {
8 | return {children} ;
9 | }
10 |
--------------------------------------------------------------------------------
/components/radar/IconContainer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 |
4 | import { twMerge } from "tailwind-merge";
5 | import { HiDocumentText } from "react-icons/hi";
6 | import { motion } from "framer-motion";
7 |
8 | export const IconContainer = ({ icon, text, delay }: any) => {
9 | return (
10 |
27 |
28 | {icon || }
29 |
30 |
31 |
32 | {text || `Web Development`}
33 |
34 |
35 |
36 | );
37 | };
--------------------------------------------------------------------------------
/components/radar/Preview.tsx:
--------------------------------------------------------------------------------
1 | import { IconContainer } from "./IconContainer";
2 | import { FaTwitter, FaLinkedin, FaReddit } from "react-icons/fa";
3 | import { Radar } from "./Radar";
4 |
5 | export const PreviewRadar = () => {
6 | return (
7 |
8 |
9 |
10 | }
14 | />
15 |
16 |
17 |
18 |
19 | }
23 | />
24 | }
28 | />
29 |
30 |
31 |
32 |
33 | }
37 | />
38 | }
42 | />
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default PreviewRadar;
53 |
--------------------------------------------------------------------------------
/components/radar/Radar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { twMerge } from "tailwind-merge";
4 | import { motion } from "framer-motion";
5 |
6 | export const Radar = ({ className }: any) => {
7 | const circles = new Array(8).fill(1);
8 | return (
9 |
15 |
22 | {/* Radar line that rotates */}
23 |
24 |
25 | {/* concentric circles */}
26 | {circles.map((circle, idx) => (
27 |
36 | ))}
37 |
38 | );
39 | };
40 |
41 | {/* Creating circles */}
42 | export const Circle = ({ className, children, idx, ...rest }: any) => {
43 | return (
44 |
61 | );
62 | };
63 |
64 |
--------------------------------------------------------------------------------
/components/shared/callout.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | interface CalloutProps {
4 | icon?: string
5 | children?: React.ReactNode
6 | type?: "default" | "warning" | "danger" | "info"
7 | }
8 |
9 | // ✅💡⚠️🚫🚨
10 | export function Callout({
11 | children,
12 | icon,
13 | type = "default",
14 | ...props
15 | }: CalloutProps) {
16 | return (
17 |
28 | {icon &&
{icon} }
29 |
{children}
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/components/shared/card-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
2 | import { Skeleton } from "@/components/ui/skeleton"
3 |
4 | export function CardSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/components/shared/header-section.tsx:
--------------------------------------------------------------------------------
1 | interface HeaderSectionProps {
2 | label?: string;
3 | title: string;
4 | subtitle?: string;
5 | }
6 |
7 | export function HeaderSection({ label, title, subtitle }: HeaderSectionProps) {
8 | return (
9 |
10 | {label ? (
11 |
12 | {label}
13 |
14 | ) : null}
15 |
16 | {title}
17 |
18 | {subtitle ? (
19 |
20 | {subtitle}
21 |
22 | ) : null}
23 |
24 | );
25 | }
--------------------------------------------------------------------------------
/components/shared/modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import useMediaQuery from "@/hooks/use-media-query";
4 | import { cn } from "@/lib/utils";
5 | import { Drawer } from "vaul";
6 |
7 | import {
8 | Dialog,
9 | DialogContent,
10 | } from "@/components/ui/dialog";
11 |
12 | interface ModalProps {
13 | children: React.ReactNode;
14 | className?: string;
15 | showModal: boolean;
16 | setShowModal: () => void;
17 | }
18 |
19 | export function Modal({children, className, showModal, setShowModal}: ModalProps) {
20 | const { isMobile } = useMediaQuery();
21 |
22 | if (isMobile) {
23 | return (
24 |
25 |
26 |
27 |
33 |
36 | {children}
37 |
38 |
39 |
40 |
41 | );
42 | }
43 | return (
44 |
45 |
46 | {children}
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/components/shared/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | import { User } from "@prisma/client"
2 | import { AvatarProps } from "@radix-ui/react-avatar"
3 |
4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
5 | import { Icons } from "@/components/shared/icons"
6 |
7 | interface UserAvatarProps extends AvatarProps {
8 | user: Pick
9 | }
10 |
11 | export function UserAvatar({ user, ...props }: UserAvatarProps) {
12 | return (
13 |
14 | {user.image ? (
15 |
16 | ) : (
17 |
18 | {user.name}
19 |
20 |
21 | )}
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/components/tailwind-indicator.tsx:
--------------------------------------------------------------------------------
1 | export function TailwindIndicator() {
2 | if (process.env.NODE_ENV === "production") return null
3 |
4 | return (
5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/components/ugc-text.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { motion } from "framer-motion";
3 | import { HeroHighlight, Highlight } from "./ui/hero-highlight";
4 | import SparklesText from "./magicui/sparkles-text";
5 |
6 | export function UGCText() {
7 | return (
8 |
9 |
24 |
25 |
26 | {" "}
27 | generate{" "}
28 |
29 | 10x more leads
30 | than traditional advertisements.
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
55 | {children}
56 |
57 | ))
58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
59 |
60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
61 |
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { VariantProps, cva } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { VariantProps, cva } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
13 | secondary:
14 | "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
15 | destructive:
16 | "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/bento-grid.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utils/cn";
2 |
3 | export const BentoGrid = ({
4 | className,
5 | children,
6 | }: {
7 | className?: string;
8 | children?: React.ReactNode;
9 | }) => {
10 | return (
11 |
17 | {children}
18 |
19 | );
20 | };
21 |
22 | export const BentoGridItem = ({
23 | className,
24 | title,
25 | description,
26 | header,
27 | icon,
28 | }: {
29 | className?: string;
30 | title?: string | React.ReactNode;
31 | description?: string | React.ReactNode;
32 | header?: React.ReactNode;
33 | icon?: React.ReactNode;
34 | }) => {
35 | return (
36 |
42 | {header}
43 |
44 | {icon}
45 |
46 | {title}
47 |
48 |
49 | {description}
50 |
51 |
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const buttonVariants = cva(
7 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
12 | destructive:
13 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14 | outline:
15 | "border border-input hover:bg-accent hover:text-accent-foreground",
16 | secondary:
17 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18 | ghost: "hover:bg-accent hover:text-accent-foreground",
19 | link: "underline-offset-4 hover:underline text-primary",
20 | },
21 | size: {
22 | default: "h-10 py-2 px-4",
23 | sm: "h-9 px-3 rounded-md",
24 | lg: "h-11 px-8 rounded-md",
25 | icon: "h-10 w-10 rounded-md",
26 | },
27 | },
28 | defaultVariants: {
29 | variant: "default",
30 | size: "default",
31 | },
32 | },
33 | );
34 |
35 | export interface ButtonProps
36 | extends React.ButtonHTMLAttributes,
37 | VariantProps {}
38 |
39 | const Button = React.forwardRef(
40 | ({ className, variant, size, ...props }, ref) => {
41 | return (
42 |
47 | );
48 | },
49 | );
50 | Button.displayName = "Button";
51 |
52 | export { Button, buttonVariants };
53 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent }
30 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { VariantProps, cva } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { GripVertical } from "lucide-react"
4 | import * as ResizablePrimitive from "react-resizable-panels"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | )
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean
29 | }) => (
30 | div]:rotate-90",
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | )
44 |
45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
46 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/components/ui/stepper/context.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import type { StepperProps } from "./types";
4 |
5 | interface StepperContextValue extends StepperProps {
6 | clickable?: boolean;
7 | isError?: boolean;
8 | isLoading?: boolean;
9 | isVertical?: boolean;
10 | stepCount?: number;
11 | expandVerticalSteps?: boolean;
12 | activeStep: number;
13 | initialStep: number;
14 | }
15 |
16 | type StepperContextProviderProps = {
17 | value: Omit;
18 | children: React.ReactNode;
19 | };
20 |
21 | const StepperContext = React.createContext<
22 | StepperContextValue & {
23 | nextStep: () => void;
24 | prevStep: () => void;
25 | resetSteps: () => void;
26 | setStep: (step: number) => void;
27 | }
28 | >({
29 | steps: [],
30 | activeStep: 0,
31 | initialStep: 0,
32 | nextStep: () => {},
33 | prevStep: () => {},
34 | resetSteps: () => {},
35 | setStep: () => {},
36 | });
37 |
38 | const StepperProvider = ({ value, children }: StepperContextProviderProps) => {
39 | const isError = value.state === "error";
40 | const isLoading = value.state === "loading";
41 |
42 | const [activeStep, setActiveStep] = React.useState(value.initialStep);
43 |
44 | const nextStep = () => {
45 | setActiveStep((prev) => prev + 1);
46 | };
47 |
48 | const prevStep = () => {
49 | setActiveStep((prev) => prev - 1);
50 | };
51 |
52 | const resetSteps = () => {
53 | setActiveStep(value.initialStep);
54 | };
55 |
56 | const setStep = (step: number) => {
57 | setActiveStep(step);
58 | };
59 |
60 | return (
61 |
73 | {children}
74 |
75 | );
76 | };
77 |
78 | export { StepperContext, StepperProvider };
79 |
--------------------------------------------------------------------------------
/components/ui/stepper/step-button-container.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { Button } from "@/components/ui/button";
5 |
6 | import type { StepSharedProps } from "./types";
7 | import { useStepper } from "./use-stepper";
8 |
9 | type StepButtonContainerProps = StepSharedProps & {
10 | children?: React.ReactNode;
11 | };
12 |
13 | const StepButtonContainer = ({
14 | isCurrentStep,
15 | isCompletedStep,
16 | children,
17 | isError,
18 | isLoading: isLoadingProp,
19 | onClickStep,
20 | }: StepButtonContainerProps) => {
21 | const {
22 | clickable,
23 | isLoading: isLoadingContext,
24 | variant,
25 | styles,
26 | } = useStepper();
27 |
28 | const currentStepClickable = clickable || !!onClickStep;
29 |
30 | const isLoading = isLoadingProp || isLoadingContext;
31 |
32 | if (variant === "line") {
33 | return null;
34 | }
35 |
36 | return (
37 |
59 | {children}
60 |
61 | );
62 | };
63 |
64 | export { StepButtonContainer };
65 |
--------------------------------------------------------------------------------
/components/ui/stepper/step.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { HorizontalStep } from "./horizontal-step";
4 | import type { StepProps } from "./types";
5 | import { useStepper } from "./use-stepper";
6 | import { VerticalStep } from "./vertical-step";
7 |
8 | // Props which shouldn't be passed to to the Step component from the user
9 | interface StepInternalConfig {
10 | index: number;
11 | isCompletedStep?: boolean;
12 | isCurrentStep?: boolean;
13 | isLastStep?: boolean;
14 | }
15 |
16 | interface FullStepProps extends StepProps, StepInternalConfig {}
17 |
18 | const Step = React.forwardRef(
19 | (props, ref: React.Ref) => {
20 | const {
21 | children,
22 | description,
23 | icon,
24 | state,
25 | checkIcon,
26 | errorIcon,
27 | index,
28 | isCompletedStep,
29 | isCurrentStep,
30 | isLastStep,
31 | isKeepError,
32 | label,
33 | onClickStep,
34 | } = props as FullStepProps;
35 |
36 | const { isVertical, isError, isLoading, clickable } = useStepper();
37 |
38 | const hasVisited = isCurrentStep || isCompletedStep;
39 |
40 | const sharedProps = {
41 | isLastStep,
42 | isCompletedStep,
43 | isCurrentStep,
44 | index,
45 | isError,
46 | isLoading,
47 | clickable,
48 | label,
49 | description,
50 | hasVisited,
51 | icon,
52 | isKeepError,
53 | checkIcon,
54 | state,
55 | errorIcon,
56 | onClickStep,
57 | };
58 |
59 | const renderStep = () => {
60 | switch (isVertical) {
61 | case true:
62 | return (
63 |
64 | {children}
65 |
66 | );
67 | default:
68 | return ;
69 | }
70 | };
71 |
72 | return renderStep();
73 | },
74 | );
75 |
76 | Step.displayName = "Step";
77 |
78 | export { Step };
79 |
--------------------------------------------------------------------------------
/components/ui/stepper/use-media-query.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function useMediaQuery(query: string) {
4 | const [value, setValue] = React.useState(false);
5 |
6 | React.useEffect(() => {
7 | function onChange(event: MediaQueryListEvent) {
8 | setValue(event.matches);
9 | }
10 |
11 | const result = matchMedia(query);
12 | result.addEventListener("change", onChange);
13 | setValue(result.matches);
14 |
15 | return () => result.removeEventListener("change", onChange);
16 | }, [query]);
17 |
18 | return value;
19 | }
20 |
--------------------------------------------------------------------------------
/components/ui/stepper/use-stepper.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { StepperContext } from "./context";
4 |
5 | function usePrevious(value: T): T | undefined {
6 | const ref = React.useRef();
7 |
8 | React.useEffect(() => {
9 | ref.current = value;
10 | }, [value]);
11 |
12 | return ref.current;
13 | }
14 |
15 | export function useStepper() {
16 | const context = React.useContext(StepperContext);
17 |
18 | if (context === undefined) {
19 | throw new Error("useStepper must be used within a StepperProvider");
20 | }
21 |
22 | const { children, className, ...rest } = context;
23 |
24 | const isLastStep = context.activeStep === context.steps.length - 1;
25 | const hasCompletedAllSteps = context.activeStep === context.steps.length;
26 |
27 | const previousActiveStep = usePrevious(context.activeStep);
28 |
29 | const currentStep = context.steps[context.activeStep];
30 | const isOptionalStep = !!currentStep?.optional;
31 |
32 | const isDisabledStep = context.activeStep === 0;
33 |
34 | return {
35 | ...rest,
36 | isLastStep,
37 | hasCompletedAllSteps,
38 | isOptionalStep,
39 | isDisabledStep,
40 | currentStep,
41 | previousActiveStep,
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsContent, TabsList, TabsTrigger };
56 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { VariantProps, cva } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background hover:bg-muted hover:text-muted-foreground",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "bg-transparent border border-input hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-10 px-3",
20 | sm: "h-9 px-2.5",
21 | lg: "h-11 px-5",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/config/marketing.ts:
--------------------------------------------------------------------------------
1 | import { MarketingConfig } from "types"
2 |
3 | export const marketingConfig: MarketingConfig = {
4 | mainNav: [
5 | {
6 | title: "Pricing",
7 | href: "/pricing",
8 | },
9 | {
10 | title: "Blog",
11 | href: "/blog",
12 | },
13 | {
14 | title: "Documentation",
15 | href: "/docs",
16 | },
17 | ],
18 | }
19 |
--------------------------------------------------------------------------------
/config/site.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@/env.mjs";
2 | import { SiteConfig } from "types"
3 |
4 | const site_url = env.NEXT_PUBLIC_APP_URL;
5 |
6 | export const siteConfig: SiteConfig = {
7 | name: "BuzzDaddy",
8 | description:
9 | "Harness the power of Next.js 14, Prisma, Planetscale, Auth.js, Resend, React Email, Shadcn/ui and Stripe to build your next big thing.",
10 | url: site_url,
11 | ogImage: `${site_url}/og.jpg`,
12 | links: {
13 | twitter: "https://twitter.com/cameronyking4",
14 | github: "https://github.com/cameronking4/saas-starter",
15 | },
16 | mailSupport: "support@saas-starter.com"
17 | }
18 |
--------------------------------------------------------------------------------
/config/subscriptions.ts:
--------------------------------------------------------------------------------
1 | import { SubscriptionPlan } from "types";
2 | import { env } from "@/env.mjs";
3 |
4 | export const pricingData: SubscriptionPlan[] = [
5 | // {
6 | // title: 'Starter',
7 | // description: 'For Beginners',
8 | // benefits: [
9 | // 'Up to 100 monthly posts',
10 | // 'Basic analytics and reporting',
11 | // 'Access to standard templates',
12 | // ],
13 | // limitations: [
14 | // 'No priority access to new features.',
15 | // 'Limited customer support',
16 | // 'No custom branding',
17 | // 'Limited access to business resources.',
18 | // ],
19 | // prices: {
20 | // monthly: 0,
21 | // yearly: 0,
22 | // },
23 | // stripeIds: {
24 | // monthly: null,
25 | // yearly: null,
26 | // },
27 | // },
28 | {
29 | title: "Pro",
30 | description: "Unlock Advanced Features",
31 | benefits: [
32 | "Up to 500 monthly posts",
33 | "Advanced analytics and reporting",
34 | "Access to business templates",
35 | "Priority customer support",
36 | "Exclusive webinars and training.",
37 | ],
38 | limitations: [
39 | "No custom branding",
40 | "Limited access to business resources.",
41 | ],
42 | prices: {
43 | monthly: 15,
44 | yearly: 144,
45 | },
46 | stripeIds: {
47 | monthly: env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID,
48 | yearly: env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID,
49 | },
50 | },
51 | {
52 | title: "Business",
53 | description: "For Power Users",
54 | benefits: [
55 | "Unlimited posts",
56 | "Real-time analytics and reporting",
57 | "Access to all templates, including custom branding",
58 | "24/7 business customer support",
59 | "Personalized onboarding and account management.",
60 | ],
61 | limitations: [],
62 | prices: {
63 | monthly: 30,
64 | yearly: 300,
65 | },
66 | stripeIds: {
67 | monthly: env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID,
68 | yearly: env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID,
69 | },
70 | },
71 | ];
72 |
--------------------------------------------------------------------------------
/content/authors/shadcn.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: shadcn
3 | avatar: /images/avatars/shadcn.png
4 | twitter: shadcn
5 | ---
6 |
--------------------------------------------------------------------------------
/content/docs/documentation/code-blocks.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Code Blocks
3 | description: Advanced code blocks with highlighting, file names and more.
4 | ---
5 |
6 | The code blocks on the documentation site and the blog are powered by [rehype-pretty-code](https://github.com/atomiks/rehype-pretty-code). The syntax highlighting is done using [shiki](https://github.com/shikijs/shiki).
7 |
8 | It has the following features:
9 |
10 | 1. Beautiful code blocks with syntax highlighting.
11 | 2. Support for VS Code themes.
12 | 3. Works with hundreds of languages.
13 | 4. Line and word highlighting.
14 | 5. Support for line numbers.
15 | 6. Show code block titles using meta strings.
16 |
17 |
18 |
19 | Thanks to Shiki, highlighting is done at build time. No JavaScript is sent to the client for runtime highlighting.
20 |
21 |
22 |
23 | ## Example
24 |
25 | ```ts showLineNumbers title="next.config.js" {3} /appDir: true/
26 | import { withContentlayer } from "next-contentlayer"
27 |
28 | /** @type {import('next').NextConfig} */
29 | const nextConfig = {
30 | reactStrictMode: true,
31 | images: {
32 | domains: ["avatars.githubusercontent.com"],
33 | },
34 | experimental: {
35 | appDir: true,
36 | serverComponentsExternalPackages: ["@prisma/client"],
37 | },
38 | }
39 |
40 | export default withContentlayer(nextConfig)
41 | ```
42 |
43 | ## Title
44 |
45 | ````mdx
46 | ```ts title="path/to/file.ts"
47 | // Code here
48 | ```
49 | ````
50 |
51 | ## Line Highlight
52 |
53 | ````mdx
54 | ```ts {1,3-6}
55 | // Highlight line 1 and line 3 to 6
56 | ```
57 | ````
58 |
59 | ## Word Highlight
60 |
61 | ````mdx
62 | ```ts /shadcn/
63 | // Highlight the word shadcn.
64 | ```
65 | ````
66 |
67 | ## Line Numbers
68 |
69 | ````mdx
70 | ```ts showLineNumbers
71 | // This will show line numbers.
72 | ```
73 | ````
74 |
--------------------------------------------------------------------------------
/content/docs/documentation/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Documentation
3 | description: Build your documentation site using Contentlayer and MDX.
4 | ---
5 |
6 | Taxonomy includes a documentation site built using [Contentlayer](https://contentlayer.dev) and [MDX](http://mdxjs.com).
7 |
8 | ## Features
9 |
10 | It comes with the following features out of the box:
11 |
12 | 1. Write content using MDX.
13 | 2. Transform and validate content using Contentlayer.
14 | 3. MDX components such as ` ` and ` `.
15 | 4. Support for table of contents.
16 | 5. Custom navigation with prev and next pager.
17 | 6. Beautiful code blocks using `rehype-pretty-code`.
18 | 7. Syntax highlighting using `shiki`.
19 | 8. Built-in search (_in progress_).
20 | 9. Dark mode (_in progress_).
21 |
22 | ## How is it built
23 |
24 | Click on a section below to learn how the documentation site built.
25 |
26 |
27 |
28 |
29 |
30 | ### Contentlayer
31 |
32 | Learn how to use MDX with Contentlayer.
33 |
34 |
35 |
36 |
37 |
38 | ### Components
39 |
40 | Using React components in Mardown.
41 |
42 |
43 |
44 |
45 |
46 | ### Code Blocks
47 |
48 | Beautiful code blocks with syntax highlighting.
49 |
50 |
51 |
52 |
53 |
54 | ### Style Guide
55 |
56 | View a sample page with all the styles.
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/content/docs/in-progress.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Not Implemented
3 | description: This page is in progress.
4 | ---
5 |
6 |
7 |
8 | This site is a work in progress. If you see dummy text on a page, it means I'm still working on it. You can follow updates on Twitter [@shadcn](https://twitter.com/shadcn).
9 |
10 |
11 |
--------------------------------------------------------------------------------
/content/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Documentation
3 | description: Welcome to the Taxonomy documentation.
4 | ---
5 |
6 | This is the documentation for the Taxonomy site.
7 |
8 | This is an example of a doc site built using [ContentLayer](/docs/documentation/contentlayer) and MDX.
9 |
10 |
11 |
12 | This site is a work in progress. If you see dummy text on a page, it means I'm still working on it. You can follow updates on Twitter [@shadcn](https://twitter.com/shadcn).
13 |
14 |
15 |
16 | ## Features
17 |
18 | Select a feature below to learn more about it.
19 |
20 |
21 |
22 |
23 |
24 | ### Documentation
25 |
26 | This documentation site built using Contentlayer.
27 |
28 |
29 |
30 |
31 |
32 | ### Marketing
33 |
34 | The marketing site with landing pages.
35 |
36 |
37 |
38 |
39 |
40 | ### App
41 |
42 | The dashboard with auth and subscriptions.
43 |
44 |
45 |
46 |
47 |
48 | ### Blog
49 |
50 | The blog built using Contentlayer and MDX.
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/content/pages/privacy.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Privacy
3 | description: The Privacy Policy for Taxonomy App.
4 | ---
5 |
6 | Blandit libero volutpat sed cras ornare arcu. Cursus sit amet dictum sit amet. Nunc vel risus commodo viverra maecenas accumsan. Libero id faucibus nisl tincidunt eget nullam non nisi est. Varius quam quisque id diam vel quam. Id donec ultrices tincidunt arcu non.
7 |
8 | ## Consent
9 |
10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Volutpat sed cras ornare arcu. Nibh ipsum consequat nisl vel pretium lectus quam id leo. A arcu cursus vitae congue. Amet justo donec enim diam. Vel pharetra vel turpis nunc eget lorem. Gravida quis blandit turpis cursus in. Semper auctor neque vitae tempus. Elementum facilisis leo vel fringilla est ullamcorper eget nulla. Imperdiet nulla malesuada pellentesque elit eget.
11 |
12 | Felis donec et odio pellentesque diam volutpat commodo sed.
13 |
14 | Tortor consequat id porta nibh. Fames ac turpis egestas maecenas pharetra convallis posuere morbi leo. Scelerisque fermentum dui faucibus in. Tortor posuere ac ut consequat semper viverra.
15 |
16 | ## Information we collect
17 |
18 | Amet justo donec enim diam. In hendrerit gravida rutrum quisque non. Hac habitasse platea dictumst quisque sagittis purus sit.
19 |
20 | ## How we use your Information
21 |
22 | Ut sem nulla pharetra diam sit amet nisl suscipit adipiscing. Consectetur adipiscing elit pellentesque habitant. Ut tristique et egestas quis ipsum suspendisse ultrices gravida.
23 |
--------------------------------------------------------------------------------
/emails/magic-link-email.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Hr,
7 | Html, Preview,
8 | Section,
9 | Tailwind,
10 | Text
11 | } from '@react-email/components';
12 | import { Icons } from '../components/shared/icons';
13 |
14 | type MagicLinkEmailProps = {
15 | actionUrl: string
16 | firstName: string
17 | mailType: "login" | "register"
18 | siteName: string
19 | }
20 |
21 | export const MagicLinkEmail = ({
22 | firstName = '',
23 | actionUrl,
24 | mailType,
25 | siteName
26 | }: MagicLinkEmailProps) => (
27 |
28 |
29 |
30 | The sales intelligence platform that helps you uncover qualified leads.
31 |
32 |
33 |
34 |
35 |
36 |
37 | Hi {firstName},
38 |
39 |
40 | Welcome to {siteName} ! Click the link below to {mailType === "login" ? "sign in to" : "activate"} your account.
41 |
42 |
43 |
47 | {mailType === "login" ? "Sign in" : "Activate Account"}
48 |
49 |
50 |
51 | This link expires in 24 hours and can only be used once.
52 |
53 | {mailType === "login" ? (
54 |
55 | If you did not try to log into your account, you can safely ignore it.
56 |
57 | ) : null}
58 |
59 |
60 | 123 Code Street, Suite 404, Devtown, CA 98765
61 |
62 |
63 |
64 |
65 |
66 | );
67 |
68 | export default MagicLinkEmail;
--------------------------------------------------------------------------------
/env.mjs:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 |
4 | export const env = createEnv({
5 | server: {
6 | // This is optional because it's only used in development.
7 | // See https://next-auth.js.org/deployment.
8 | NEXTAUTH_URL: z.string().url().optional(),
9 | NEXTAUTH_SECRET: z.string().min(1),
10 | GOOGLE_CLIENT_ID: z.string().min(1),
11 | GOOGLE_CLIENT_SECRET: z.string().min(1),
12 | GITHUB_OAUTH_TOKEN: z.string().min(1),
13 | DATABASE_URL: z.string().min(1),
14 | RESEND_API_KEY: z.string().min(1),
15 | STRIPE_API_KEY: z.string().min(1),
16 | STRIPE_WEBHOOK_SECRET: z.string().min(1),
17 | },
18 | client: {
19 | NEXT_PUBLIC_APP_URL: z.string().min(1),
20 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID: z.string().min(1),
21 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID: z.string().min(1),
22 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID: z.string().min(1),
23 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID: z.string().min(1),
24 | },
25 | runtimeEnv: {
26 | NEXTAUTH_URL: process.env.NEXTAUTH_URL,
27 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
28 | GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
29 | GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
30 | GITHUB_OAUTH_TOKEN: process.env.GITHUB_OAUTH_TOKEN,
31 | DATABASE_URL: process.env.DATABASE_URL,
32 | RESEND_API_KEY: process.env.RESEND_API_KEY,
33 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
34 | // Stripe
35 | STRIPE_API_KEY: process.env.STRIPE_API_KEY,
36 | STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
37 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID:
38 | process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID,
39 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID:
40 | process.env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID,
41 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID:
42 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID,
43 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID:
44 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID,
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/hooks/use-intersection-observer.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect, useState } from "react";
2 |
3 | interface Args extends IntersectionObserverInit {
4 | freezeOnceVisible?: boolean;
5 | }
6 |
7 | function useIntersectionObserver(
8 | elementRef: RefObject,
9 | {
10 | threshold = 0,
11 | root = null,
12 | rootMargin = "0%",
13 | freezeOnceVisible = false,
14 | }: Args,
15 | ): IntersectionObserverEntry | undefined {
16 | const [entry, setEntry] = useState();
17 |
18 | const frozen = entry?.isIntersecting && freezeOnceVisible;
19 |
20 | const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
21 | setEntry(entry);
22 | };
23 |
24 | useEffect(() => {
25 | const node = elementRef?.current; // DOM Ref
26 | const hasIOSupport = !!window.IntersectionObserver;
27 |
28 | if (!hasIOSupport || frozen || !node) return;
29 |
30 | const observerParams = { threshold, root, rootMargin };
31 | const observer = new IntersectionObserver(updateEntry, observerParams);
32 |
33 | observer.observe(node);
34 |
35 | return () => observer.disconnect();
36 |
37 | // eslint-disable-next-line react-hooks/exhaustive-deps
38 | }, [threshold, root, rootMargin, frozen]);
39 |
40 | return entry;
41 | }
42 |
43 | export default useIntersectionObserver;
44 |
--------------------------------------------------------------------------------
/hooks/use-keywords-modal.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface State {
4 | isOpen: boolean;
5 | campaignId: string | null;
6 | onOpen: (campaignId: string) => void;
7 | onClose: () => void;
8 | onChange: (open: boolean) => void;
9 | }
10 |
11 | export const useKeywordsModal = create((set) => ({
12 | isOpen: false,
13 | campaignId: null,
14 | onOpen: (campaignId) => set({ isOpen: true, campaignId }),
15 | onClose: () => set({ isOpen: false, campaignId: null }),
16 | onChange: (open) => {
17 | if (!open) {
18 | set({ isOpen: false, campaignId: null });
19 | }
20 | },
21 | }));
22 |
--------------------------------------------------------------------------------
/hooks/use-local-storage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const useLocalStorage = (
4 | key: string,
5 | initialValue: T,
6 | ): [T, (value: T) => void] => {
7 | const [storedValue, setStoredValue] = useState(initialValue);
8 |
9 | useEffect(() => {
10 | // Retrieve from localStorage
11 | const item = window.localStorage.getItem(key);
12 | if (item) {
13 | setStoredValue(JSON.parse(item));
14 | }
15 | }, [key]);
16 |
17 | const setValue = (value: T) => {
18 | // Save state
19 | setStoredValue(value);
20 | // Save to localStorage
21 | window.localStorage.setItem(key, JSON.stringify(value));
22 | };
23 | return [storedValue, setValue];
24 | };
25 |
26 | export default useLocalStorage;
27 |
--------------------------------------------------------------------------------
/hooks/use-lock-body.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | // @see https://usehooks.com/useLockBodyScroll.
4 | export function useLockBody() {
5 | React.useLayoutEffect((): (() => void) => {
6 | const originalStyle: string = window.getComputedStyle(
7 | document.body
8 | ).overflow
9 | document.body.style.overflow = "hidden"
10 | return () => (document.body.style.overflow = originalStyle)
11 | }, [])
12 | }
13 |
--------------------------------------------------------------------------------
/hooks/use-media-query.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export default function useMediaQuery() {
4 | const [device, setDevice] = useState<"mobile" | "tablet" | "desktop" | null>(
5 | null,
6 | );
7 | const [dimensions, setDimensions] = useState<{
8 | width: number;
9 | height: number;
10 | } | null>(null);
11 |
12 | useEffect(() => {
13 | const checkDevice = () => {
14 | if (window.matchMedia("(max-width: 640px)").matches) {
15 | setDevice("mobile");
16 | } else if (
17 | window.matchMedia("(min-width: 641px) and (max-width: 1024px)").matches
18 | ) {
19 | setDevice("tablet");
20 | } else {
21 | setDevice("desktop");
22 | }
23 | setDimensions({ width: window.innerWidth, height: window.innerHeight });
24 | };
25 |
26 | // Initial detection
27 | checkDevice();
28 |
29 | // Listener for windows resize
30 | window.addEventListener("resize", checkDevice);
31 |
32 | // Cleanup listener
33 | return () => {
34 | window.removeEventListener("resize", checkDevice);
35 | };
36 | }, []);
37 |
38 | return {
39 | device,
40 | width: dimensions?.width,
41 | height: dimensions?.height,
42 | isMobile: device === "mobile",
43 | isTablet: device === "tablet",
44 | isDesktop: device === "desktop",
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/hooks/use-mounted.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | export function useMounted() {
4 | const [mounted, setMounted] = React.useState(false)
5 |
6 | React.useEffect(() => {
7 | setMounted(true)
8 | }, [])
9 |
10 | return mounted
11 | }
12 |
--------------------------------------------------------------------------------
/hooks/use-mutation-observer.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | export const useMutationObserver = (
4 | ref: React.MutableRefObject,
5 | callback: MutationCallback,
6 | options = {
7 | attributes: true,
8 | characterData: true,
9 | childList: true,
10 | subtree: true,
11 | }
12 | ) => {
13 | React.useEffect(() => {
14 | if (ref.current) {
15 | const observer = new MutationObserver(callback)
16 | observer.observe(ref.current, options)
17 | return () => observer.disconnect()
18 | }
19 | }, [ref, callback, options])
20 | }
--------------------------------------------------------------------------------
/hooks/use-post-preferences.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface State {
4 | isOpen: boolean;
5 | campaignId: string | null;
6 | onOpen: (campaignId: string) => void;
7 | onClose: () => void;
8 | onChange: (open: boolean) => void;
9 | }
10 |
11 | export const usePostPreferencesModal = create((set) => ({
12 | isOpen: false,
13 | campaignId: null,
14 | onOpen: (campaignId) => set({ isOpen: true, campaignId }),
15 | onClose: () => set({ isOpen: false, campaignId: null }),
16 | onChange: (open) => {
17 | if (!open) {
18 | set({ isOpen: false, campaignId: null });
19 | }
20 | },
21 | }));
22 |
--------------------------------------------------------------------------------
/hooks/use-scroll.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 |
3 | export default function useScroll(threshold: number) {
4 | const [scrolled, setScrolled] = useState(false);
5 |
6 | const onScroll = useCallback(() => {
7 | setScrolled(window.pageYOffset > threshold);
8 | }, [threshold]);
9 |
10 | useEffect(() => {
11 | window.addEventListener("scroll", onScroll);
12 | return () => window.removeEventListener("scroll", onScroll);
13 | }, [onScroll]);
14 |
15 | return scrolled;
16 | }
17 |
--------------------------------------------------------------------------------
/hooks/use-signin-modal.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | interface useSigninModalStore {
4 | isOpen: boolean;
5 | onOpen: () => void;
6 | onClose: () => void;
7 | }
8 |
9 | export const useSigninModal = create((set) => ({
10 | isOpen: false,
11 | onOpen: () => set({ isOpen: true }),
12 | onClose: () => set({ isOpen: false }),
13 | }));
--------------------------------------------------------------------------------
/hooks/use-tab-store.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from "@/schemas/content-interaction";
2 | import { create } from "zustand";
3 |
4 | interface TabStore {
5 | activeTab: Platform;
6 | setActiveTab: (tab: Platform) => void;
7 | }
8 |
9 | export const useTabStore = create((set) => ({
10 | activeTab: "TWITTER",
11 | setActiveTab: (tab) => set({ activeTab: tab }),
12 | }));
13 |
14 | export const getActiveTab = () => {
15 | const activeTab = useTabStore.getState().activeTab;
16 | return activeTab;
17 | };
18 |
19 | export const setActiveTab = (tab: Platform) => {
20 | useTabStore.getState().setActiveTab(tab);
21 | };
22 |
--------------------------------------------------------------------------------
/lib/apify.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { ApifyClient } from "apify-client";
4 |
5 | export const apifyClient = new ApifyClient({
6 | token: "apify_api_XpGJ5Ree0yswEdab11WyUzoq1Ed8XM0uI6dp",
7 | });
8 |
--------------------------------------------------------------------------------
/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | import "server-only";
4 |
5 | declare global {
6 | // eslint-disable-next-line no-var
7 | var cachedPrisma: PrismaClient;
8 | }
9 |
10 | export let prisma: PrismaClient;
11 | if (process.env.NODE_ENV === "production") {
12 | prisma = new PrismaClient();
13 | } else {
14 | if (!global.cachedPrisma) {
15 | global.cachedPrisma = new PrismaClient();
16 | }
17 | prisma = global.cachedPrisma;
18 | }
19 |
--------------------------------------------------------------------------------
/lib/email.ts:
--------------------------------------------------------------------------------
1 | import { Resend } from "resend";
2 |
3 | import { env } from "@/env.mjs";
4 |
5 | export const resend = new Resend(env.RESEND_API_KEY);
6 |
--------------------------------------------------------------------------------
/lib/encryption.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 |
3 | const ENCRYPTION_KEY: string | undefined = process.env.ENCRYPTION_KEY;
4 | if (!ENCRYPTION_KEY || ENCRYPTION_KEY.length !== 64) {
5 | throw new Error("ENCRYPTION_KEY must be 64 hex characters long (32 bytes).");
6 | }
7 | const IV_LENGTH = 16; // For AES, this is always 16
8 |
9 | /**
10 | * Encrypts a given text using AES-256-CBC.
11 | * @param {string} text - The text to encrypt.
12 | * @returns {string} - The encrypted text in hex format, with the IV prepended.
13 | */
14 | function encrypt(text: string): string {
15 | if (!ENCRYPTION_KEY) {
16 | throw new Error("ENCRYPTION_KEY is not defined.");
17 | }
18 | const iv = crypto.randomBytes(IV_LENGTH);
19 | const cipher = crypto.createCipheriv(
20 | "aes-256-cbc",
21 | Buffer.from(ENCRYPTION_KEY, "hex"),
22 | iv,
23 | );
24 | let encrypted = cipher.update(text, "utf8", "hex");
25 | encrypted += cipher.final("hex");
26 | return `${iv.toString("hex")}:${encrypted}`;
27 | }
28 |
29 | /**
30 | * Decrypts a given encrypted text using AES-256-CBC.
31 | * @param {string} text - The encrypted text in hex format, with the IV prepended.
32 | * @returns {string} - The decrypted text.
33 | */
34 | function decrypt(text: string): string {
35 | if (!ENCRYPTION_KEY) {
36 | throw new Error("ENCRYPTION_KEY is not defined.");
37 | }
38 | const [iv, encryptedText] = text.split(":");
39 | const ivBuffer = Buffer.from(iv, "hex");
40 | const encryptedBuffer = Buffer.from(encryptedText, "hex");
41 | const decipher = crypto.createDecipheriv(
42 | "aes-256-cbc",
43 | Buffer.from(ENCRYPTION_KEY, "hex"),
44 | ivBuffer,
45 | );
46 | // @ts-ignore
47 | let decrypted = decipher.update(encryptedBuffer, "hex", "utf8");
48 | // @ts-ignore
49 | decrypted += decipher.final("utf8");
50 | return decrypted;
51 | }
52 |
53 | export { decrypt, encrypt };
54 |
--------------------------------------------------------------------------------
/lib/exceptions.ts:
--------------------------------------------------------------------------------
1 | export class RequiresProPlanError extends Error {
2 | constructor(message = "This action requires a pro plan") {
3 | super(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/lib/linkedin-api/access_token.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/lib/linkedin-api/access_token.json
--------------------------------------------------------------------------------
/lib/linkedin.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { AuthClient, RestliClient } from "linkedin-api-client";
3 |
4 | import { getEnvVar } from "@/lib/utils";
5 |
6 | export const linkedinAuthClient = (): AuthClient => {
7 | return new AuthClient({
8 | clientId: getEnvVar("LINKEDIN_CLIENT_ID"),
9 | clientSecret: getEnvVar("LINKEDIN_CLIENT_SECRET"),
10 | redirectUrl: getEnvVar("LINKEDIN_REDIRECT_URL"),
11 | });
12 | };
13 |
14 | export const linkedinClient = (): RestliClient => {
15 | const restliClient = new RestliClient();
16 | return restliClient;
17 | };
18 |
19 | export const getLinkedInAuthUrl = () => {
20 | const clientId = getEnvVar("LINKEDIN_CLIENT_ID");
21 | const redirectUri = getEnvVar("LINKEDIN_REDIRECT_URL");
22 | const state = "random_state_string"; // Use a secure random string
23 | const scope = "r_liteprofile r_emailaddress";
24 |
25 | return `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}&scope=${encodeURIComponent(scope)}`;
26 | };
27 |
28 | export const exchangeAuthorizationCodeForToken = async (code: string) => {
29 | const clientId = getEnvVar("LINKEDIN_CLIENT_ID");
30 | const clientSecret = getEnvVar("LINKEDIN_CLIENT_SECRET");
31 | const redirectUri = getEnvVar("LINKEDIN_REDIRECT_URL");
32 |
33 | const response = await axios.post(
34 | "https://www.linkedin.com/oauth/v2/accessToken",
35 | null,
36 | {
37 | params: {
38 | grant_type: "authorization_code",
39 | code: code,
40 | redirect_uri: redirectUri,
41 | client_id: clientId,
42 | client_secret: clientSecret,
43 | },
44 | headers: {
45 | "Content-Type": "application/x-www-form-urlencoded",
46 | },
47 | },
48 | );
49 |
50 | return response.data.access_token;
51 | };
52 |
--------------------------------------------------------------------------------
/lib/reddit/reddit.ts:
--------------------------------------------------------------------------------
1 | export async function getRedditAccessToken() {
2 | const response = await fetch("https://www.reddit.com/api/v1/access_token", {
3 | method: "POST",
4 | headers: {
5 | "Content-Type": "application/x-www-form-urlencoded",
6 | Authorization: `Basic ${Buffer.from(
7 | `${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`
8 | ).toString("base64")}`,
9 | },
10 | body: new URLSearchParams({
11 | grant_type: "password",
12 | username: process.env.REDDIT_USERNAME!,
13 | password: process.env.REDDIT_PASSWORD!,
14 | }),
15 | });
16 |
17 | const data = await response.json();
18 |
19 | if (!response.ok) {
20 | throw new Error(`Failed to obtain Reddit access token: ${data.error}`);
21 | }
22 |
23 | return {
24 | accessToken: data.access_token,
25 | refreshToken: data.refresh_token,
26 | };
27 | }
28 |
29 |
30 | export class RedditClient {
31 | private accessToken: string;
32 |
33 | constructor(accessToken: string) {
34 | this.accessToken = accessToken;
35 | }
36 |
37 | async searchPosts(query: string, after: string | null = null) {
38 | const searchParams = new URLSearchParams({
39 | q: query,
40 | sort: "relevance",
41 | limit: "10",
42 | });
43 | if (after) {
44 | searchParams.append("after", after);
45 | }
46 |
47 | const response = await fetch(`https://oauth.reddit.com/search?${searchParams.toString()}`, {
48 | headers: {
49 | Authorization: `Bearer ${this.accessToken}`,
50 | "User-Agent": "your-app-name",
51 | },
52 | });
53 |
54 | if (!response.ok) {
55 | throw new Error(`Failed to fetch posts for query: ${query}`);
56 | }
57 |
58 | return response.json();
59 | }
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/lib/session.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next"
2 |
3 | import { authOptions } from "@/lib/auth"
4 |
5 | export async function getCurrentUser() {
6 | const session = await getServerSession(authOptions)
7 |
8 | return session?.user
9 | }
10 |
--------------------------------------------------------------------------------
/lib/stripe.ts:
--------------------------------------------------------------------------------
1 | import "server-only";
2 |
3 | import Stripe from "stripe";
4 |
5 | import { env } from "@/env.mjs";
6 |
7 | export const stripe = new Stripe(env.STRIPE_API_KEY, {
8 | apiVersion: "2024-06-20",
9 | typescript: true,
10 | });
11 |
--------------------------------------------------------------------------------
/lib/subscription.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // TODO: Fix this when we turn strict mode on.
3 | import { UserSubscriptionPlan } from "types";
4 | import { pricingData } from "@/config/subscriptions";
5 | import { prisma } from "@/lib/db";
6 | import { stripe } from "@/lib/stripe";
7 |
8 | export async function getUserSubscriptionPlan(
9 | userId: string,
10 | ): Promise {
11 | const user = await prisma.user.findFirst({
12 | where: {
13 | id: userId,
14 | },
15 | select: {
16 | stripeSubscriptionId: true,
17 | stripeCurrentPeriodEnd: true,
18 | stripeCustomerId: true,
19 | stripePriceId: true,
20 | },
21 | });
22 |
23 | if (!user) {
24 | throw new Error("User not found");
25 | }
26 |
27 | // Check if user is on a paid plan.
28 | const isPaid =
29 | user.stripePriceId &&
30 | user.stripeCurrentPeriodEnd?.getTime() + 86_400_000 > Date.now()
31 | ? true
32 | : false;
33 |
34 | // Find the pricing data corresponding to the user's plan
35 | const userPlan =
36 | pricingData.find((plan) => plan.stripeIds.monthly === user.stripePriceId) ||
37 | pricingData.find((plan) => plan.stripeIds.yearly === user.stripePriceId);
38 |
39 | const plan = isPaid && userPlan ? userPlan : pricingData[0];
40 |
41 | const interval = isPaid
42 | ? userPlan?.stripeIds.monthly === user.stripePriceId
43 | ? "month"
44 | : userPlan?.stripeIds.yearly === user.stripePriceId
45 | ? "year"
46 | : null
47 | : null;
48 |
49 | let isCanceled = false;
50 | if (isPaid && user.stripeSubscriptionId) {
51 | const stripePlan = await stripe.subscriptions.retrieve(
52 | user.stripeSubscriptionId,
53 | );
54 | isCanceled = stripePlan.cancel_at_period_end;
55 | }
56 |
57 | return {
58 | ...plan,
59 | ...user,
60 | stripeCurrentPeriodEnd: user.stripeCurrentPeriodEnd?.getTime(),
61 | isPaid,
62 | interval,
63 | isCanceled,
64 | };
65 | }
66 |
--------------------------------------------------------------------------------
/lib/toc.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // TODO: Fix this when we turn strict mode on.
3 |
4 | import { toc } from "mdast-util-toc"
5 | import { remark } from "remark"
6 | import { visit } from "unist-util-visit"
7 |
8 | const textTypes = ["text", "emphasis", "strong", "inlineCode"]
9 |
10 | function flattenNode(node) {
11 | const p = []
12 | visit(node, (node) => {
13 | if (!textTypes.includes(node.type)) return
14 | p.push(node.value)
15 | })
16 | return p.join(``)
17 | }
18 |
19 | interface Item {
20 | title: string
21 | url: string
22 | items?: Item[]
23 | }
24 |
25 | interface Items {
26 | items?: Item[]
27 | }
28 |
29 | function getItems(node, current): Items {
30 | if (!node) {
31 | return {}
32 | }
33 |
34 | if (node.type === "paragraph") {
35 | visit(node, (item) => {
36 | if (item.type === "link") {
37 | current.url = item.url
38 | current.title = flattenNode(node)
39 | }
40 |
41 | if (item.type === "text") {
42 | current.title = flattenNode(node)
43 | }
44 | })
45 |
46 | return current
47 | }
48 |
49 | if (node.type === "list") {
50 | current.items = node.children.map((i) => getItems(i, {}))
51 |
52 | return current
53 | } else if (node.type === "listItem") {
54 | const heading = getItems(node.children[0], {})
55 |
56 | if (node.children.length > 1) {
57 | getItems(node.children[1], heading)
58 | }
59 |
60 | return heading
61 | }
62 |
63 | return {}
64 | }
65 |
66 | const getToc = () => (node, file) => {
67 | const table = toc(node)
68 | file.data = getItems(table.map, {})
69 | }
70 |
71 | export type TableOfContents = Items
72 |
73 | export async function getTableOfContents(
74 | content: string
75 | ): Promise {
76 | const result = await remark().use(getToc).process(content)
77 |
78 | return result.data
79 | }
80 |
--------------------------------------------------------------------------------
/lib/twitter.ts:
--------------------------------------------------------------------------------
1 | import { TwitterApiRateLimitPlugin } from "@twitter-api-v2/plugin-rate-limit";
2 | import { TwitterApi } from "twitter-api-v2";
3 |
4 | import { getEnvVar } from "@/lib/utils";
5 |
6 | export const TWITTER_CALLBACK_URL = getEnvVar("TWITTER_REDIRECT_URL");
7 |
8 | export const twitterClient = (): TwitterApi | null => {
9 | return new TwitterApi({
10 | clientId: process.env.TWITTER_CLIENT_ID,
11 | clientSecret: "GdzvuTKI1v0B_-DyE-p3-d6RqvQUGZNcnLFS7CstQRuy-tP8KY",
12 | });
13 | };
14 |
15 | export const twitterAuthClient = (
16 | accessToken: string,
17 | rateLimitPlugin?: TwitterApiRateLimitPlugin,
18 | ): TwitterApi => {
19 | let client: TwitterApi;
20 |
21 | rateLimitPlugin
22 | ? (client = new TwitterApi(accessToken, {
23 | plugins: [rateLimitPlugin],
24 | }))
25 | : (client = new TwitterApi(accessToken));
26 |
27 | return client;
28 | };
29 |
30 | export const client = () => {
31 | const tC = new TwitterApi({
32 | appKey: process.env.TWITTER_APP_KEY || "",
33 | appSecret: process.env.TWITTER_APP_SECRET || "",
34 | accessToken: process.env.TWITTER_ACCESS_TOKEN || "",
35 | accessSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET || "",
36 | });
37 | return tC;
38 | };
39 |
--------------------------------------------------------------------------------
/lib/validations/auth.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const userAuthSchema = z.object({
4 | email: z.string().email(),
5 | })
6 |
--------------------------------------------------------------------------------
/lib/validations/og.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const ogImageSchema = z.object({
4 | heading: z.string(),
5 | type: z.string(),
6 | mode: z.enum(["light", "dark"]).default("dark"),
7 | })
8 |
--------------------------------------------------------------------------------
/lib/validations/twitter.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const connectXSchema = z.object({
4 | clientId: z.string().min(2, {
5 | message: "Client ID can't be empty.",
6 | }),
7 | clientSecret: z.string().min(2, {
8 | message: "Client Secret can't be empty.",
9 | }),
10 | });
11 |
12 | export type ConnectX = z.infer;
13 |
--------------------------------------------------------------------------------
/lib/validations/user.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const userNameSchema = z.object({
4 | name: z.string().min(3).max(32),
5 | })
6 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { getToken } from "next-auth/jwt";
3 | import { withAuth } from "next-auth/middleware";
4 |
5 | export default withAuth(
6 | async function middleware(req) {
7 | const token = await getToken({ req });
8 | const isAuth = !!token;
9 | const isAuthPage =
10 | req.nextUrl.pathname.startsWith("/login") ||
11 | req.nextUrl.pathname.startsWith("/register");
12 |
13 | if (isAuthPage) {
14 | if (isAuth) {
15 | return NextResponse.redirect(new URL("/dashboard", req.url));
16 | }
17 |
18 | return null;
19 | }
20 |
21 | if (!isAuth) {
22 | let from = req.nextUrl.pathname;
23 | if (req.nextUrl.search) {
24 | from += req.nextUrl.search;
25 | }
26 |
27 | return NextResponse.redirect(
28 | new URL(`/login?from=${encodeURIComponent(from)}`, req.url),
29 | );
30 | }
31 | },
32 | {
33 | callbacks: {
34 | async authorized() {
35 | // This is a work-around for handling redirect on auth pages.
36 | // We return true here so that the middleware function above
37 | // is always called.
38 |
39 | return true;
40 | },
41 | },
42 | },
43 | );
44 |
45 | export const config = {
46 | matcher: ["/dashboard/:path*", "/login", "/register"],
47 | };
48 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | // FIX: I changed .mjs to .js
2 | // More info: https://github.com/shadcn-ui/taxonomy/issues/100#issuecomment-1605867844
3 |
4 | import("./env.mjs");
5 |
6 | /** @type {import('next').NextConfig} */
7 | const nextConfig = {
8 | reactStrictMode: true,
9 | swcMinify: true,
10 | images: {
11 | remotePatterns: [
12 | {
13 | protocol: "https",
14 | hostname: "avatars.githubusercontent.com",
15 | },
16 | {
17 | protocol: "https",
18 | hostname: "utfs.io",
19 | },
20 | {
21 | protocol: "https",
22 | hostname: "randomuser.me",
23 | },
24 | {
25 | protocol: "https",
26 | hostname: "lh3.googleusercontent.com",
27 | },
28 | {
29 | protocol: "https",
30 | hostname: "images.unsplash.com",
31 | },
32 | {
33 | protocol: "https",
34 | hostname: "pbs.twimg.com",
35 | },
36 | ],
37 | },
38 | experimental: {
39 | serverComponentsExternalPackages: ["@prisma/client"],
40 | serverActions: {
41 | allowedOrigins: [
42 | "https://special-space-eureka-j4qv5xq5vq6fqp45-3000.app.github.dev",
43 | ],
44 | },
45 | },
46 | headers: async () => {
47 | return [
48 | {
49 | source: "/api/:path*",
50 | headers: [
51 | {
52 | key: "Access-Control-Allow-Origin",
53 | value: "*", // Set your origin
54 | },
55 | {
56 | key: "Access-Control-Allow-Methods",
57 | value: "GET, POST, PUT, DELETE, OPTIONS",
58 | },
59 | {
60 | key: "Access-Control-Allow-Headers",
61 | value: "Content-Type, Authorization",
62 | },
63 | ],
64 | },
65 | ];
66 | },
67 | };
68 |
69 | module.exports = nextConfig;
70 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/cases/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/cases/1.png
--------------------------------------------------------------------------------
/public/cases/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/cases/2.png
--------------------------------------------------------------------------------
/public/cases/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/cases/3.png
--------------------------------------------------------------------------------
/public/cases/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/cases/4.png
--------------------------------------------------------------------------------
/public/cases/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/cases/5.png
--------------------------------------------------------------------------------
/public/creates.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/creates.gif
--------------------------------------------------------------------------------
/public/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/demo.gif
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/400x400.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/images/400x400.jpg
--------------------------------------------------------------------------------
/public/images/avatars/shadcn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/images/avatars/shadcn.png
--------------------------------------------------------------------------------
/public/images/blog/blog-post-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/images/blog/blog-post-1.jpg
--------------------------------------------------------------------------------
/public/images/blog/blog-post-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/images/blog/blog-post-2.jpg
--------------------------------------------------------------------------------
/public/images/blog/blog-post-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/images/blog/blog-post-3.jpg
--------------------------------------------------------------------------------
/public/images/blog/blog-post-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/images/blog/blog-post-4.jpg
--------------------------------------------------------------------------------
/public/images/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/images/hero.png
--------------------------------------------------------------------------------
/public/images/user-step1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/images/user-step1.gif
--------------------------------------------------------------------------------
/public/images/user-step2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/images/user-step2.gif
--------------------------------------------------------------------------------
/public/images/user-step3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/images/user-step3.gif
--------------------------------------------------------------------------------
/public/og.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/og.jpg
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "BuzzDaddy",
3 | "short_name": "BuzzDaddy",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/public/steps/step1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/steps/step1.gif
--------------------------------------------------------------------------------
/public/steps/step2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/steps/step2.gif
--------------------------------------------------------------------------------
/public/steps/step3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cameronking4/ReplyGuy-clone/391eefab4301d1cd71037714746fd43c98cca16a/public/steps/step3.gif
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/schemas/logging-notification.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export enum ActivityType {
4 | REPLY_SENT = "REPLY_SENT",
5 | CAMPAIGN_CREATED = "CAMPAIGN_CREATED",
6 | AUTOPILOT_ACTIVATED = "AUTOPILOT_ACTIVATED",
7 | AUTOPILOT_DEACTIVATED = "AUTOPILOT_DEACTIVATED",
8 | CAMPAIGN_UPDATED = "CAMPAIGN_UPDATED",
9 | }
10 |
11 | export const CreateActivityLogSchema = z.object({
12 | userId: z.string(),
13 | campaignId: z.string().optional(),
14 | type: z.nativeEnum(ActivityType),
15 | message: z.string().min(1, "Message cannot be empty"),
16 | status: z.enum(["Success", "Failed"]),
17 | });
18 |
19 | export type CreateActivityLogRequest = z.infer;
20 |
--------------------------------------------------------------------------------
/schemas/notification.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const createNotificationSchema = z.object({
4 | userId: z.string(),
5 | campaignId: z.string(),
6 | message: z.string(),
7 | });
8 |
9 | export type CreateNotificationRequest = z.infer<
10 | typeof createNotificationSchema
11 | >;
12 |
13 | export const updateNotificationSchema = z.object({
14 | id: z.string(),
15 | message: z.string(),
16 | read: z.boolean(),
17 | });
18 |
19 | export type UpdateNotificationRequest = z.infer<
20 | typeof updateNotificationSchema
21 | >;
22 |
23 | export const notificationResponseSchema = z.object({
24 | id: z.string(),
25 | userId: z.string(),
26 | campaignId: z.string(),
27 | message: z.string(),
28 | read: z.boolean(),
29 | createdAt: z.date(),
30 | updatedAt: z.date(),
31 | });
32 |
33 | export type NotificationResponse = z.infer;
34 |
--------------------------------------------------------------------------------
/schemas/playground.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const PlatformEnum = z.enum([
4 | "twitter.com",
5 | "reddit.com",
6 | "linkedin.com",
7 | ]);
8 | export type Platform = z.infer;
9 |
10 | export const SafeSearchEnum = z.enum(["off", "active", "medium"]);
11 | export type SafeSearch = z.infer;
12 |
13 | export const TimeRangeEnum = z.enum([
14 | "all",
15 | "qdr:h",
16 | "qdr:d",
17 | "qdr:w",
18 | "qdr:m",
19 | "qdr:y",
20 | ]);
21 | export type TimeRange = z.infer;
22 |
23 | export const SortByEnum = z.enum(["relevance", "date"]);
24 | export type SortBy = z.infer;
25 |
26 | export const SearchFieldEnum = z.enum(["all", "intitle", "inurl", "inanchor"]);
27 | export type SearchField = z.infer;
28 |
29 | export const PlaygroundSettingsSchema = z.object({
30 | keywords: z.string().min(1, "Keywords are required."),
31 | num: z.coerce
32 | .number()
33 | .min(1, "Number of results must be at least 1")
34 | .max(100, "Number of results must be at most 100")
35 | .default(10),
36 | platform: PlatformEnum.default("twitter.com"),
37 | safe: SafeSearchEnum.default("off"),
38 | timeRange: TimeRangeEnum,
39 | sortBy: SortByEnum,
40 | field: SearchFieldEnum,
41 | });
42 | export type PlaygroundSettingsRequest = z.infer<
43 | typeof PlaygroundSettingsSchema
44 | >;
45 |
--------------------------------------------------------------------------------
/styles/mdx.css:
--------------------------------------------------------------------------------
1 | [data-rehype-pretty-code-fragment] code {
2 | @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0 text-sm text-black;
3 | counter-reset: line;
4 | box-decoration-break: clone;
5 | }
6 | [data-rehype-pretty-code-fragment] .line {
7 | @apply px-4 py-1;
8 | }
9 | [data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before {
10 | counter-increment: line;
11 | content: counter(line);
12 | display: inline-block;
13 | width: 1rem;
14 | margin-right: 1rem;
15 | text-align: right;
16 | color: gray;
17 | }
18 | [data-rehype-pretty-code-fragment] .line--highlighted {
19 | @apply bg-slate-300 bg-opacity-10;
20 | }
21 | [data-rehype-pretty-code-fragment] .line-highlighted span {
22 | @apply relative;
23 | }
24 | [data-rehype-pretty-code-fragment] .word--highlighted {
25 | @apply rounded-md bg-slate-300 bg-opacity-10 p-1;
26 | }
27 | [data-rehype-pretty-code-title] {
28 | @apply mt-4 py-2 px-4 text-sm font-medium;
29 | }
30 | [data-rehype-pretty-code-title] + pre {
31 | @apply mt-0;
32 | }
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["./*"]
20 | },
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "strictNullChecks": true
27 | },
28 | "include": [
29 | "next-env.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx",
32 | ".next/types/**/*.ts"
33 | ],
34 | "exclude": ["node_modules"]
35 | }
36 |
--------------------------------------------------------------------------------
/types/env.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace NodeJS {
2 | interface ProcessEnv {
3 | NEXTAUTH_URL: string;
4 | NEXTAUTH_SECRET: string;
5 | AUTH_SECRET: string;
6 |
7 | GOOGLE_CLIENT_ID: string;
8 | GOOGLE_CLIENT_SECRET: string;
9 |
10 | GITHUB_OAUTH_TOKEN: string;
11 | GITHUB_CLIENT_ID: string;
12 | GITHUB_CLIENT_SECRET: string;
13 |
14 | DATABASE_URL: string;
15 | RESEND_API_KEY: string;
16 |
17 | STRIPE_API_KEY: string;
18 | STRIPE_WEBHOOK_SECRET: string;
19 |
20 | NEXT_PUBLIC_APP_URL: string;
21 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID: string;
22 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID: string;
23 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID: string;
24 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID: string;
25 |
26 | OPENAI_API_KEY: string;
27 | SERP_API_KEY: string;
28 |
29 | ENCRYPTION_KEY: string;
30 |
31 | TWITTER_CLIENT_ID: string;
32 | TWITTER_CLIENT_SECRET: string;
33 | TWITTER_REDIRECT_URL: string;
34 |
35 | LINKEDIN_CLIENT_ID: string;
36 | LINKEDIN_CLIENT_SECRET: string;
37 | LINKEDIN_REDIRECT_URL: string;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import { User } from "next-auth"
2 | import { JWT } from "next-auth/jwt"
3 |
4 | type UserId = string
5 |
6 | declare module "next-auth/jwt" {
7 | interface JWT {
8 | id: UserId
9 | }
10 | }
11 |
12 | declare module "next-auth" {
13 | interface Session {
14 | user: User & {
15 | id: UserId
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/types/reddit.ts:
--------------------------------------------------------------------------------
1 | export interface RedditPost {
2 | id: string;
3 | title: string;
4 | selftext: string;
5 | url: string;
6 | author: string;
7 | subreddit: string;
8 | ups: number;
9 | downs: number;
10 | num_comments: number;
11 | created_utc: number;
12 | }
13 |
14 | export interface RedditSearchResult {
15 | posts: RedditPost[];
16 | after: string | null;
17 | }
18 |
19 | export interface RedditCommentResult {
20 | id: string;
21 | body: string;
22 | }
23 |
--------------------------------------------------------------------------------
/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": {
3 | "app/api/**/*": {
4 | "maxDuration": 300
5 | },
6 | "app/**/*": {
7 | "maxDuration": 300
8 | }
9 | },
10 | "crons": [{
11 | "path": "/api/cron/campaign/comment",
12 | "schedule": "0 6 * * *"
13 | },
14 | {
15 | "path": "/api/cron/campaign/post",
16 | "schedule": "0 6 * * *"
17 | }]
18 | }
19 |
--------------------------------------------------------------------------------