├── .eslintrc.json
├── .gitignore
├── README.md
├── app
├── [keyword]
│ ├── keywordConfig.ts
│ ├── keywords.ts
│ └── page.tsx
├── blog
│ ├── [articleId]
│ │ └── page.tsx
│ ├── _assets
│ │ ├── components
│ │ │ ├── Avatar.tsx
│ │ │ ├── BadgeCategory.tsx
│ │ │ ├── CardArticle.tsx
│ │ │ └── CardCategory.tsx
│ │ └── content.tsx
│ ├── category
│ │ └── [categoryId]
│ │ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── accordion-features.tsx
│ ├── before-after.tsx
│ ├── button.tsx
│ ├── container.tsx
│ ├── cta.tsx
│ ├── faq.tsx
│ ├── footer.tsx
│ ├── header.tsx
│ ├── hero.tsx
│ ├── logo-clouds.tsx
│ ├── nav-links.tsx
│ ├── pricing.tsx
│ ├── svg-logo.tsx
│ ├── testimonial-single.tsx
│ └── testimonials-avatars.tsx
├── favicon.ico
├── fonts
│ ├── GeistMonoVF.woff
│ └── GeistVF.woff
├── globals.css
├── layout.tsx
├── lib
│ └── seo.tsx
└── page.tsx
├── config.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── public
├── artem.png
├── cover.png
├── feature_1.gif
├── feature_2.png
└── feature_3.gif
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Learning Next.js Series
2 |
3 | This repository contains the code for a YouTube video series dedicated to learning Next.js. Follow along as we learn together.
4 |
5 | ## 🎥 Video Series
6 |
7 | 1. [Building Beautiful Landing Pages with Next.js](https://www.youtube.com/watch?v=u8itgg8216k)
8 | 2. [SEO Best Practices in Next.js. SEO Part 1](https://www.youtube.com/watch?v=_w5Jn2sAJXw)
9 | 3. [Creating Unique Landing Pages for Different Keywords. SEO Part 2](https://www.youtube.com/watch?v=IMUGTOiyhWk)
10 |
11 | ## 🚀 Getting Started
12 |
13 | First, run the development server:
14 |
15 | ```bash
16 | npm run dev
17 | # or
18 | yarn dev
19 | # or
20 | pnpm dev
21 | # or
22 | bun dev
23 | ```
24 |
25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
26 |
27 | ## 🛠 Tech Stack
28 |
29 | - [Next.js](https://nextjs.org/)
30 | - [React](https://reactjs.org/)
31 | - [Vercel](https://vercel.com/) for deployment
32 |
33 | ## 📚 Learn More
34 |
35 | To dive deeper into Next.js, check out the following resources:
36 |
37 | - [Next.js Documentation](https://nextjs.org/docs)
38 | - [Learn Next.js](https://nextjs.org/learn)
39 |
40 | ## 📄 License
41 |
42 | This project is [MIT](https://choosealicense.com/licenses/mit/) licensed.
43 |
--------------------------------------------------------------------------------
/app/[keyword]/keywordConfig.ts:
--------------------------------------------------------------------------------
1 | export type ComponentConfig = {
2 | [key: string]: any;
3 | };
4 |
5 | export type KeywordConfig = {
6 | [component: string]: ComponentConfig;
7 | };
8 |
9 | const keywordConfigs: { [keyword: string]: KeywordConfig } = {
10 | "ai-quiz-creation": {
11 | Hero: {
12 | title: "AI-Powered Quiz Creation",
13 | subtitle: "Create engaging quizzes in minutes with AI assistance",
14 | },
15 | AccordionFeatures: {
16 | features: [
17 | { title: "Automatic Question Generation", description: "..." },
18 | { title: "Customizable Difficulty Levels", description: "..." },
19 | ],
20 | },
21 | BeforeAfter: {
22 | before: "Hours spent creating quizzes manually",
23 | after: "Minutes to generate AI-powered quizzes",
24 | },
25 | FAQ: {
26 | questions: [
27 | { question: "How does AI quiz creation work?", answer: "..." },
28 | { question: "Can I customize the generated quizzes?", answer: "..." },
29 | ],
30 | },
31 | CTA: {
32 | title: "Start Creating AI-Powered Quizzes Today",
33 | buttonText: "Try AI Quiz Creator",
34 | },
35 | },
36 | "ai-lesson-planning": {
37 | Hero: {
38 | title: "AI-Powered Lesson Planning",
39 | subtitle: "Create engaging lessons in minutes with AI assistance",
40 | },
41 | },
42 | };
43 |
44 | export default keywordConfigs;
45 |
--------------------------------------------------------------------------------
/app/[keyword]/keywords.ts:
--------------------------------------------------------------------------------
1 | export const keywords = [
2 | "ai lesson planning",
3 | "ai quiz creation",
4 | "ai flashcards creation",
5 | "ai assignment creation",
6 | "lesson-plan-generator",
7 | "quiz-generator",
8 | "flashcards",
9 | "assignment-creator",
10 | ];
11 |
--------------------------------------------------------------------------------
/app/[keyword]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from "@/app/components/container";
2 | import Hero from "@/app/components/hero";
3 | import LogoClouds from "@/app/components/logo-clouds";
4 | import { Header } from "@/app/components/header";
5 | import BeforeAfter from "@/app/components/before-after";
6 | import AccordionFeatures from "@/app/components/accordion-features";
7 | import Pricing from "@/app/components/pricing";
8 | import FAQ from "@/app/components/faq";
9 | import Footer from "@/app/components/footer";
10 | import CTA from "@/app/components/cta";
11 | import { getSEOTags } from "@/app/lib/seo";
12 | import { keywords } from "./keywords";
13 | import { redirect } from "next/navigation";
14 | import keywordConfigs from "./keywordConfig";
15 |
16 | export const generateMetadata = ({
17 | params,
18 | }: {
19 | params: { keyword: string };
20 | }) => {
21 | return getSEOTags({
22 | title: `Quillminds for ${params.keyword}`,
23 | description: `Quillminds helps with ${params.keyword}. Create lesson plans, quizzes, and more.`,
24 | canonicalUrlRelative: `/${params.keyword}`,
25 | });
26 | };
27 |
28 | export async function generateStaticParams() {
29 | return keywords.map((keyword) => ({
30 | keyword: keyword.replace(/\s+/g, "-").toLowerCase(),
31 | }));
32 | }
33 | function isValidKeyword(keyword: string): boolean {
34 | return keywords
35 | .map((k) => k.replace(/\s+/g, "-").toLowerCase())
36 | .includes(keyword.toLowerCase());
37 | }
38 |
39 | export default function KeywordPage({
40 | params,
41 | }: {
42 | params: { keyword: string };
43 | }) {
44 | if (!isValidKeyword(params.keyword)) {
45 | return redirect("/");
46 | }
47 | //Use the decoded keyword when needed
48 | const decodedKeyword = decodeURIComponent(params.keyword).replace(/-/g, " ");
49 | const config = keywordConfigs[params.keyword] || {};
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/app/blog/[articleId]/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Script from "next/script";
3 | import { articles } from "../_assets/content";
4 | import BadgeCategory from "../_assets/components/BadgeCategory";
5 | import Avatar from "../_assets/components/Avatar";
6 | import { getSEOTags } from "@/app/lib/seo";
7 | import config from "@/config";
8 |
9 | export async function generateMetadata({
10 | params,
11 | }: {
12 | params: { articleId: string };
13 | }) {
14 | const article = articles.find((article) => article.slug === params.articleId);
15 | if (!article) {
16 | return {};
17 | }
18 | return getSEOTags({
19 | title: article.title,
20 | description: article.description,
21 | canonicalUrlRelative: `/blog/${article.slug}`,
22 | openGraph: {
23 | title: article.title,
24 | description: article.description,
25 | url: `/blog/${article.slug}`,
26 | images: [
27 | {
28 | url: article.image.urlRelative,
29 | width: 1200,
30 | height: 660,
31 | },
32 | ],
33 | locale: "en_US",
34 | type: "website",
35 | },
36 | });
37 | }
38 |
39 | export default async function Article({
40 | params,
41 | }: {
42 | params: { articleId: string };
43 | }) {
44 | const article = articles.find((article) => article.slug === params.articleId);
45 | const articlesRelated = articles
46 | .filter(
47 | (a) =>
48 | a.slug !== params.articleId &&
49 | a.categories.some((c) =>
50 | article?.categories.map((c) => c.slug).includes(c.slug)
51 | )
52 | )
53 | .sort(
54 | (a, b) =>
55 | new Date(b.publishedAt).valueOf() - new Date(a.publishedAt).valueOf()
56 | )
57 | .slice(0, 3);
58 |
59 | return (
60 | <>
61 | {/* SCHEMA JSON-LD MARKUP FOR GOOGLE */}
62 | {article && (
63 |
87 | )}
88 |
89 | {/* GO BACK LINK */}
90 |
91 |
96 |
102 |
107 |
108 | Back to Blog
109 |
110 |
111 |
112 |
113 | {/* HEADER WITH CATEGORIES AND DATE AND TITLE */}
114 | {article && (
115 | <>
116 |
117 |
118 | {article.categories.map((category) => (
119 |
120 | ))}
121 |
122 | {new Date(article.publishedAt).toLocaleDateString("en-US", {
123 | month: "long",
124 | day: "numeric",
125 | year: "numeric",
126 | })}
127 |
128 |
129 |
130 |
131 | {article.title}
132 |
133 |
134 |
135 | {article.description}
136 |
137 |
138 |
139 |
140 | {/* SIDEBAR WITH AUTHORS AND 3 RELATED ARTICLES */}
141 |
142 | Posted by
143 |
144 |
145 | {articlesRelated.length > 0 && (
146 |
147 |
148 | Related reading
149 |
150 |
151 | {articlesRelated.map((article) => (
152 |
153 |
154 |
160 | {article.title}
161 |
162 |
163 |
164 | {article.description}
165 |
166 |
167 | ))}
168 |
169 |
170 | )}
171 |
172 |
173 | {/* ARTICLE CONTENT */}
174 |
175 | {article.content}
176 |
177 |
178 | >
179 | )}
180 |
181 | >
182 | );
183 | }
184 |
--------------------------------------------------------------------------------
/app/blog/_assets/components/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 | import { articleType } from "../content";
4 |
5 | // This is the author avatar that appears in the article page and in component
6 | const Avatar = ({ article }: { article: articleType }) => {
7 | return (
8 |
14 |
15 |
23 |
24 | {article.author.name}
25 |
26 | );
27 | };
28 |
29 | export default Avatar;
30 |
--------------------------------------------------------------------------------
/app/blog/_assets/components/BadgeCategory.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { categoryType } from "../content";
3 |
4 | // This is the category badge that appears in the article page and in component
5 | const Category = ({
6 | category,
7 | extraStyle,
8 | }: {
9 | category: categoryType;
10 | extraStyle?: string;
11 | }) => {
12 | return (
13 |
21 | {category.titleShort}
22 |
23 | );
24 | };
25 |
26 | export default Category;
27 |
--------------------------------------------------------------------------------
/app/blog/_assets/components/CardArticle.tsx:
--------------------------------------------------------------------------------
1 | import type { JSX } from "react";
2 | import Link from "next/link";
3 | import Image from "next/image";
4 | import BadgeCategory from "./BadgeCategory";
5 | import Avatar from "./Avatar";
6 | import { articleType } from "../content";
7 |
8 | // This is the article card that appears in the home page, in the category page, and in the author's page
9 | const CardArticle = ({
10 | article,
11 | tag = "h2",
12 | showCategory = true,
13 | isImagePriority = false,
14 | }: {
15 | article: articleType;
16 | tag?: keyof JSX.IntrinsicElements;
17 | showCategory?: boolean;
18 | isImagePriority?: boolean;
19 | }) => {
20 | const TitleTag = tag;
21 |
22 | return (
23 |
24 | {article.image?.src && (
25 |
31 |
32 |
41 |
42 |
43 | )}
44 |
45 | {/* CATEGORIES */}
46 | {showCategory && (
47 |
48 | {article.categories.map((category) => (
49 |
50 | ))}
51 |
52 | )}
53 |
54 | {/* TITLE WITH RIGHT TAG */}
55 |
56 |
62 | {article.title}
63 |
64 |
65 |
66 |
67 | {/* DESCRIPTION */}
68 |
{article.description}
69 |
70 | {/* AUTHOR & DATE */}
71 |
72 |
73 |
74 |
75 | {new Date(article.publishedAt).toLocaleDateString("en-US", {
76 | month: "long",
77 | day: "numeric",
78 | })}
79 |
80 |
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export default CardArticle;
88 |
--------------------------------------------------------------------------------
/app/blog/_assets/components/CardCategory.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import type { JSX } from "react";
3 | import Link from "next/link";
4 | import { categoryType } from "../content";
5 |
6 | // This is the category card that appears in the home page and in the category page
7 | import { usePathname } from "next/navigation";
8 |
9 | const CardCategory = ({
10 | category,
11 | tag = "h2",
12 | }: {
13 | category: categoryType;
14 | tag?: keyof JSX.IntrinsicElements;
15 | }) => {
16 | const TitleTag = tag;
17 | const pathname = usePathname();
18 | const isActive = pathname.includes(`/blog/category/${category.slug}`);
19 |
20 | return (
21 |
31 |
36 | {category?.titleShort || category.title}
37 |
38 |
43 |
50 |
56 |
57 |
{0} articles
58 |
59 |
60 | );
61 | };
62 |
63 | export default CardCategory;
64 |
--------------------------------------------------------------------------------
/app/blog/_assets/content.tsx:
--------------------------------------------------------------------------------
1 | import type { JSX } from "react";
2 | import Image, { StaticImageData } from "next/image";
3 |
4 | import artemImg from "../../../public/artem.png";
5 | import quillmindsImg from "../../../public/cover.png";
6 |
7 | // ==================================================================================================================================================================
8 | // BLOG CATEGORIES 🏷️
9 | // ==================================================================================================================================================================
10 |
11 | export type categoryType = {
12 | slug: string;
13 | title: string;
14 | titleShort?: string;
15 | description: string;
16 | descriptionShort?: string;
17 | };
18 |
19 | // These slugs are used to generate pages in the /blog/category/[categoryI].js. It's a way to group articles by category.
20 | const categorySlugs: { [key: string]: string } = {
21 | feature: "feature",
22 | tutorial: "tutorial",
23 | };
24 |
25 | // All the blog categories data display in the /blog/category/[categoryI].js pages.
26 | export const categories: categoryType[] = [
27 | {
28 | // The slug to use in the URL, from the categorySlugs object above.
29 | slug: categorySlugs.feature,
30 | // The title to display the category title (h1), the category badge, the category filter, and more. Less than 60 characters.
31 | title: "New Features",
32 | // A short version of the title above, display in small components like badges. 1 or 2 words
33 | titleShort: "Features",
34 | // The description of the category to display in the category page. Up to 160 characters.
35 | description:
36 | "Here are the latest features we've added to Quillminds. I'm constantly improving our product to help you teach better.",
37 | // A short version of the description above, only displayed in the on mobile. Up to 60 characters.
38 | descriptionShort: "Latest features added to Quillminds.",
39 | },
40 | {
41 | slug: categorySlugs.tutorial,
42 | title: "How Tos & Tutorials",
43 | titleShort: "Tutorials",
44 | description:
45 | "Learn how to use Quillminds with these step-by-step tutorials. I'll show you how to teach better and save time.",
46 | descriptionShort:
47 | "Learn how to use Quillminds with these step-by-step tutorials.",
48 | },
49 | ];
50 |
51 | // ==================================================================================================================================================================
52 | // BLOG AUTHORS 📝
53 | // ==================================================================================================================================================================
54 |
55 | export type authorType = {
56 | slug: string;
57 | name: string;
58 | job: string;
59 | description: string;
60 | avatar: StaticImageData | string;
61 | socials?: {
62 | name: string;
63 | icon: JSX.Element;
64 | url: string;
65 | }[];
66 | };
67 |
68 | // Social icons used in the author's bio.
69 | const socialIcons: {
70 | [key: string]: {
71 | name: string;
72 | svg: JSX.Element;
73 | };
74 | } = {
75 | twitter: {
76 | name: "Twitter",
77 | svg: (
78 | className="w-9 h-9 fill-white"
86 | >
87 |
88 |
92 |
93 |
94 | ),
95 | },
96 | linkedin: {
97 | name: "LinkedIn",
98 | svg: (
99 | className="w-6 h-6 fill-white"
103 | viewBox="0 0 24 24"
104 | >
105 |
106 |
107 | ),
108 | },
109 | github: {
110 | name: "GitHub",
111 | svg: (
112 | className="w-6 h-6 fill-white"
116 | viewBox="0 0 24 24"
117 | >
118 |
119 |
120 | ),
121 | },
122 | };
123 |
124 | // These slugs are used to generate pages in the /blog/author/[authorId].js. It's a way to show all articles from an author.
125 | const authorSlugs: {
126 | [key: string]: string;
127 | } = {
128 | marc: "artem",
129 | };
130 |
131 | // All the blog authors data display in the /blog/author/[authorId].js pages.
132 | export const authors: authorType[] = [
133 | {
134 | // The slug to use in the URL, from the authorSlugs object above.
135 | slug: authorSlugs.marc,
136 | // The name to display in the author's bio. Up to 60 characters.
137 | name: "Artem Kirsanov",
138 | // The job to display in the author's bio. Up to 60 characters.
139 | job: "Maker of Quillminds",
140 | // The description of the author to display in the author's bio. Up to 160 characters.
141 | description:
142 | "Artem is a developer and an entrepreneur. He's built 3 startups in the last years.He's currently building Quillminds, the #1 Stripe AI tool for teachers.",
143 | // The avatar of the author to display in the author's bio and avatar badge. It's better to use a local image, but you can also use an external image (https://...)
144 | avatar: artemImg,
145 | // A list of social links to display in the author's bio.
146 | socials: [
147 | {
148 | name: socialIcons.twitter.name,
149 | icon: socialIcons.twitter.svg,
150 | url: "https://twitter.com/kirsnvartem",
151 | },
152 | {
153 | name: socialIcons.linkedin.name,
154 | icon: socialIcons.linkedin.svg,
155 | url: "https://www.linkedin.com/in/kirsnvartem/",
156 | },
157 | {
158 | name: socialIcons.github.name,
159 | icon: socialIcons.github.svg,
160 | url: "https://github.com/kirsanow",
161 | },
162 | ],
163 | },
164 | ];
165 |
166 | // ==================================================================================================================================================================
167 | // BLOG ARTICLES 📚
168 | // ==================================================================================================================================================================
169 |
170 | export type articleType = {
171 | slug: string;
172 | title: string;
173 | description: string;
174 | categories: categoryType[];
175 | author: authorType;
176 | publishedAt: string;
177 | image: {
178 | src?: StaticImageData;
179 | urlRelative: string;
180 | alt: string;
181 | };
182 | content: JSX.Element;
183 | };
184 |
185 | // These styles are used in the content of the articles. When you update them, all articles will be updated.
186 | const styles: {
187 | [key: string]: string;
188 | } = {
189 | h2: "text-2xl lg:text-4xl font-bold tracking-tight mb-4 text-slate-800",
190 | h3: "text-xl lg:text-2xl font-bold tracking-tight mb-2 text-slate-800",
191 | p: "text-slate-700 leading-relaxed",
192 | ul: "list-inside list-disc text-slate-700 leading-relaxed",
193 | li: "list-item",
194 | // Altnernatively, you can use the library react-syntax-highlighter to display code snippets.
195 | code: "text-sm font-mono bg-slate-50 text-slate-900 p-6 rounded-box my-4 overflow-x-scroll select-all",
196 | codeInline:
197 | "text-sm font-mono bg-slate-50 px-1 py-0.5 rounded-box select-all",
198 | };
199 |
200 | // All the blog articles data display in the /blog/[articleId].js pages.
201 | export const articles: articleType[] = [
202 | {
203 | slug: "ai-tools-for-teaching",
204 | title: "Revolutionizing Education: AI Tools for Teaching",
205 | description:
206 | "Discover how AI is transforming the educational landscape. Learn about innovative tools that can enhance your teaching methods and engage students like never before.",
207 | categories: [
208 | categories.find(
209 | (category) => category.slug === categorySlugs.feature
210 | ) as categoryType,
211 | ],
212 | author: authors.find(
213 | (author) => author.slug === authorSlugs.marc
214 | ) as authorType,
215 | publishedAt: "2023-11-20",
216 | image: {
217 | src: quillmindsImg,
218 | urlRelative: "/cover.png",
219 | alt: "Quillminds",
220 | },
221 | content: (
222 | <>
223 |
231 |
232 | Introduction
233 |
234 | Artificial Intelligence is reshaping the educational landscape,
235 | offering innovative tools that can significantly enhance teaching
236 | methods and student engagement. This article explores some of the
237 | most promising AI tools for educators.
238 |
239 |
240 |
241 |
242 | 1. AI-Powered Lesson Planning
243 |
244 | One of the most time-consuming tasks for teachers is lesson
245 | planning. AI tools can now assist in creating personalized lesson
246 | plans based on curriculum requirements and student needs. For
247 | example, Quillminds offers:
248 |
249 |
250 |
251 | Customizable lesson templates
252 |
253 | Content suggestions based on learning objectives
254 |
255 |
256 | Automatic alignment with educational standards
257 |
258 |
259 |
260 |
261 |
262 | 2. Intelligent Tutoring Systems
263 |
264 | AI-driven tutoring systems can provide personalized learning
265 | experiences for students. These systems adapt to each student's
266 | pace and learning style, offering:
267 |
268 |
269 |
270 | Real-time feedback on assignments
271 | Adaptive quizzes and exercises
272 |
273 | Progress tracking and performance analytics
274 |
275 |
276 |
277 |
278 |
279 |
280 | 3. Natural Language Processing for Education
281 |
282 |
283 | Natural Language Processing (NLP) technologies are being used to
284 | enhance various aspects of education, including:
285 |
286 |
287 |
288 | Automated essay grading
289 | Language learning applications
290 |
291 | Text-to-speech and speech-to-text tools for accessibility
292 |
293 |
294 |
295 | >
296 | ),
297 | },
298 | ];
299 |
--------------------------------------------------------------------------------
/app/blog/category/[categoryId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { categories, articles } from "../../_assets/content";
2 | import CardArticle from "../../_assets/components/CardArticle";
3 | import CardCategory from "../../_assets/components/CardCategory";
4 | import { getSEOTags } from "@/app/lib/seo";
5 | import config from "@/config";
6 |
7 | export async function generateMetadata({
8 | params,
9 | }: {
10 | params: { categoryId: string };
11 | }) {
12 | const category = categories.find(
13 | (category) => category.slug === params.categoryId
14 | );
15 |
16 | return getSEOTags({
17 | title: `${category?.title} | Blog by ${config.appName}`,
18 | description: category?.description,
19 | canonicalUrlRelative: `/blog/category/${category?.slug}`,
20 | });
21 | }
22 |
23 | export default async function Category({
24 | params,
25 | }: {
26 | params: { categoryId: string };
27 | }) {
28 | const category = categories.find(
29 | (category) => category.slug === params.categoryId
30 | );
31 | const articlesInCategory = articles
32 | .filter((article) =>
33 | article.categories.map((c) => c.slug).includes(category?.slug || "")
34 | )
35 | .sort(
36 | (a, b) =>
37 | new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
38 | )
39 | .slice(0, 3);
40 |
41 | return (
42 | <>
43 |
44 |
45 | {category?.title}
46 |
47 |
48 | {category?.description}
49 |
50 |
51 |
52 |
53 |
54 | Most recent articles in {category?.title}
55 |
56 |
57 |
58 | {articlesInCategory.map((article) => (
59 |
65 | ))}
66 |
67 |
68 |
69 |
70 |
71 | Other categories you might like
72 |
73 |
74 |
75 | {categories
76 | .filter((c) => c.slug !== category?.slug)
77 | .map((category) => (
78 |
79 | ))}
80 |
81 |
82 | >
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/app/blog/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import Footer from "@/app/components/footer";
3 | import { Header } from "../components/header";
4 |
5 | export default async function LayoutBlog({
6 | children,
7 | }: {
8 | children: React.ReactNode;
9 | }) {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
{children}
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/app/blog/page.tsx:
--------------------------------------------------------------------------------
1 | import { categories, articles } from "./_assets/content";
2 | import CardArticle from "./_assets/components/CardArticle";
3 | import CardCategory from "./_assets/components/CardCategory";
4 | import config from "@/config";
5 | import { getSEOTags } from "@/app/lib/seo";
6 |
7 | export const metadata = getSEOTags({
8 | title: `${config.appName} Blog | AI for Teachers`,
9 | description: "Learn how to leverage AI for your teaching",
10 | canonicalUrlRelative: "/blog",
11 | });
12 |
13 | export default async function Blog() {
14 | const articlesToDisplay = articles
15 | .sort(
16 | (a, b) =>
17 | new Date(b.publishedAt).valueOf() - new Date(a.publishedAt).valueOf()
18 | )
19 | .slice(0, 6);
20 | return (
21 | <>
22 |
23 |
24 | The {config.appName} Blog
25 |
26 |
27 | Get the best tips about how to leverage AI for your teaching
28 |
29 |
30 |
31 |
32 | {articlesToDisplay.map((article, i) => (
33 |
38 | ))}
39 |
40 |
41 |
42 |
43 | Browse articles by category
44 |
45 |
46 |
47 | {categories.map((category) => (
48 |
49 | ))}
50 |
51 |
52 | >
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/app/components/accordion-features.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useState } from "react";
3 | import TestimonialSingle from "./testimonial-single";
4 | import feature1 from "../../public/feature_1.gif";
5 | import feature2 from "../../public/feature_2.png";
6 | import feature3 from "../../public/feature_3.gif";
7 |
8 | const features = [
9 | {
10 | id: 1,
11 | title: "AI-Powered Lesson Creation",
12 | description:
13 | "Say goodbye to hours of planning. Our AI creates engaging lesson plans in minutes, giving you more time to focus on what matters most - your students.",
14 | additionalInfo: [
15 | "Save hours of prep time",
16 | "Get fresh, creative ideas",
17 | "Customize to your needs",
18 | ],
19 | icons: [
20 |
27 |
32 | ,
33 |
40 |
41 |
46 | ,
47 |
54 |
55 | ,
56 | ],
57 | image: feature1,
58 | },
59 | {
60 | id: 2,
61 | title: "One-Click Resource Generation",
62 | description:
63 | "Need a quiz, worksheet, or study guide? Our AI creates them instantly. Just choose what you need, and it's done - saving you time and boosting your productivity.",
64 | additionalInfo: [
65 | "Instant resource creation",
66 | "Multiple resource types",
67 | "Perfectly aligned with your lessons",
68 | ],
69 | icons: [
70 |
77 |
82 | ,
83 |
90 |
91 | ,
92 |
99 |
104 | ,
105 | ],
106 | image: feature2,
107 | },
108 | {
109 | id: 3,
110 | title: "Easy Editing and Sharing",
111 | description:
112 | "Tweak AI-generated content to perfection and share it instantly. Edit with ease, export as a polished PDF, and you're ready to teach - it's that simple!",
113 | additionalInfo: [
114 | "User-friendly editing",
115 | "Professional PDF export",
116 | "Ready to use in class",
117 | ],
118 | icons: [
119 |
126 |
127 | ,
128 |
135 |
140 |
141 | ,
142 |
149 |
150 |
151 |
152 | ,
153 | ],
154 | image: feature3,
155 | },
156 | ];
157 |
158 | export default function AccordionFeatures({ config }: { config?: any }) {
159 | const [activeFeature, setActiveFeature] = useState(1);
160 |
161 | return (
162 |
166 |
167 |
168 | AI-powered lesson preparation
169 |
170 |
171 |
172 | made easy
173 |
174 |
175 |
176 |
177 |
178 |
179 | {features.map((feature) => (
180 |
184 | setActiveFeature(feature.id)}
188 | >
189 |
196 | {feature.id}.{" "}
197 |
198 |
205 | {feature.title}
206 |
207 |
208 |
213 |
222 |
233 |
234 |
235 |
236 |
243 |
244 |
245 |
{feature.description}
246 |
247 |
248 | {Array.isArray(feature.additionalInfo) ? (
249 | feature.additionalInfo.map((info, index) => (
250 |
254 | {feature.icons?.[index]}
255 | {info}
256 |
257 | ))
258 | ) : (
259 |
260 | {feature.icons}
261 | {feature.additionalInfo}
262 |
263 | )}
264 |
265 |
266 |
267 |
268 | ))}
269 |
270 |
271 | {features.map((feature) => (
272 |
288 | ))}
289 |
290 |
291 |
292 |
293 |
302 |
303 | );
304 | }
305 |
--------------------------------------------------------------------------------
/app/components/before-after.tsx:
--------------------------------------------------------------------------------
1 | export default function BeforeAfter({ config }: { config?: any }) {
2 | return (
3 |
4 |
5 | {/*
*/}
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
Before
20 |
21 |
22 | Traditional teaching methods that limit effectiveness
23 |
24 |
25 |
26 |
27 |
36 |
37 |
38 |
39 |
40 | One-size-fits-all approach fails to address individual student
41 | needs
42 |
43 |
44 |
45 |
54 |
55 |
56 |
57 |
58 | Limited resources for personalized lesson planning and
59 | materials
60 |
61 |
62 |
63 |
72 |
73 |
74 |
75 |
76 | Time-consuming grading and assessment processes
77 |
78 |
79 |
80 |
89 |
90 |
91 |
92 |
93 | Difficulty in tracking individual student progress and
94 | identifying areas for improvement
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
After
103 |
110 |
111 |
112 |
113 |
114 | An AI-powered teaching assistant that enhances education
115 |
116 |
117 |
118 |
129 | Personalized learning experiences tailored to each
130 | student's needs
131 |
132 |
133 |
144 | AI-generated lesson plans and teaching materials
145 |
146 |
147 |
158 | Automated grading and instant feedback for students
159 |
160 |
161 |
172 | Detailed progress tracking and data-driven insights for
173 | targeted interventions
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | );
182 | }
183 |
--------------------------------------------------------------------------------
/app/components/button.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button as HeadlessButton,
3 | type ButtonProps as HeadlessButtonProps,
4 | } from "@headlessui/react";
5 | import { clsx } from "clsx";
6 | import React from "react";
7 | import Link from "next/link";
8 | const styles = {
9 | base: [
10 | // Base
11 | "relative isolate inline-flex items-center justify-center gap-x-2 rounded-lg border text-base/6 font-semibold",
12 |
13 | // Sizing
14 | "px-[calc(theme(spacing[3.5])-1px)] py-[calc(theme(spacing[2.5])-1px)] sm:px-[calc(theme(spacing.3)-1px)] sm:py-[calc(theme(spacing[1.5])-1px)] sm:text-sm/6",
15 |
16 | // Focus
17 | "focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500",
18 |
19 | // Disabled
20 | "data-[disabled]:opacity-50",
21 |
22 | // Icon
23 | "[&>[data-slot=icon]]:-mx-0.5 [&>[data-slot=icon]]:my-0.5 [&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:shrink-0 [&>[data-slot=icon]]:text-[--btn-icon] [&>[data-slot=icon]]:sm:my-1 [&>[data-slot=icon]]:sm:size-4 forced-colors:[--btn-icon:ButtonText] forced-colors:data-[hover]:[--btn-icon:ButtonText]",
24 | ],
25 | solid: [
26 | // Optical border, implemented as the button background to avoid corner artifacts
27 | "border-transparent bg-[--btn-border]",
28 |
29 | // Dark mode: border is rendered on `after` so background is set to button background
30 | "dark:bg-[--btn-bg]",
31 |
32 | // Button background, implemented as foreground layer to stack on top of pseudo-border layer
33 | "before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-[--btn-bg]",
34 |
35 | // Drop shadow, applied to the inset `before` layer so it blends with the border
36 | "before:shadow",
37 |
38 | // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
39 | "dark:before:hidden",
40 |
41 | // Dark mode: Subtle white outline is applied using a border
42 | "dark:border-white/5",
43 |
44 | // Shim/overlay, inset to match button foreground and used for hover state + highlight shadow
45 | "after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.lg)-1px)]",
46 |
47 | // Inner highlight shadow
48 | "after:shadow-[shadow:inset_0_1px_theme(colors.white/15%)]",
49 |
50 | // White overlay on hover
51 | "after:data-[active]:bg-[--btn-hover-overlay] after:data-[hover]:bg-[--btn-hover-overlay]",
52 |
53 | // Dark mode: `after` layer expands to cover entire button
54 | "dark:after:-inset-px dark:after:rounded-lg",
55 |
56 | // Disabled
57 | "before:data-[disabled]:shadow-none after:data-[disabled]:shadow-none",
58 | ],
59 | outline: [
60 | // Base
61 | "border-zinc-950/10 text-zinc-950 data-[active]:bg-zinc-950/[2.5%] data-[hover]:bg-zinc-950/[2.5%]",
62 |
63 | // Dark mode
64 | "dark:border-white/15 dark:text-white dark:[--btn-bg:transparent] dark:data-[active]:bg-white/5 dark:data-[hover]:bg-white/5",
65 |
66 | // Icon
67 | "[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]",
68 | ],
69 | plain: [
70 | // Base
71 | "border-transparent text-zinc-950 data-[active]:bg-zinc-950/5 data-[hover]:bg-zinc-950/5",
72 |
73 | // Dark mode
74 | "dark:text-white dark:data-[active]:bg-white/10 dark:data-[hover]:bg-white/10",
75 |
76 | // Icon
77 | "[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]",
78 | ],
79 | colors: {
80 | "dark/zinc": [
81 | "text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]",
82 | "dark:text-white dark:[--btn-bg:theme(colors.zinc.600)] dark:[--btn-hover-overlay:theme(colors.white/5%)]",
83 | "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]",
84 | ],
85 | light: [
86 | "text-zinc-950 [--btn-bg:white] [--btn-border:theme(colors.zinc.950/10%)] [--btn-hover-overlay:theme(colors.zinc.950/2.5%)] data-[active]:[--btn-border:theme(colors.zinc.950/15%)] data-[hover]:[--btn-border:theme(colors.zinc.950/15%)]",
87 | "dark:text-white dark:[--btn-hover-overlay:theme(colors.white/5%)] dark:[--btn-bg:theme(colors.zinc.800)]",
88 | "[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]",
89 | ],
90 | "dark/white": [
91 | "text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]",
92 | "dark:text-zinc-950 dark:[--btn-bg:white] dark:[--btn-hover-overlay:theme(colors.zinc.950/5%)]",
93 | "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]",
94 | ],
95 | dark: [
96 | "text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]",
97 | "dark:[--btn-hover-overlay:theme(colors.white/5%)] dark:[--btn-bg:theme(colors.zinc.800)]",
98 | "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]",
99 | ],
100 | white: [
101 | "text-zinc-950 [--btn-bg:white] [--btn-border:theme(colors.zinc.950/10%)] [--btn-hover-overlay:theme(colors.zinc.950/2.5%)] data-[active]:[--btn-border:theme(colors.zinc.950/15%)] data-[hover]:[--btn-border:theme(colors.zinc.950/15%)]",
102 | "dark:[--btn-hover-overlay:theme(colors.zinc.950/5%)]",
103 | "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.500)] data-[hover]:[--btn-icon:theme(colors.zinc.500)]",
104 | ],
105 | zinc: [
106 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.zinc.600)] [--btn-border:theme(colors.zinc.700/90%)]",
107 | "dark:[--btn-hover-overlay:theme(colors.white/5%)]",
108 | "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]",
109 | ],
110 | indigo: [
111 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.indigo.500)] [--btn-border:theme(colors.indigo.600/90%)]",
112 | "[--btn-icon:theme(colors.indigo.300)] data-[active]:[--btn-icon:theme(colors.indigo.200)] data-[hover]:[--btn-icon:theme(colors.indigo.200)]",
113 | ],
114 | cyan: [
115 | "text-cyan-950 [--btn-bg:theme(colors.cyan.300)] [--btn-border:theme(colors.cyan.400/80%)] [--btn-hover-overlay:theme(colors.white/25%)]",
116 | "[--btn-icon:theme(colors.cyan.500)]",
117 | ],
118 | red: [
119 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.red.600)] [--btn-border:theme(colors.red.700/90%)]",
120 | "[--btn-icon:theme(colors.red.300)] data-[active]:[--btn-icon:theme(colors.red.200)] data-[hover]:[--btn-icon:theme(colors.red.200)]",
121 | ],
122 | orange: [
123 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.orange.500)] [--btn-border:theme(colors.orange.600/90%)]",
124 | "[--btn-icon:theme(colors.orange.300)] data-[active]:[--btn-icon:theme(colors.orange.200)] data-[hover]:[--btn-icon:theme(colors.orange.200)]",
125 | ],
126 | amber: [
127 | "text-amber-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.amber.400)] [--btn-border:theme(colors.amber.500/80%)]",
128 | "[--btn-icon:theme(colors.amber.600)]",
129 | ],
130 | yellow: [
131 | "text-yellow-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.yellow.300)] [--btn-border:theme(colors.yellow.400/80%)]",
132 | "[--btn-icon:theme(colors.yellow.600)] data-[active]:[--btn-icon:theme(colors.yellow.700)] data-[hover]:[--btn-icon:theme(colors.yellow.700)]",
133 | ],
134 | lime: [
135 | "text-lime-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.lime.300)] [--btn-border:theme(colors.lime.400/80%)]",
136 | "[--btn-icon:theme(colors.lime.600)] data-[active]:[--btn-icon:theme(colors.lime.700)] data-[hover]:[--btn-icon:theme(colors.lime.700)]",
137 | ],
138 | green: [
139 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.green.600)] [--btn-border:theme(colors.green.700/90%)]",
140 | "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]",
141 | ],
142 | emerald: [
143 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.emerald.600)] [--btn-border:theme(colors.emerald.700/90%)]",
144 | "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]",
145 | ],
146 | teal: [
147 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.teal.600)] [--btn-border:theme(colors.teal.700/90%)]",
148 | "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]",
149 | ],
150 | sky: [
151 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.sky.500)] [--btn-border:theme(colors.sky.600/80%)]",
152 | "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]",
153 | ],
154 | blue: [
155 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.blue.600)] [--btn-border:theme(colors.blue.700/90%)]",
156 | "[--btn-icon:theme(colors.blue.400)] data-[active]:[--btn-icon:theme(colors.blue.300)] data-[hover]:[--btn-icon:theme(colors.blue.300)]",
157 | ],
158 | violet: [
159 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.violet.500)] [--btn-border:theme(colors.violet.600/90%)]",
160 | "[--btn-icon:theme(colors.violet.300)] data-[active]:[--btn-icon:theme(colors.violet.200)] data-[hover]:[--btn-icon:theme(colors.violet.200)]",
161 | ],
162 | purple: [
163 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.purple.500)] [--btn-border:theme(colors.purple.600/90%)]",
164 | "[--btn-icon:theme(colors.purple.300)] data-[active]:[--btn-icon:theme(colors.purple.200)] data-[hover]:[--btn-icon:theme(colors.purple.200)]",
165 | ],
166 | fuchsia: [
167 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.fuchsia.500)] [--btn-border:theme(colors.fuchsia.600/90%)]",
168 | "[--btn-icon:theme(colors.fuchsia.300)] data-[active]:[--btn-icon:theme(colors.fuchsia.200)] data-[hover]:[--btn-icon:theme(colors.fuchsia.200)]",
169 | ],
170 | pink: [
171 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.pink.500)] [--btn-border:theme(colors.pink.600/90%)]",
172 | "[--btn-icon:theme(colors.pink.300)] data-[active]:[--btn-icon:theme(colors.pink.200)] data-[hover]:[--btn-icon:theme(colors.pink.200)]",
173 | ],
174 | rose: [
175 | "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.rose.500)] [--btn-border:theme(colors.rose.600/90%)]",
176 | "[--btn-icon:theme(colors.rose.300)] data-[active]:[--btn-icon:theme(colors.rose.200)] data-[hover]:[--btn-icon:theme(colors.rose.200)]",
177 | ],
178 | },
179 | };
180 |
181 | type ButtonProps = (
182 | | { color?: keyof typeof styles.colors; outline?: never; plain?: never }
183 | | { color?: never; outline: true; plain?: never }
184 | | { color?: never; outline?: never; plain: true }
185 | ) & { children: React.ReactNode } & (
186 | | HeadlessButtonProps
187 | | React.ComponentPropsWithoutRef
188 | );
189 |
190 | export const Button = React.forwardRef(function Button(
191 | { color, outline, plain, className, children, ...props }: ButtonProps,
192 | ref: React.ForwardedRef
193 | ) {
194 | const classes = clsx(
195 | className,
196 | styles.base,
197 | outline
198 | ? styles.outline
199 | : plain
200 | ? styles.plain
201 | : clsx(styles.solid, styles.colors[color ?? "dark/zinc"])
202 | );
203 |
204 | return "href" in props ? (
205 | }
209 | >
210 | {children}
211 |
212 | ) : (
213 |
218 | {children}
219 |
220 | );
221 | });
222 |
223 | /* Expand the hit area to at least 44×44px on touch devices */
224 | export function TouchTarget({ children }: { children: React.ReactNode }) {
225 | return (
226 | <>
227 | {children}
228 |
232 | >
233 | );
234 | }
235 |
--------------------------------------------------------------------------------
/app/components/container.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | export function Container({
4 | className,
5 | ...props
6 | }: React.ComponentPropsWithoutRef<"div">) {
7 | return
;
8 | }
9 |
--------------------------------------------------------------------------------
/app/components/cta.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button } from "./button";
3 |
4 | export default function CTA({ config }: { config?: any }) {
5 | return (
6 |
7 |
8 |
9 |
10 | Revolutionize Your Lesson Planning with AI-Powered Tools
11 |
12 |
13 | Create engaging, personalized lessons in minutes and focus on what
14 | matters most - your students.
15 |
16 |
17 | Get Started
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/app/components/faq.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useState } from "react";
3 | import TestimonialSingle from "./testimonial-single";
4 |
5 | const faqData = [
6 | {
7 | question: "What is QuillMinds?",
8 | answer: (
9 |
10 |
11 | QuillMinds is an AI-powered platform designed to assist teachers with
12 | lesson planning and educational content creation. Our goal is to save
13 | educators time and enhance the quality of their teaching materials.
14 |
15 |
16 | ),
17 | },
18 | {
19 | question: "How does the free plan work?",
20 | answer:
21 | "Our free plan offers 50 credits for AI-powered content generation. This allows you to explore QuillMinds' capabilities and create several lesson plans or educational resources without any cost.",
22 | },
23 | {
24 | question: "What features are included in the subscription plan?",
25 | answer:
26 | "The subscription plan offers unlimited AI-powered content generation, advanced customization options, priority support, and access to premium educational resources and templates.",
27 | },
28 | {
29 | question: "Can I try QuillMinds before subscribing?",
30 | answer:
31 | "Absolutely! You can start with our free plan, which includes 50 credits for content generation. This allows you to experience the platform's capabilities before deciding to subscribe.",
32 | },
33 | {
34 | question: "How does AI assist in lesson planning?",
35 | answer:
36 | "Our AI analyzes educational standards, best teaching practices, and your input to generate comprehensive lesson plans, activities, and assessments tailored to your specific needs and teaching style.",
37 | },
38 | {
39 | question: "Is my data secure?",
40 | answer:
41 | "Yes, we take data security very seriously. All user data is encrypted, and we adhere to strict privacy policies to ensure your information and created content remain confidential.",
42 | },
43 | {
44 | question: "Can I customize the AI-generated content?",
45 | answer:
46 | "Absolutely! While our AI provides a strong foundation, you have full control to edit, customize, and refine any generated content to perfectly match your teaching needs and style.",
47 | },
48 | {
49 | question: "What subjects and grade levels does QuillMinds support?",
50 | answer:
51 | "QuillMinds supports a wide range of subjects and grade levels, from elementary to high school. Our AI is continuously updated to align with current educational standards across various disciplines.",
52 | },
53 | {
54 | question: "How often is new content added?",
55 | answer:
56 | "We regularly update our content library and AI models to ensure you have access to the latest educational resources and teaching methodologies. Subscribers receive notifications about new features and content.",
57 | },
58 | {
59 | question: "Can I collaborate with other teachers on QuillMinds?",
60 | answer:
61 | "Yes, our platform includes collaboration features that allow you to share and co-edit lesson plans with colleagues, fostering a community of educators and enabling the exchange of ideas.",
62 | },
63 | ];
64 |
65 | const FAQItem = ({
66 | question,
67 | answer,
68 | isOpen,
69 | onClick,
70 | }: {
71 | question: string;
72 | answer: React.ReactNode;
73 | isOpen: boolean;
74 | onClick: () => void;
75 | }) => {
76 | return (
77 |
78 |
83 | {question}
84 |
91 |
92 |
93 |
94 |
99 |
100 | {typeof answer === "string" ?
{answer}
: answer}
101 |
102 |
103 |
104 | );
105 | };
106 |
107 | export default function FAQ({ config }: { config?: any }) {
108 | const [openIndex, setOpenIndex] = useState(null);
109 |
110 | const handleToggle = (index: number) => {
111 | setOpenIndex(openIndex === index ? null : index);
112 | };
113 |
114 | return (
115 |
116 |
117 |
118 |
119 |
FAQ
120 |
121 | Frequently Asked Questions
122 |
123 |
124 |
125 | {faqData.map((item, index) => (
126 | handleToggle(index)}
132 | />
133 | ))}
134 |
135 |
136 |
145 |
146 |
147 | );
148 | }
149 |
--------------------------------------------------------------------------------
/app/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import SVGLogo from "./svg-logo";
4 | import artemImg from "../../public/artem.png";
5 |
6 | export default function Footer() {
7 | return (
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 | Quillminds
20 |
21 |
22 |
23 | AI-powered lesson planning made easy. Create engaging and
24 | personalized lessons in minutes.
25 |
26 |
27 | Copyright © {new Date().getFullYear()} - All rights reserved
28 |
29 |
30 |
31 |
32 |
33 | LINKS
34 |
35 |
49 |
50 |
51 |
52 | LEGAL
53 |
54 |
55 |
56 | Terms of services
57 |
58 |
59 | Privacy policy
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
74 |
75 | Hey Curious 👋 I'm{" "}
76 |
82 | Artem
83 |
84 | , the creator of QuillMinds. You can follow my work on{" "}
85 |
91 | X.
92 |
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/app/components/header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import {
5 | Popover,
6 | PopoverButton,
7 | PopoverOverlay,
8 | PopoverPanel,
9 | } from "@headlessui/react";
10 | import { AnimatePresence, motion } from "framer-motion";
11 |
12 | import { Container } from "./container";
13 | import { NavLinks } from "./nav-links";
14 | import { Button } from "./button";
15 | import SVGLogo from "./svg-logo";
16 |
17 | function MenuIcon(props: React.ComponentPropsWithoutRef<"svg">) {
18 | return (
19 |
20 |
26 |
27 | );
28 | }
29 |
30 | function ChevronUpIcon(props: React.ComponentPropsWithoutRef<"svg">) {
31 | return (
32 |
33 |
39 |
40 | );
41 | }
42 |
43 | function MobileNavLink(
44 | props: Omit<
45 | React.ComponentPropsWithoutRef>,
46 | "as" | "className"
47 | >
48 | ) {
49 | return (
50 |
55 | );
56 | }
57 |
58 | export function Header() {
59 | return (
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | Quillminds
68 |
69 |
70 |
71 |
72 |
73 | Get Started
74 |
75 |
76 |
77 |
78 |
79 | {({ open }) => (
80 | <>
81 |
85 | {({ open }) =>
86 | open ? (
87 |
88 | ) : (
89 |
90 | )
91 | }
92 |
93 |
94 | {open && (
95 | <>
96 |
104 |
116 |
117 |
118 | Features
119 |
120 |
121 | Pricing
122 |
123 | FAQs
124 | Blog
125 |
126 |
127 |
128 | Log in
129 |
130 | Get Started
131 |
132 |
133 | >
134 | )}
135 |
136 | >
137 | )}
138 |
139 | {/*
140 | Log in
141 |
142 |
143 | Download
144 | */}
145 |
146 |
147 |
148 |
149 | );
150 | }
151 |
--------------------------------------------------------------------------------
/app/components/hero.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "./button";
2 | import TestimonialsAvatars from "./testimonials-avatars";
3 |
4 | function Hero({ config }: { config?: any }) {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | Focus on teaching,
13 | not endless preparation.
14 |
15 |
16 | AI-Powered Lesson Planning Made Easy. For teachers at any level.
17 |
18 |
19 |
24 | Get Started
25 |
26 |
27 |
28 |
29 |
30 | {/*
*/}
31 |
32 |
33 | );
34 | }
35 | const HeroFooter = () => {
36 | return (
37 |
38 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
3857 happy teachers
60 |
61 |
62 | {[...Array(5)].map((_, index) => (
63 |
64 |
70 |
75 |
76 |
77 | ))}
78 |
79 |
80 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | export default Hero;
106 |
--------------------------------------------------------------------------------
/app/components/logo-clouds.tsx:
--------------------------------------------------------------------------------
1 | export default function LogoClouds() {
2 | return (
3 |
4 |
5 |
6 | Trusted by teachers worldwide
7 |
8 |
9 |
16 |
23 |
30 |
37 |
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/app/components/nav-links.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRef, useState } from "react";
4 | import Link from "next/link";
5 | import { AnimatePresence, motion } from "framer-motion";
6 | import { usePathname } from "next/navigation";
7 |
8 | export function NavLinks() {
9 | const [hoveredIndex, setHoveredIndex] = useState(null);
10 | const timeoutRef = useRef(null);
11 | const pathname = usePathname();
12 |
13 | return [
14 | ["Features", "/#features"],
15 | ["Pricing", "/#pricing"],
16 | ["FAQs", "/#faq"],
17 | ["Blog", "/blog"],
18 | ].map(([label, href], index) => (
19 | {
28 | if (timeoutRef.current) {
29 | window.clearTimeout(timeoutRef.current);
30 | }
31 | setHoveredIndex(index);
32 | }}
33 | onMouseLeave={() => {
34 | timeoutRef.current = window.setTimeout(() => {
35 | setHoveredIndex(null);
36 | }, 200);
37 | }}
38 | >
39 |
40 | {hoveredIndex === index && (
41 |
51 | )}
52 |
53 | {label}
54 |
55 | ));
56 | }
57 |
--------------------------------------------------------------------------------
/app/components/pricing.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import TestimonialSingle from "./testimonial-single";
3 | import { Button } from "./button";
4 | export default function Pricing({ config }: { config?: any }) {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | ✨ Launch discount — $50 OFF ✨
12 |
13 |
14 |
15 | Transform Your Lesson Planning with AI
16 |
17 |
18 | Save hours on lesson preparation, create engaging content, and focus
19 | on what matters most - your students. Get started in minutes.
20 |
21 |
22 |
23 |
50 AI-generated lesson plans,
31 | Basic customization options ,
32 | Email support ,
33 | ]}
34 | buttonLink="/sign-up"
35 | description="Perfect for trying out QuillMinds"
36 | />
37 | Unlimited AI-generated lesson plans,
45 | Advanced customization options ,
46 | Priority support ,
47 | Collaboration tools ,
48 | Premium templates ,
49 | ]}
50 | buttonLink="/sign-up?priceId=pro-plan"
51 | description="For educators who want the full QuillMinds experience"
52 | />
53 |
54 |
63 |
64 |
65 | );
66 | }
67 |
68 | function PricingCard({
69 | title,
70 | price,
71 | originalPrice,
72 | features,
73 | buttonLink,
74 | isFeatured,
75 | description,
76 | }: {
77 | title: string;
78 | price: number;
79 | originalPrice: number;
80 | features: React.ReactNode[];
81 | buttonLink: string;
82 | isFeatured?: boolean;
83 | description: string;
84 | }) {
85 | return (
86 |
87 | {isFeatured && (
88 |
89 |
90 | BEST TEACHER'S CHOICE
91 |
92 |
93 | )}
94 |
99 |
100 |
101 |
{title}
102 |
{description}
103 |
104 |
105 |
110 |
111 |
112 | ${originalPrice}
113 |
114 |
115 |
116 | ${price}
117 |
118 |
119 |
120 | USD / month
121 |
122 |
123 |
124 |
129 | {features.map((feature, index) => (
130 |
131 |
137 |
142 |
143 | {feature}
144 |
145 | ))}
146 |
147 |
148 |
149 | Get Quillminds
150 |
151 |
152 |
153 |
154 |
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/app/components/svg-logo.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const SVGLogo: React.FC = () => (
4 |
11 |
12 |
16 |
21 |
26 |
27 | );
28 |
29 | export default SVGLogo;
30 |
--------------------------------------------------------------------------------
/app/components/testimonial-single.tsx:
--------------------------------------------------------------------------------
1 | export default function TestimonialSingle({
2 | testimonial,
3 | }: {
4 | testimonial: {
5 | name: string;
6 | content: React.ReactNode;
7 | schoolName: string;
8 | image: string;
9 | };
10 | }) {
11 | const image = testimonial.image;
12 | return (
13 |
14 |
15 | {[...Array(5)].map((_, i) => (
16 |
23 |
28 |
29 | ))}
30 |
31 |
32 | {testimonial.content}
33 |
34 |
35 |
46 |
47 |
{testimonial.name}
48 |
{testimonial.schoolName}
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/app/components/testimonials-avatars.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | const avatars: {
3 | alt: string;
4 | src: string;
5 | }[] = [
6 | {
7 | alt: "User avatar",
8 | // Ideally, load from a statically generated image for better SEO performance (import userImage from "@/public/userImage.png")
9 | src: "",
10 | },
11 | {
12 | alt: "User avatar",
13 | src: "",
14 | },
15 | {
16 | alt: "User avatar",
17 | src: "",
18 | },
19 | {
20 | alt: "User avatar",
21 | src: "",
22 | },
23 | {
24 | alt: "User avatar",
25 | src: "",
26 | },
27 | ];
28 |
29 | const TestimonialsAvatars = ({ priority }: { priority?: boolean }) => {
30 | return (
31 |
32 | {/* AVATARS */}
33 |
34 | {avatars.map((image, i) => (
35 |
36 |
44 |
45 | ))}
46 |
47 |
48 | {/* RATING */}
49 |
50 |
51 | {[...Array(5)].map((_, i) => (
52 |
59 |
64 |
65 | ))}
66 |
67 |
68 |
69 | loved by teachers worldwide
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default TestimonialsAvatars;
77 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kirsanow/nextjs-saas-landing-page/f64545f6f0b4187a652f37d288545152628b543f/app/favicon.ico
--------------------------------------------------------------------------------
/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kirsanow/nextjs-saas-landing-page/f64545f6f0b4187a652f37d288545152628b543f/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kirsanow/nextjs-saas-landing-page/f64545f6f0b4187a652f37d288545152628b543f/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer utilities {
6 | .text-balance {
7 | text-wrap: balance;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import localFont from "next/font/local";
2 | import "./globals.css";
3 | import { getSEOTags } from "./lib/seo";
4 |
5 | const geistSans = localFont({
6 | src: "./fonts/GeistVF.woff",
7 | variable: "--font-geist-sans",
8 | weight: "100 900",
9 | });
10 | const geistMono = localFont({
11 | src: "./fonts/GeistMonoVF.woff",
12 | variable: "--font-geist-mono",
13 | weight: "100 900",
14 | });
15 |
16 | export const metadata = getSEOTags({
17 | title: "QuillMinds",
18 | description: "QuillMinds is ai-powered lesson preparation tool for teachers.",
19 | canonicalUrlRelative: "/",
20 | });
21 |
22 | export default function RootLayout({
23 | children,
24 | }: Readonly<{
25 | children: React.ReactNode;
26 | }>) {
27 | return (
28 |
29 |
32 | {children}
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/app/lib/seo.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import config from "@/config";
3 |
4 | // These are all the SEO tags you can add to your pages.
5 | // It prefills data with default title/description/OG, etc.. and you can cusotmize it for each page.
6 | // It's already added in the root layout.js so you don't have to add it to every pages
7 | // But I recommend to set the canonical URL for each page (export const metadata = getSEOTags({canonicalUrlRelative: "/"});)
8 | // See https://shipfa.st/docs/features/seo
9 | export const getSEOTags = ({
10 | title,
11 | description,
12 | keywords,
13 | openGraph,
14 | canonicalUrlRelative,
15 | extraTags,
16 | }: Metadata & {
17 | canonicalUrlRelative?: string;
18 | extraTags?: Record;
19 | } = {}) => {
20 | return {
21 | // up to 50 characters (what does your app do for the user?) > your main should be here
22 | title: title || config.appName,
23 | // up to 160 characters (how does your app help the user?)
24 | description: description || config.appDescription,
25 | // some keywords separated by commas. by default it will be your app name
26 | keywords: keywords || [config.appName],
27 | applicationName: config.appName,
28 | // set a base URL prefix for other fields that require a fully qualified URL (.e.g og:image: og:image: 'https://yourdomain.com/share.png' => '/share.png')
29 | metadataBase: new URL(
30 | process.env.NODE_ENV === "development"
31 | ? "http://localhost:3000/"
32 | : `https://${config.domainName}/`
33 | ),
34 |
35 | openGraph: {
36 | title: openGraph?.title || config.appName,
37 | description: openGraph?.description || config.appDescription,
38 | url: openGraph?.url || `https://${config.domainName}/`,
39 | siteName: openGraph?.title || config.appName,
40 | // If you add an opengraph-image.(jpg|jpeg|png|gif) image to the /app folder, you don't need the code below
41 | // images: [
42 | // {
43 | // url: `https://${config.domainName}/share.png`,
44 | // width: 1200,
45 | // height: 660,
46 | // },
47 | // ],
48 | locale: "en_US",
49 | type: "website",
50 | },
51 |
52 | twitter: {
53 | title: openGraph?.title || config.appName,
54 | description: openGraph?.description || config.appDescription,
55 | // If you add an twitter-image.(jpg|jpeg|png|gif) image to the /app folder, you don't need the code below
56 | // images: [openGraph?.image || defaults.og.image],
57 | card: "summary_large_image",
58 | creator: "@kirsnvartem",
59 | },
60 |
61 | // If a canonical URL is given, we add it. The metadataBase will turn the relative URL into a fully qualified URL
62 | ...(canonicalUrlRelative && {
63 | alternates: { canonical: canonicalUrlRelative },
64 | }),
65 |
66 | // If you want to add extra tags, you can pass them here
67 | ...extraTags,
68 | };
69 | };
70 |
71 | // Strctured Data for Rich Results on Google. Learn more: https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data
72 | // Find your type here (SoftwareApp, Book...): https://developers.google.com/search/docs/appearance/structured-data/search-gallery
73 | // Use this tool to check data is well structure: https://search.google.com/test/rich-results
74 | // You don't have to use this component, but it increase your chances of having a rich snippet on Google.
75 | // I recommend this one below to your /page.js for software apps: It tells Google your AppName is a Software, and it has a rating of 4.8/5 from 12 reviews.
76 | // Fill the fields with your own data
77 | export const renderSchemaTags = () => {
78 | return (
79 |
110 | );
111 | };
112 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import AccordionFeatures from "./components/accordion-features";
2 | import BeforeAfter from "./components/before-after";
3 | import { Container } from "./components/container";
4 | import CTA from "./components/cta";
5 | import FAQ from "./components/faq";
6 | import Footer from "./components/footer";
7 | import { Header } from "./components/header";
8 | import Hero from "./components/hero";
9 | import LogoClouds from "./components/logo-clouds";
10 | import Pricing from "./components/pricing";
11 | import { renderSchemaTags } from "./lib/seo";
12 |
13 | export default function Home() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {renderSchemaTags()}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/config.ts:
--------------------------------------------------------------------------------
1 | const config = {
2 | domainName: "quillminds.com",
3 | appName: "QuillMinds",
4 | appDescription:
5 | "QuillMinds is ai-powered lesson preparation tool for teachers.",
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [{ hostname: "api.dicebear.com" }],
5 | },
6 | };
7 |
8 | export default nextConfig;
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-saas-landing-page",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@headlessui/react": "^2.1.8",
13 | "clsx": "^2.1.1",
14 | "framer-motion": "^11.9.0",
15 | "next": "14.2.13",
16 | "react": "^18",
17 | "react-dom": "^18"
18 | },
19 | "devDependencies": {
20 | "@types/node": "^20",
21 | "@types/react": "^18",
22 | "@types/react-dom": "^18",
23 | "eslint": "^8",
24 | "eslint-config-next": "14.2.13",
25 | "postcss": "^8",
26 | "tailwindcss": "^3.4.1",
27 | "typescript": "^5"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/artem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kirsanow/nextjs-saas-landing-page/f64545f6f0b4187a652f37d288545152628b543f/public/artem.png
--------------------------------------------------------------------------------
/public/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kirsanow/nextjs-saas-landing-page/f64545f6f0b4187a652f37d288545152628b543f/public/cover.png
--------------------------------------------------------------------------------
/public/feature_1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kirsanow/nextjs-saas-landing-page/f64545f6f0b4187a652f37d288545152628b543f/public/feature_1.gif
--------------------------------------------------------------------------------
/public/feature_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kirsanow/nextjs-saas-landing-page/f64545f6f0b4187a652f37d288545152628b543f/public/feature_2.png
--------------------------------------------------------------------------------
/public/feature_3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kirsanow/nextjs-saas-landing-page/f64545f6f0b4187a652f37d288545152628b543f/public/feature_3.gif
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: "var(--background)",
13 | foreground: "var(--foreground)",
14 | },
15 | },
16 | },
17 | plugins: [],
18 | };
19 | export default config;
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------