15 | Launch your SaaS startup within a few days with the first Admin Dashboard Shadcn UI NextJS boilerplate. Get started with Horizon AI Boilerplate today!
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ### Introduction
29 |
30 | Horizon AI Boilerplate is the first open-source Admin Dashboard OpenAI ChatGPT AI Template made for Shadcn UI, NextJS, and Tailwind CSS! Start creating outstanding Chat AI SaaS Apps faster.
31 |
32 | It comes with over 30+ dark/light frontend individual elements, like buttons, inputs, navbars, nav tabs, cards, or alerts, giving you the freedom of choosing and combining.
33 |
34 | ### Documentation
35 |
36 | Each element is well presented in a very complex documentation. You can read more about the documentation here.
37 |
38 | ### Quick Start
39 |
40 | Install Horizon ChatGPT AI Template by running either of the following:
41 |
42 | - Install NodeJS LTS from [NodeJs Official Page](https://nodejs.org/en/?ref=horizon-documentation) (NOTE: Product only works with LTS version)
43 |
44 |
45 |
46 | Clone the repository with the following command:
47 |
48 | ```bash
49 | git clone https://github.com/horizon-ui/shadcn-nextjs-boilerplate.git
50 | ```
51 |
52 | Run in the terminal this command:
53 |
54 | ```
55 | npm install
56 | ```
57 |
58 |
59 |
60 | ```
61 | npm run init
62 | ```
63 |
64 |
65 |
66 | Then run this command to start your local server:
67 |
68 | ```
69 | npm run dev
70 | ```
71 |
72 |
73 | ### Your API Key is not working?
74 |
75 | - Make sure you have an [OpenAI account](https://platform.openai.com/account) and a valid API key to use ChatGPT. We don't sell API keys.
76 | - Make sure you have your billing info added in [OpenAI Billing page](https://platform.openai.com/account/billing/overview). Without billing info, your API key will not work.
77 | - The app will connect to the OpenAI API server to check if your API Key is working properly.
78 |
79 |
80 | ### Figma Version
81 |
82 | Horizon AI Boilerplate is available in Figma format as well! [Check it out here](https://www.figma.com/community/file/1374394029061088369)! 🎨
83 |
84 |
85 | ### Example Sections
86 |
87 | If you want to get inspiration for your startup project or just show something directly to your clients, you can jump-start your development with our pre-built example sections. You will be able to quickly set up the basic structure for your web project.
88 |
89 | View example sections here
90 |
91 | ---
92 |
93 |
94 | # PRO Version
95 |
96 | Unlock a huge amount of components and pages with our PRO version - Learn more
97 |
98 |
99 |
100 |
101 |
102 |
103 | ---
104 |
105 | # Reporting Issues
106 |
107 | We use GitHub Issues as the official bug tracker for the Horizon UI. Here are
108 | some advice for our users who want to report an issue:
109 |
110 | 1. Make sure that you are using the latest version of the Horizon UI Boilerplate. Check the CHANGELOG for your dashboard on our [CHANGE LOG File](https://github.com/horizon-ui/shadcn-nextjs-boilerplate/blob/main/CHANGELOG.md).
111 |
112 |
113 | 1. Providing us with reproducible steps for the issue will shorten the time it takes for it to be fixed.
114 |
115 |
116 |
117 | 3. Some issues may be browser-specific, so specifying in what browser you encountered the issue might help.
118 |
119 | ---
120 |
121 | # Community
122 |
123 | Connect with the community! Feel free to ask questions, report issues, and meet new people who already use Horizon UI!
124 |
125 | 💬 [Join the #HorizonUI Discord Community!](https://discord.gg/f6tEKFBd4m)
126 |
127 |
128 | ### Copyright and license
129 |
130 | ⭐️ [Copyright 2024 Horizon UI](https://www.horizon-ui.com/?ref=readme-horizon)
131 |
132 | 📄 [Horizon UI License](https://horizon-ui.notion.site/End-User-License-Agreement-8fb09441ea8c4c08b60c37996195a6d5)
133 |
134 |
135 | ---
136 |
137 | # Credits
138 |
139 | Special thanks to the open-source resources that helped us create this awesome boilerplate package, including:
140 |
141 | - [Shadcn UI Library](https://ui.shadcn.com/)
142 | - [NextJS Subscription Payments](https://github.com/vercel/nextjs-subscription-payments)
143 | - [ChatBot UI by mckaywrigley](https://github.com/mckaywrigley/chatbot-ui)
144 |
--------------------------------------------------------------------------------
/app/api/chatAPI/route.ts:
--------------------------------------------------------------------------------
1 | import { ChatBody } from '@/types/types';
2 | import { OpenAIStream } from '@/utils/streams/chatStream';
3 |
4 | export const runtime = 'edge';
5 |
6 | export async function GET(req: Request): Promise {
7 | try {
8 | const { inputMessage, model, apiKey } = (await req.json()) as ChatBody;
9 |
10 | let apiKeyFinal;
11 | if (apiKey) {
12 | apiKeyFinal = apiKey;
13 | } else {
14 | apiKeyFinal = process.env.NEXT_PUBLIC_OPENAI_API_KEY;
15 | }
16 |
17 | const stream = await OpenAIStream(inputMessage, model, apiKeyFinal);
18 |
19 | return new Response(stream);
20 | } catch (error) {
21 | console.error(error);
22 | return new Response('Error', { status: 500 });
23 | }
24 | }
25 | export async function POST(req: Request): Promise {
26 | try {
27 | const { inputMessage, model, apiKey } = (await req.json()) as ChatBody;
28 |
29 | let apiKeyFinal;
30 | if (apiKey) {
31 | apiKeyFinal = apiKey;
32 | } else {
33 | apiKeyFinal = process.env.NEXT_PUBLIC_OPENAI_API_KEY;
34 | }
35 |
36 | const stream = await OpenAIStream(inputMessage, model, apiKeyFinal);
37 |
38 | return new Response(stream);
39 | } catch (error) {
40 | console.error(error);
41 | return new Response('Error', { status: 500 });
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/api/essayAPI/route.ts:
--------------------------------------------------------------------------------
1 | import { EssayBody } from '@/types/types';
2 | import { OpenAIStream } from '@/utils/streams/essayStream';
3 |
4 | export const runtime = 'edge';
5 |
6 | export async function GET(req: Request): Promise {
7 | try {
8 | const {
9 | topic,
10 | words,
11 | essayType,
12 | model,
13 | apiKey
14 | } = (await req.json()) as EssayBody;
15 |
16 | let apiKeyFinal;
17 | if (apiKey) {
18 | apiKeyFinal = apiKey;
19 | } else {
20 | apiKeyFinal = process.env.NEXT_PUBLIC_OPENAI_API_KEY;
21 | }
22 |
23 | const stream = await OpenAIStream(
24 | topic,
25 | essayType,
26 | words,
27 | model,
28 | apiKeyFinal
29 | );
30 |
31 | return new Response(stream);
32 | } catch (error) {
33 | console.error(error);
34 | return new Response('Error', { status: 500 });
35 | }
36 | }
37 | export async function POST(req: Request): Promise {
38 | try {
39 | const {
40 | topic,
41 | words,
42 | essayType,
43 | model,
44 | apiKey
45 | } = (await req.json()) as EssayBody;
46 |
47 | let apiKeyFinal;
48 | if (apiKey) {
49 | apiKeyFinal = apiKey;
50 | } else {
51 | apiKeyFinal = process.env.NEXT_PUBLIC_OPENAI_API_KEY;
52 | }
53 |
54 | const stream = await OpenAIStream(
55 | topic,
56 | essayType,
57 | words,
58 | model,
59 | apiKeyFinal
60 | );
61 |
62 | return new Response(stream);
63 | } catch (error) {
64 | console.error(error);
65 | return new Response('Error', { status: 500 });
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/api/webhooks/route.ts:
--------------------------------------------------------------------------------
1 | import Stripe from 'stripe';
2 | import { stripe } from '@/utils/stripe/config';
3 | import {
4 | upsertProductRecord,
5 | upsertPriceRecord,
6 | manageSubscriptionStatusChange
7 | } from '@/utils/supabase-admin';
8 | import { headers } from 'next/headers';
9 |
10 | const relevantEvents = new Set([
11 | 'product.created',
12 | 'product.updated',
13 | 'price.created',
14 | 'price.updated',
15 | 'checkout.session.completed',
16 | 'customer.subscription.created',
17 | 'customer.subscription.updated',
18 | 'customer.subscription.deleted'
19 | ]);
20 |
21 | export async function POST(req: Request) {
22 | const body = await req.text();
23 | const sig = headers().get('Stripe-Signature') as string;
24 | const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
25 | let event: Stripe.Event;
26 |
27 | try {
28 | if (!sig || !webhookSecret) return;
29 | event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
30 | } catch (err) {
31 | console.log(`❌ Error message: ${err.message}`);
32 | return new Response(`Webhook Error: ${err.message}`, { status: 400 });
33 | }
34 |
35 | if (relevantEvents.has(event.type)) {
36 | try {
37 | switch (event.type) {
38 | case 'product.created':
39 | case 'product.updated':
40 | await upsertProductRecord(event.data.object as Stripe.Product);
41 | break;
42 | case 'price.created':
43 | case 'price.updated':
44 | await upsertPriceRecord(event.data.object as Stripe.Price);
45 | break;
46 | case 'customer.subscription.created':
47 | case 'customer.subscription.updated':
48 | case 'customer.subscription.deleted':
49 | const subscription = event.data.object as Stripe.Subscription;
50 | await manageSubscriptionStatusChange(
51 | subscription.id,
52 | subscription.customer as string,
53 | event.type === 'customer.subscription.created'
54 | );
55 | break;
56 | case 'checkout.session.completed':
57 | const checkoutSession = event.data.object as Stripe.Checkout.Session;
58 | if (checkoutSession.mode === 'subscription') {
59 | const subscriptionId = checkoutSession.subscription;
60 | await manageSubscriptionStatusChange(
61 | subscriptionId as string,
62 | checkoutSession.customer as string,
63 | true
64 | );
65 | }
66 | break;
67 | default:
68 | throw new Error('Unhandled relevant event!');
69 | }
70 | } catch (error) {
71 | console.log(error);
72 | return new Response(
73 | 'Webhook handler failed. View your nextjs function logs.',
74 | {
75 | status: 400
76 | }
77 | );
78 | }
79 | }
80 | return new Response(JSON.stringify({ received: true }));
81 | }
82 |
--------------------------------------------------------------------------------
/app/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@/utils/supabase/server';
2 | import { NextResponse } from 'next/server';
3 | import { NextRequest } from 'next/server';
4 | import { getErrorRedirect, getStatusRedirect } from '@/utils/helpers';
5 |
6 | export async function GET(request: NextRequest) {
7 | // The `/auth/callback` route is required for the server-side auth flow implemented
8 | // by the `@supabase/ssr` package. It exchanges an auth code for the user's session.
9 | const requestUrl = new URL(request.url);
10 | const code = requestUrl.searchParams.get('code');
11 |
12 | if (code) {
13 | const supabase = createClient();
14 |
15 | const { error } = await supabase.auth.exchangeCodeForSession(code);
16 |
17 | if (error) {
18 | return NextResponse.redirect(
19 | getErrorRedirect(
20 | `${requestUrl.origin}/dashboard/signin`,
21 | error.name,
22 | "Sorry, we weren't able to log you in. Please try again."
23 | )
24 | );
25 | }
26 | }
27 |
28 | // URL to redirect to after sign in process completes
29 | return NextResponse.redirect(
30 | getStatusRedirect(
31 | `${requestUrl.origin}/dashboard/main`,
32 | 'Success!',
33 | 'You are now signed in.'
34 | )
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/app/auth/reset_password/route.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '@/utils/supabase/server';
2 | import { NextResponse } from 'next/server';
3 | import { NextRequest } from 'next/server';
4 | import { getErrorRedirect, getStatusRedirect } from '@/utils/helpers';
5 |
6 | export async function GET(request: NextRequest) {
7 | // The `/auth/callback` route is required for the server-side auth flow implemented
8 | // by the `@supabase/ssr` package. It exchanges an auth code for the user's session.
9 | const requestUrl = new URL(request.url);
10 | const code = requestUrl.searchParams.get('code');
11 |
12 | if (code) {
13 | const supabase = createClient();
14 |
15 | const { error } = await supabase.auth.exchangeCodeForSession(code);
16 |
17 | if (error) {
18 | return NextResponse.redirect(
19 | getErrorRedirect(
20 | `${requestUrl.origin}/signin/forgot_password`,
21 | error.name,
22 | "Sorry, we weren't able to log you in. Please try again."
23 | )
24 | );
25 | }
26 | }
27 |
28 | // URL to redirect to after sign in process completes
29 | return NextResponse.redirect(
30 | getStatusRedirect(
31 | `${requestUrl.origin}/signin/update_password`,
32 | 'You are now signed in.',
33 | 'Please enter a new password for your account.'
34 | )
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/app/dashboard/ai-chat/page.tsx:
--------------------------------------------------------------------------------
1 | import { getUserDetails, getUser } from '@/utils/supabase/queries';
2 |
3 | import Chat from '@/components/dashboard/ai-chat';
4 | import { redirect } from 'next/navigation';
5 | import { createClient } from '@/utils/supabase/server';
6 |
7 | export default async function AiChat() {
8 | const supabase = createClient();
9 | const [user, userDetails] = await Promise.all([
10 | getUser(supabase),
11 | getUserDetails(supabase)
12 | ]);
13 |
14 | if (!user) {
15 | return redirect('/dashboard/signin');
16 | }
17 |
18 | return ;
19 | }
20 |
--------------------------------------------------------------------------------
/app/dashboard/main/page.tsx:
--------------------------------------------------------------------------------
1 | import Main from '@/components/dashboard/main';
2 | import { redirect } from 'next/navigation';
3 | import { getUserDetails, getUser } from '@/utils/supabase/queries';
4 | import { createClient } from '@/utils/supabase/server';
5 |
6 | export default async function Account() {
7 | const supabase = createClient();
8 | const [user, userDetails] = await Promise.all([
9 | getUser(supabase),
10 | getUserDetails(supabase)
11 | ]);
12 |
13 | if (!user) {
14 | return redirect('/dashboard/signin');
15 | }
16 |
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { getUser } from '@/utils/supabase/queries';
2 | import { redirect } from 'next/navigation';
3 | import { createClient } from '@/utils/supabase/server';
4 |
5 | export default async function Dashboard() {
6 | const supabase = createClient();
7 | const [user] = await Promise.all([getUser(supabase)]);
8 |
9 | if (!user) {
10 | return redirect('/dashboard/signin');
11 | } else {
12 | redirect('/dashboard/main');
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/dashboard/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import Settings from '@/components/dashboard/settings';
2 | import { redirect } from 'next/navigation';
3 | import { createClient } from '@/utils/supabase/server';
4 | import { getUserDetails, getUser } from '@/utils/supabase/queries';
5 |
6 | export default async function SettingsPage() {
7 | const supabase = createClient();
8 | const [user, userDetails] = await Promise.all([
9 | getUser(supabase),
10 | getUserDetails(supabase)
11 | ]);
12 | if (!user) {
13 | return redirect('/dashboard/signin');
14 | }
15 |
16 | return ;
17 | }
18 |
--------------------------------------------------------------------------------
/app/dashboard/signin/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import DefaultAuth from '@/components/auth';
2 | import AuthUI from '@/components/auth/AuthUI';
3 | import { redirect } from 'next/navigation';
4 | import { createClient } from '@/utils/supabase/server';
5 | import { cookies } from 'next/headers';
6 | import {
7 | getAuthTypes,
8 | getViewTypes,
9 | getDefaultSignInView,
10 | getRedirectMethod
11 | } from '@/utils/auth-helpers/settings';
12 |
13 | export default async function SignIn({
14 | params,
15 | searchParams
16 | }: {
17 | params: { id: string };
18 | searchParams: { disable_button: boolean };
19 | }) {
20 | const { allowOauth, allowEmail, allowPassword } = getAuthTypes();
21 | const viewTypes = getViewTypes();
22 | const redirectMethod = getRedirectMethod();
23 |
24 | // Declare 'viewProp' and initialize with the default value
25 | let viewProp: string;
26 |
27 | // Assign url id to 'viewProp' if it's a valid string and ViewTypes includes it
28 | if (typeof params.id === 'string' && viewTypes.includes(params.id)) {
29 | viewProp = params.id;
30 | } else {
31 | const preferredSignInView =
32 | cookies().get('preferredSignInView')?.value || null;
33 | viewProp = getDefaultSignInView(preferredSignInView);
34 | return redirect(`/dashboard/signin/${viewProp}`);
35 | }
36 |
37 | // Check if the user is already logged in and redirect to the account page if so
38 | const supabase = createClient();
39 |
40 | const {
41 | data: { user }
42 | } = await supabase.auth.getUser();
43 |
44 | if (user && viewProp !== 'update_password') {
45 | return redirect('/dashboard/main');
46 | } else if (!user && viewProp === 'update_password') {
47 | return redirect('/dashboard/signin');
48 | }
49 |
50 | return (
51 |
52 |