├── src
├── content
│ └── .gitkeep
├── utils
│ ├── permalinks.js
│ ├── utils.ts
│ └── posts.js
├── assets
│ ├── images
│ │ ├── .gitkeep
│ │ ├── gas.jpg
│ │ ├── hero.jpg
│ │ ├── hero2.jpg
│ │ ├── react-logo.png
│ │ ├── camera-back.jpg
│ │ ├── camera-front.jpg
│ │ ├── nextjs-logo.png
│ │ ├── typescript-logo.png
│ │ └── tailwind-css-logo.png
│ └── styles
│ │ └── base.css
├── stories
│ ├── assets
│ │ ├── docs.png
│ │ ├── assets.png
│ │ ├── context.png
│ │ ├── share.png
│ │ ├── styling.png
│ │ ├── testing.png
│ │ ├── theming.png
│ │ ├── figma-plugin.png
│ │ ├── accessibility.png
│ │ ├── addon-library.png
│ │ ├── avif-test-image.avif
│ │ ├── youtube.svg
│ │ ├── tutorials.svg
│ │ ├── accessibility.svg
│ │ ├── discord.svg
│ │ └── github.svg
│ └── widgets
│ │ ├── Announcement.stories.ts
│ │ ├── Hero.stories.ts
│ │ ├── Footer.stories.ts
│ │ ├── Header.stories.ts
│ │ ├── Hero2.stories.ts
│ │ ├── Footer2.stories.ts
│ │ ├── CallToAction2.stories.ts
│ │ ├── Team.stories.ts
│ │ ├── Team2.stories.ts
│ │ ├── Comparison.stories.ts
│ │ ├── SocialProof.stories.ts
│ │ ├── FAQs2.stories.ts
│ │ ├── FAQs4.stories.ts
│ │ ├── CallToAction.stories.ts
│ │ ├── Stats.stories.ts
│ │ ├── Contact.stories.ts
│ │ ├── FAQs3.stories.ts
│ │ ├── Pricing.stories.ts
│ │ ├── Contact2.stories.ts
│ │ ├── Testimonials.stories.ts
│ │ ├── Testimonials2.stories.ts
│ │ ├── Content.stories.ts
│ │ ├── FAQs.stories.ts
│ │ ├── Features2.stories.tsx
│ │ ├── Steps.stories.tsx
│ │ ├── Features.stories.tsx
│ │ ├── Features3.stories.tsx
│ │ └── Features4.stories.tsx
├── components
│ ├── atoms
│ │ ├── Logo.tsx
│ │ ├── Providers.tsx
│ │ ├── ToggleMenu.tsx
│ │ └── ToggleDarkMode.tsx
│ ├── common
│ │ ├── DividerLine.tsx
│ │ ├── Background.tsx
│ │ ├── WidgetWrapper.tsx
│ │ ├── CTA.tsx
│ │ ├── Headline.tsx
│ │ ├── ItemTeam.tsx
│ │ ├── Timeline.tsx
│ │ ├── Collapse.tsx
│ │ ├── ItemTestimonial.tsx
│ │ ├── Dropdown.tsx
│ │ ├── ItemGrid.tsx
│ │ └── Form.tsx
│ └── widgets
│ │ ├── Contact2.tsx
│ │ ├── FAQs2.tsx
│ │ ├── FAQs.tsx
│ │ ├── SocialProof.tsx
│ │ ├── Stats.tsx
│ │ ├── Features2.tsx
│ │ ├── CallToAction.tsx
│ │ ├── Features.tsx
│ │ ├── Features3.tsx
│ │ ├── Announcement.tsx
│ │ ├── Team2.tsx
│ │ ├── Team.tsx
│ │ ├── Features4.tsx
│ │ ├── FAQs3.tsx
│ │ ├── Steps.tsx
│ │ ├── Hero.tsx
│ │ ├── Contact.tsx
│ │ ├── Content.tsx
│ │ ├── Comparison.tsx
│ │ ├── Hero2.tsx
│ │ ├── Footer2.tsx
│ │ ├── FAQs4.tsx
│ │ ├── Testimonials.tsx
│ │ ├── Footer.tsx
│ │ ├── CallToAction2.tsx
│ │ ├── Testimonials2.tsx
│ │ ├── Pricing.tsx
│ │ └── Header.tsx
├── config.js
├── hooks
│ ├── useCollapse.tsx
│ ├── useWindowSize.tsx
│ └── useOnClickOutside.tsx
└── shared
│ ├── data
│ ├── pages
│ │ ├── contact.data.tsx
│ │ └── faqs.data.tsx
│ └── global.data.tsx
│ └── types.d.ts
├── screenshot.jpg
├── public
├── favicon.ico
└── vercel.svg
├── .prettierignore
├── .eslintrc.json
├── postcss.config.js
├── .vscode
├── extensions.json
└── settings.json
├── .prettierrc
├── .editorconfig
├── next-sitemap.config.js
├── .storybook
├── manager.ts
├── main.ts
└── preview.tsx
├── vscode.tailwind.json
├── next.config.js
├── app
├── (pages)
│ ├── faqs
│ │ └── page.tsx
│ ├── contact
│ │ └── page.tsx
│ ├── pricing
│ │ └── page.tsx
│ ├── services
│ │ └── page.tsx
│ └── about
│ │ └── page.tsx
├── (legal)
│ ├── privacy
│ │ └── page.tsx
│ └── terms
│ │ └── page.tsx
├── (blog)
│ ├── blog
│ │ └── page.tsx
│ └── [slug]
│ │ └── page.jsx
├── layout.tsx
└── page.tsx
├── tailwind.config.js
├── .gitignore
├── tsconfig.json
├── LICENSE.md
├── package.json
└── README.md
/src/content/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/permalinks.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/screenshot.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .next
2 | out
3 | node_modules
4 | .github
5 | .changeset
6 | package-lock.json
--------------------------------------------------------------------------------
/src/assets/images/gas.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/assets/images/gas.jpg
--------------------------------------------------------------------------------
/src/assets/images/hero.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/assets/images/hero.jpg
--------------------------------------------------------------------------------
/src/assets/images/hero2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/assets/images/hero2.jpg
--------------------------------------------------------------------------------
/src/stories/assets/docs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/docs.png
--------------------------------------------------------------------------------
/src/stories/assets/assets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/assets.png
--------------------------------------------------------------------------------
/src/stories/assets/context.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/context.png
--------------------------------------------------------------------------------
/src/stories/assets/share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/share.png
--------------------------------------------------------------------------------
/src/stories/assets/styling.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/styling.png
--------------------------------------------------------------------------------
/src/stories/assets/testing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/testing.png
--------------------------------------------------------------------------------
/src/stories/assets/theming.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/theming.png
--------------------------------------------------------------------------------
/src/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/assets/images/react-logo.png
--------------------------------------------------------------------------------
/src/assets/images/camera-back.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/assets/images/camera-back.jpg
--------------------------------------------------------------------------------
/src/assets/images/camera-front.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/assets/images/camera-front.jpg
--------------------------------------------------------------------------------
/src/assets/images/nextjs-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/assets/images/nextjs-logo.png
--------------------------------------------------------------------------------
/src/stories/assets/figma-plugin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/figma-plugin.png
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "plugin:storybook/recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/assets/images/typescript-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/assets/images/typescript-logo.png
--------------------------------------------------------------------------------
/src/stories/assets/accessibility.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/accessibility.png
--------------------------------------------------------------------------------
/src/stories/assets/addon-library.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/addon-library.png
--------------------------------------------------------------------------------
/src/assets/images/tailwind-css-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/assets/images/tailwind-css-logo.png
--------------------------------------------------------------------------------
/src/stories/assets/avif-test-image.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onwidget/tailnext/HEAD/src/stories/assets/avif-test-image.avif
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["bradlc.vscode-tailwindcss", "dbaeumer.vscode-eslint"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "semi": true,
4 | "trailingComma": "all",
5 | "singleQuote": true,
6 | "printWidth": 120,
7 | "tabWidth": 2
8 | }
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | end_of_line = lf
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "css.customData": ["./vscode.tailwind.json"],
3 | "typescript.tsdk": "node_modules/typescript/lib",
4 | "typescript.enablePromptUseWorkspaceTsdk": true
5 | }
6 |
--------------------------------------------------------------------------------
/next-sitemap.config.js:
--------------------------------------------------------------------------------
1 | const SITE = require('./src/config.js').SITE;
2 |
3 | /** @type {import('next-sitemap').IConfig} */
4 | module.exports = {
5 | siteUrl: `${SITE.origin}${SITE.basePathname}`,
6 | generateRobotsTxt: true,
7 | };
8 |
--------------------------------------------------------------------------------
/src/components/atoms/Logo.tsx:
--------------------------------------------------------------------------------
1 | const Logo = () => (
2 |
3 | TailNext
4 |
5 | );
6 |
7 | export default Logo;
8 |
--------------------------------------------------------------------------------
/.storybook/manager.ts:
--------------------------------------------------------------------------------
1 | import { addons } from '@storybook/manager-api';
2 | import { create } from '@storybook/theming';
3 |
4 | addons.setConfig({
5 | theme: create({
6 | base: 'light',
7 |
8 | // Logo
9 | brandTitle: 'TailNext',
10 | brandUrl: 'https://github.com/onwidget/tailnext',
11 | brandTarget: '_blank',
12 | }),
13 | });
14 |
--------------------------------------------------------------------------------
/src/components/common/DividerLine.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from 'tailwind-merge';
2 |
3 | interface DividerLine {
4 | dividerLineClass?: string;
5 | }
6 |
7 | const DividerLine = ({ dividerLineClass }: DividerLine) => (
8 |
9 | );
10 |
11 | export default DividerLine;
12 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | module.exports.SITE = {
2 | name: 'TailNext',
3 |
4 | origin: 'https://tailnext.vercel.app',
5 | basePathname: '/',
6 | trailingSlash: false,
7 |
8 | title: 'TailNext — Your website with Next.js + Tailwind CSS',
9 | description: 'TailNext is a free and ready to start template to make your website using Next.js and Tailwind CSS.',
10 | };
11 |
--------------------------------------------------------------------------------
/vscode.tailwind.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1.1,
3 | "atDirectives": [
4 | {
5 | "name": "@tailwind",
6 | "description": "@tailwind tailwindcss"
7 | },
8 | {
9 | "name": "@layer",
10 | "description": "@layer tailwindcss"
11 | },
12 | {
13 | "name": "@apply",
14 | "description": "@apply tailwindcss"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/common/Background.tsx:
--------------------------------------------------------------------------------
1 | import { BackgroundProps } from '~/shared/types';
2 |
3 | const Background = ({ children, hasBackground }: BackgroundProps) => {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | };
10 |
11 | export default Background;
12 |
--------------------------------------------------------------------------------
/src/components/atoms/Providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ThemeProvider } from 'next-themes';
4 |
5 | export interface ProvidersProps {
6 | children: React.ReactNode
7 | }
8 |
9 | const Providers = ({ children }: ProvidersProps) => (
10 |
11 | {children}
12 |
13 | );
14 |
15 | export default Providers;
16 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const SITE = require('./src/config.js').SITE;
2 |
3 | /** @type {import('next').NextConfig} */
4 | module.exports = {
5 | reactStrictMode: true,
6 |
7 | trailingSlash: SITE.trailingSlash,
8 | basePath: SITE.basePathname !== '/' ? SITE.basePathname : '',
9 |
10 | swcMinify: true,
11 | poweredByHeader: false,
12 | images: {
13 | remotePatterns: [
14 | {
15 | protocol: 'https',
16 | hostname: 'images.unsplash.com',
17 | },
18 | {
19 | protocol: 'https',
20 | hostname: 'source.unsplash.com',
21 | },
22 | ],
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from '@storybook/nextjs';
2 |
3 | const config: StorybookConfig = {
4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
5 | addons: [
6 | '@storybook/addon-links',
7 | '@storybook/addon-essentials',
8 | '@storybook/addon-onboarding',
9 | '@storybook/addon-interactions',
10 | '@storybook/addon-themes',
11 | '@storybook/addon-a11y'
12 | ],
13 | framework: {
14 | name: '@storybook/nextjs',
15 | options: {},
16 | },
17 | docs: {
18 | autodocs: 'tag',
19 | },
20 | };
21 | export default config;
22 |
--------------------------------------------------------------------------------
/src/hooks/useCollapse.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const useCollapse = () => {
4 | const [toggle, setToggle] = useState(true);
5 | const [activeIndex, setActiveIndex] = useState(undefined);
6 |
7 | const handleSetIndex = (index: number) => {
8 | if (activeIndex !== index) {
9 | setActiveIndex(index);
10 | setToggle(!toggle);
11 | } else {
12 | setActiveIndex(undefined);
13 | setToggle(!toggle);
14 | }
15 | };
16 |
17 | return {
18 | activeIndex,
19 | handleSetIndex,
20 | };
21 | };
22 |
23 | export default useCollapse;
24 |
--------------------------------------------------------------------------------
/app/(pages)/faqs/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 |
3 | import CallToAction from '~/components/widgets/CallToAction';
4 | import FAQs4 from '~/components/widgets/FAQs4';
5 | import { heroFaqs, callToActionFaqs, faqs4Faqs } from '~/shared/data/pages/faqs.data';
6 | import Hero from '~/components/widgets/Hero';
7 |
8 | export const metadata: Metadata = {
9 | title: 'FAQs',
10 | };
11 |
12 | const Page = () => {
13 | return (
14 | <>
15 |
16 |
17 |
18 | >
19 | );
20 | };
21 |
22 | export default Page;
23 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme');
2 | const colors = require('tailwindcss/colors');
3 |
4 | /** @type {import('tailwindcss').Config} */
5 | module.exports = {
6 | content: ['./app/**/*.{js,ts,jsx,tsx}', './src/**/*.{js,ts,jsx,tsx,md,mdx}'],
7 | theme: {
8 | extend: {
9 | colors: {
10 | primary: colors.blue,
11 | secondary: colors.blue,
12 | },
13 | fontFamily: {
14 | sans: ['var(--font-custom)', ...defaultTheme.fontFamily.sans],
15 | },
16 | },
17 | },
18 | plugins: [require('@tailwindcss/typography')],
19 | darkMode: 'class',
20 | };
21 |
--------------------------------------------------------------------------------
/app/(pages)/contact/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 |
3 | import Contact2 from '~/components/widgets/Contact2';
4 | import Features2 from '~/components/widgets/Features2';
5 | import Hero from '~/components/widgets/Hero';
6 | import { heroContact, contact2Contact, features2Contact } from '~/shared/data/pages/contact.data';
7 |
8 | export const metadata: Metadata = {
9 | title: 'Contact us',
10 | };
11 |
12 | const Page = () => {
13 | return (
14 | <>
15 |
16 |
17 |
18 | >
19 | );
20 | };
21 |
22 | export default Page;
23 |
--------------------------------------------------------------------------------
/.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 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | .vscode
39 |
40 | package-lock.json
41 |
42 | public/sitemap.xml
43 | public/sitemap-0.xml
44 | public/robots.txt
--------------------------------------------------------------------------------
/src/components/common/WidgetWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from 'tailwind-merge';
2 | import { WrapperTagProps } from '~/shared/types';
3 | import Background from './Background';
4 |
5 | const WidgetWrapper = ({ children, id, hasBackground, containerClass }: WrapperTagProps) => (
6 |
7 |
8 |
14 | {children}
15 |
16 |
17 | );
18 |
19 | export default WidgetWrapper;
20 |
--------------------------------------------------------------------------------
/src/hooks/useWindowSize.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { WindowSize } from '~/shared/types';
3 |
4 | const useWindowSize = () => {
5 | const [windowSize, setWindowSize] = useState({
6 | width: 0,
7 | height: 0,
8 | });
9 |
10 | useEffect(() => {
11 | const handler = () => {
12 | setWindowSize({
13 | width: window.innerWidth,
14 | height: window.innerHeight,
15 | });
16 | };
17 |
18 | handler();
19 |
20 | window.addEventListener('resize', handler);
21 |
22 | return () => {
23 | window.removeEventListener('resize', handler);
24 | };
25 | }, []);
26 |
27 | return windowSize;
28 | };
29 |
30 | export default useWindowSize;
31 |
--------------------------------------------------------------------------------
/src/stories/assets/youtube.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/widgets/Contact2.tsx:
--------------------------------------------------------------------------------
1 | import Form from '../common/Form';
2 | import Headline from '../common/Headline';
3 | import { ContactProps } from '~/shared/types';
4 | import WidgetWrapper from '../common/WidgetWrapper';
5 |
6 | const Contact2 = ({ header, form, id, hasBackground = false }: ContactProps) => (
7 |
8 | {header && }
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default Contact2;
16 |
--------------------------------------------------------------------------------
/src/hooks/useOnClickOutside.tsx:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect } from 'react';
2 |
3 | type AnyEvent = MouseEvent | TouchEvent;
4 |
5 | export function useOnClickOutside(ref: RefObject, handler: (event: AnyEvent) => void) {
6 | useEffect(() => {
7 | const listener = (event: AnyEvent) => {
8 | if (!ref.current || ref.current.contains(event.target)) {
9 | return;
10 | }
11 | handler(event);
12 | };
13 | document.addEventListener('mousedown', listener);
14 | document.addEventListener('touchstart', listener);
15 | return () => {
16 | document.removeEventListener('mousedown', listener);
17 | document.removeEventListener('touchstart', listener);
18 | };
19 | }, [ref, handler]);
20 | }
21 |
--------------------------------------------------------------------------------
/app/(pages)/pricing/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 |
3 | import Hero from '~/components/widgets/Hero';
4 | import Pricing from '~/components/widgets/Pricing';
5 | import Comparison from '~/components/widgets/Comparison';
6 | import FAQs3 from '~/components/widgets/FAQs3';
7 | import { heroPricing, comparisonPricing, faqs3Pricing, pricingPricing } from '~/shared/data/pages/pricing.data';
8 |
9 | export const metadata: Metadata = {
10 | title: 'Pricing',
11 | };
12 |
13 | const Page = () => {
14 | return (
15 | <>
16 |
17 |
18 |
19 |
20 | >
21 | );
22 | };
23 |
24 | export default Page;
25 |
--------------------------------------------------------------------------------
/src/components/atoms/ToggleMenu.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { IconMenu, IconX } from '@tabler/icons-react';
4 | import { ToggleMenuProps } from '~/shared/types';
5 |
6 | const ToggleMenu = ({ handleToggleMenuOnClick, isToggleMenuOpen }: ToggleMenuProps) => (
7 |
13 | {isToggleMenuOpen ? : }
14 |
15 | );
16 |
17 | export default ToggleMenu;
18 |
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | // Function to format a number in thousands (K) or millions (M) format depending on its value
2 | export const getSuffixNumber = (number: number, digits: number = 1): string => {
3 | const lookup = [
4 | { value: 1, symbol: '' },
5 | { value: 1e3, symbol: 'K' },
6 | { value: 1e6, symbol: 'M' },
7 | { value: 1e9, symbol: 'G' },
8 | { value: 1e12, symbol: 'T' },
9 | { value: 1e15, symbol: 'P' },
10 | { value: 1e18, symbol: 'E' },
11 | ];
12 |
13 | const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
14 | const lookupItem = lookup
15 | .slice()
16 | .reverse()
17 | .find((item) => number >= item.value);
18 | return lookupItem ? (number / lookupItem.value).toFixed(digits).replace(rx, '$1') + lookupItem.symbol : '0';
19 | };
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
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 | "~/*": ["src/*"]
20 | },
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ]
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/(blog)/blog/page.tsx", "app/(blog)/[slug]/page.jsx"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/widgets/FAQs2.tsx:
--------------------------------------------------------------------------------
1 | import Headline from '../common/Headline';
2 | import Collapse from '../common/Collapse';
3 | import { IconChevronDown, IconChevronUp } from '@tabler/icons-react';
4 | import { FAQsProps, Item } from '~/shared/types';
5 | import WidgetWrapper from '../common/WidgetWrapper';
6 |
7 | const FAQs2 = ({ header, items, id, hasBackground = false }: FAQsProps) => (
8 |
9 | {header && }
10 | }
14 | iconDown={ }
15 | />
16 |
17 | );
18 |
19 | export default FAQs2;
20 |
--------------------------------------------------------------------------------
/app/(legal)/privacy/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 |
3 | import fs from 'fs';
4 | import path from 'path';
5 | import md from 'markdown-it';
6 |
7 | export const metadata: Metadata = {
8 | title: 'Privacy',
9 | };
10 |
11 | const Page = () => {
12 | const filePath = path.join(process.cwd(), 'src/content/privacy/privacy.md');
13 | const fileContent = fs.readFileSync(filePath, 'utf8');
14 |
15 | return (
16 |
24 | );
25 | };
26 |
27 | export default Page;
28 |
--------------------------------------------------------------------------------
/app/(legal)/terms/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 |
3 | import fs from 'fs';
4 | import path from 'path';
5 | import md from 'markdown-it';
6 |
7 | export const metadata: Metadata = {
8 | title: 'Terms and conditions',
9 | };
10 |
11 | const Page = () => {
12 | const filePath = path.join(process.cwd(), 'src/content/terms/terms.md');
13 | const fileContent = fs.readFileSync(filePath, 'utf8');
14 |
15 | return (
16 |
24 | );
25 | };
26 |
27 | export default Page;
28 |
--------------------------------------------------------------------------------
/src/components/widgets/FAQs.tsx:
--------------------------------------------------------------------------------
1 | import { FAQsProps } from '~/shared/types';
2 | import Headline from '../common/Headline';
3 | import WidgetWrapper from '../common/WidgetWrapper';
4 | import ItemGrid from '../common/ItemGrid';
5 | import { IconArrowDownRight } from '@tabler/icons-react';
6 |
7 | const FAQs = ({ header, items, columns, id, hasBackground = false }: FAQsProps) => (
8 |
9 | {header && }
10 |
21 |
22 | );
23 |
24 | export default FAQs;
25 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/widgets/SocialProof.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { SocialProofProps } from '~/shared/types';
3 | import WidgetWrapper from '../common/WidgetWrapper';
4 |
5 | const SocialProof = ({ images, id, hasBackground = false }: SocialProofProps) => (
6 |
7 |
8 | {images &&
9 | images.map(({ src, alt, link }, index) => (
10 |
22 | ))}
23 |
24 |
25 | );
26 |
27 | export default SocialProof;
28 |
--------------------------------------------------------------------------------
/src/components/widgets/Stats.tsx:
--------------------------------------------------------------------------------
1 | import { StatsProps } from '~/shared/types';
2 | import { getSuffixNumber } from '~/utils/utils';
3 | import WidgetWrapper from '../common/WidgetWrapper';
4 |
5 | const Stats = ({ items, id, hasBackground = false }: StatsProps) => (
6 |
7 |
8 | {items.map(({ title, description }, index) => (
9 |
13 |
14 | {getSuffixNumber(title as number)}
15 |
16 |
17 | {description}
18 |
19 |
20 | ))}
21 |
22 |
23 | );
24 |
25 | export default Stats;
26 |
--------------------------------------------------------------------------------
/src/components/widgets/Features2.tsx:
--------------------------------------------------------------------------------
1 | import { FeaturesProps } from '~/shared/types';
2 | import Headline from '../common/Headline';
3 | import ItemGrid from '../common/ItemGrid';
4 |
5 | const Features2 = ({ header, items, columns = 3, id }: FeaturesProps) => (
6 |
7 |
8 |
9 | {header && }
10 |
21 |
22 |
23 | );
24 |
25 | export default Features2;
26 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 onWidget
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/widgets/CallToAction.tsx:
--------------------------------------------------------------------------------
1 | import { CallToActionProps, CallToActionType } from '~/shared/types';
2 | import CTA from '../common/CTA';
3 | import WidgetWrapper from '../common/WidgetWrapper';
4 |
5 | const CallToAction = ({ title, subtitle, callToAction, id, hasBackground = false }: CallToActionProps) => {
6 | const { text, href } = callToAction as CallToActionType;
7 |
8 | return (
9 |
10 |
11 | {title && (
12 |
{title}
13 | )}
14 | {subtitle &&
{subtitle}
}
15 | {text && href && (
16 |
21 | )}
22 |
23 |
24 | );
25 | };
26 |
27 | export default CallToAction;
28 |
--------------------------------------------------------------------------------
/src/stories/widgets/Announcement.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Announcement';
4 | import { announcementData as mockData } from '~/shared/data/global.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Announcement',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/widgets/Features.tsx:
--------------------------------------------------------------------------------
1 | import { FeaturesProps } from '~/shared/types';
2 | import Headline from '../common/Headline';
3 | import WidgetWrapper from '../common/WidgetWrapper';
4 | import ItemGrid from '../common/ItemGrid';
5 |
6 | const Features = ({ id, header, items, columns = 3, hasBackground = false }: FeaturesProps) => (
7 |
8 | {header && }
9 |
21 |
22 | );
23 |
24 | export default Features;
25 |
--------------------------------------------------------------------------------
/src/components/widgets/Features3.tsx:
--------------------------------------------------------------------------------
1 | import { FeaturesProps } from '~/shared/types';
2 | import Headline from '../common/Headline';
3 | import WidgetWrapper from '../common/WidgetWrapper';
4 | import ItemGrid from '../common/ItemGrid';
5 |
6 | const Features3 = ({
7 | header,
8 | items,
9 | columns = 3,
10 | isBeforeContent,
11 | isAfterContent,
12 | id,
13 | hasBackground = false,
14 | }: FeaturesProps) => (
15 |
22 | {header && }
23 |
33 |
34 | );
35 |
36 | export default Features3;
37 |
--------------------------------------------------------------------------------
/app/(blog)/blog/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 |
3 | import Image from 'next/image';
4 | import Link from 'next/link';
5 |
6 | import { findLatestPosts } from '~/utils/posts';
7 |
8 | export const metadata: Metadata = {
9 | title: 'Blog',
10 | };
11 |
12 | export default async function Home({}) {
13 | const posts = await findLatestPosts();
14 | return (
15 |
16 |
21 |
22 | {posts.map(({ slug, title, image }: { slug: string, title: string, image: string }) => (
23 |
24 |
25 |
26 |
{title}
27 |
28 |
29 | ))}
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/atoms/ToggleDarkMode.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState, useEffect } from 'react';
4 | import { useTheme } from 'next-themes';
5 | import { IconSun, IconMoon } from '@tabler/icons-react';
6 |
7 | const ToggleDarkMode = () => {
8 | const [mounted, setMounted] = useState(false);
9 | const { systemTheme, theme, setTheme } = useTheme();
10 |
11 | const currentTheme = theme === 'system' ? systemTheme : theme;
12 |
13 | const handleOnClick = () => setTheme(currentTheme === 'dark' ? 'light' : 'dark');
14 |
15 | useEffect(() => {
16 | setMounted(true);
17 | }, []);
18 |
19 | return (
20 |
25 | {mounted ? (
26 | currentTheme === 'dark' ? (
27 |
28 | ) : (
29 |
30 | )
31 | ) : (
32 |
33 | )}
34 |
35 | );
36 | };
37 |
38 | export default ToggleDarkMode;
39 |
--------------------------------------------------------------------------------
/app/(pages)/services/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next';
2 | import CallToAction from '~/components/widgets/CallToAction';
3 | import Content from '~/components/widgets/Content';
4 | import FAQs from '~/components/widgets/FAQs';
5 | import Features2 from '~/components/widgets/Features2';
6 | import Features4 from '~/components/widgets/Features4';
7 | import Hero from '~/components/widgets/Hero';
8 | import Testimonials from '~/components/widgets/Testimonials';
9 | import {
10 | callToActionServices,
11 | contentServicesOne,
12 | contentServicesTwo,
13 | faqsServices,
14 | features2Services,
15 | features4Services,
16 | heroServices,
17 | testimonialsServices,
18 | } from '~/shared/data/pages/services.data';
19 |
20 | export const metadata: Metadata = {
21 | title: 'Services',
22 | };
23 |
24 | const Page = () => {
25 | return (
26 | <>
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | >
36 | );
37 | };
38 |
39 | export default Page;
40 |
--------------------------------------------------------------------------------
/src/stories/assets/tutorials.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/stories/widgets/Hero.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Hero';
4 | import { heroHome as mockData } from '~/shared/data/pages/home.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Hero',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const Mobile: Story = {
31 | args: {
32 | ...mockData,
33 | },
34 | parameters: {
35 | viewport: {
36 | defaultViewport: 'SMALL',
37 | },
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/stories/widgets/Footer.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Footer';
4 | import { footerData as mockData } from '~/shared/data/global.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Footer',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const Mobile: Story = {
31 | args: {
32 | ...mockData,
33 | },
34 | parameters: {
35 | viewport: {
36 | defaultViewport: 'SMALL',
37 | },
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/stories/widgets/Header.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Header';
4 | import { headerData as mockData } from '~/shared/data/global.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Header',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const Mobile: Story = {
31 | args: {
32 | ...mockData,
33 | },
34 | parameters: {
35 | viewport: {
36 | defaultViewport: 'SMALL',
37 | },
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/stories/widgets/Hero2.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Hero2';
4 | import { heroHome as mockData } from '~/shared/data/pages/home.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Hero2',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const Mobile: Story = {
31 | args: {
32 | ...mockData,
33 | },
34 | parameters: {
35 | viewport: {
36 | defaultViewport: 'SMALL',
37 | },
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/stories/widgets/Footer2.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Footer2';
4 | import { footerData as mockData } from '~/shared/data/global.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Footer2',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const Mobile: Story = {
31 | args: {
32 | ...mockData,
33 | },
34 | parameters: {
35 | viewport: {
36 | defaultViewport: 'SMALL',
37 | },
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/stories/widgets/CallToAction2.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/CallToAction2';
4 | import { callToAction2Home as mockData } from '~/shared/data/pages/home.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/CallToAction2',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const Mobile: Story = {
31 | args: {
32 | ...mockData,
33 | },
34 | parameters: {
35 | viewport: {
36 | defaultViewport: 'SMALL',
37 | },
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/common/CTA.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { twMerge } from 'tailwind-merge';
3 | import { CallToActionType, LinkOrButton } from '~/shared/types';
4 |
5 | const CTA = ({ callToAction, containerClass, linkClass, iconClass }: LinkOrButton) => {
6 | const { text, href, icon: Icon, targetBlank } = callToAction as CallToActionType;
7 |
8 | return (
9 | <>
10 | {href && (text || Icon) && (
11 |
12 | {targetBlank ? (
13 |
19 | {Icon && }
20 | {text}
21 |
22 | ) : (
23 |
24 | {Icon && }
25 | {text}
26 |
27 | )}
28 |
29 | )}
30 | >
31 | );
32 | };
33 |
34 | export default CTA;
35 |
--------------------------------------------------------------------------------
/src/stories/widgets/Team.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Team';
4 | import { teamHome as mockData } from '~/shared/data/pages/home.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Team',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const WithBackground: Story = {
31 | args: {
32 | ...mockData,
33 | hasBackground: true,
34 | },
35 | };
36 |
37 | export const Mobile: Story = {
38 | args: {
39 | ...mockData,
40 | },
41 | parameters: {
42 | viewport: {
43 | defaultViewport: 'SMALL',
44 | },
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/src/stories/widgets/Team2.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Team2';
4 | import { teamAbout as mockData } from '~/shared/data/pages/about.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Team2',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const WithBackground: Story = {
31 | args: {
32 | ...mockData,
33 | hasBackground: true,
34 | },
35 | };
36 |
37 | export const Mobile: Story = {
38 | args: {
39 | ...mockData,
40 | },
41 | parameters: {
42 | viewport: {
43 | defaultViewport: 'SMALL',
44 | },
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next';
2 |
3 | import { SITE } from '~/config.js';
4 |
5 | import Providers from '~/components/atoms/Providers';
6 | import Header from '~/components/widgets/Header';
7 | import Announcement from '~/components/widgets/Announcement';
8 | import Footer2 from '~/components/widgets/Footer2';
9 |
10 | import { Inter as CustomFont } from 'next/font/google';
11 | import '~/assets/styles/base.css';
12 |
13 | const customFont = CustomFont({ subsets: ['latin'], variable: '--font-custom' });
14 |
15 | export interface LayoutProps {
16 | children: React.ReactNode;
17 | }
18 |
19 | export const metadata: Metadata = {
20 | title: {
21 | template: `%s — ${SITE.name}`,
22 | default: SITE.title,
23 | },
24 | description: SITE.description,
25 | };
26 |
27 | export default function RootLayout({ children }: LayoutProps) {
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {children}
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/stories/widgets/Comparison.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Comparison';
4 | import { comparisonPricing as mockData } from '~/shared/data/pages/pricing.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Comparison',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const WithBackground: Story = {
31 | args: {
32 | ...mockData,
33 | hasBackground: true,
34 | },
35 | };
36 |
37 | export const Mobile: Story = {
38 | args: {
39 | ...mockData,
40 | },
41 | parameters: {
42 | viewport: {
43 | defaultViewport: 'SMALL',
44 | },
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/src/stories/widgets/SocialProof.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/SocialProof';
4 | import { socialProofHome as mockData } from '~/shared/data/pages/home.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/SocialProof',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const WithBackground: Story = {
31 | args: {
32 | ...mockData,
33 | hasBackground: true,
34 | },
35 | };
36 |
37 | export const Mobile: Story = {
38 | args: {
39 | ...mockData,
40 | },
41 | parameters: {
42 | viewport: {
43 | defaultViewport: 'SMALL',
44 | },
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/src/stories/widgets/FAQs2.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/FAQs2';
4 | import { faqs2Home as mockData } from '~/shared/data/pages/home.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/FAQs2',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | hasBackground: false,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const Mobile: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | parameters: {
43 | viewport: {
44 | defaultViewport: 'SMALL',
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/stories/widgets/FAQs4.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/FAQs4';
4 | import { faqs4Faqs as mockData } from '~/shared/data/pages/faqs.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/FAQs4',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | hasBackground: false,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const Mobile: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | parameters: {
43 | viewport: {
44 | defaultViewport: 'SMALL',
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/stories/widgets/CallToAction.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/CallToAction';
4 | import { callToActionServices as mockData } from '~/shared/data/pages/services.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/CallToAction',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | },
28 | };
29 |
30 | export const WithBackground: Story = {
31 | args: {
32 | ...mockData,
33 | hasBackground: true,
34 | },
35 | };
36 |
37 | export const Mobile: Story = {
38 | args: {
39 | ...mockData,
40 | },
41 | parameters: {
42 | viewport: {
43 | defaultViewport: 'SMALL',
44 | },
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/src/stories/widgets/Stats.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Stats';
4 | import { statsAbout as mockData } from '~/shared/data/pages/about.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Stats',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | hasBackground: false,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const Mobile: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | parameters: {
43 | viewport: {
44 | defaultViewport: 'SMALL',
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/stories/widgets/Contact.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Contact';
4 | import { contactHome as mockData } from '~/shared/data/pages/home.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Contact',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | hasBackground: false,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const Mobile: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | parameters: {
43 | viewport: {
44 | defaultViewport: 'SMALL',
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/stories/widgets/FAQs3.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/FAQs3';
4 | import { faqs3Pricing as mockData } from '~/shared/data/pages/pricing.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/FAQs3',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | hasBackground: false,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const Mobile: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | parameters: {
43 | viewport: {
44 | defaultViewport: 'SMALL',
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/stories/widgets/Pricing.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Pricing';
4 | import { pricingHome as mockData } from '~/shared/data/pages/home.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Pricing',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | hasBackground: false,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const Mobile: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | parameters: {
43 | viewport: {
44 | defaultViewport: 'SMALL',
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/stories/widgets/Contact2.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Contact2';
4 | import { contact2Contact as mockData } from '~/shared/data/pages/contact.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Contact2',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | hasBackground: false,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const Mobile: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | parameters: {
43 | viewport: {
44 | defaultViewport: 'SMALL',
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/stories/widgets/Testimonials.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Testimonials';
4 | import { testimonialsHome as mockData } from '~/shared/data/pages/home.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Testimonials',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | hasBackground: false,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const Mobile: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | parameters: {
43 | viewport: {
44 | defaultViewport: 'SMALL',
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/stories/widgets/Testimonials2.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Testimonials2';
4 | import { testimonials2About as mockData } from '~/shared/data/pages/about.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Testimonials2',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | hasBackground: false,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const Mobile: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | parameters: {
43 | viewport: {
44 | defaultViewport: 'SMALL',
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/src/stories/assets/accessibility.svg:
--------------------------------------------------------------------------------
1 |
2 | Accessibility
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/(pages)/about/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 | import Contact from '~/components/widgets/Contact';
3 |
4 | import FAQs from '~/components/widgets/FAQs';
5 | import Features from '~/components/widgets/Features';
6 | import Features3 from '~/components/widgets/Features3';
7 | import Features4 from '~/components/widgets/Features4';
8 | import Hero2 from '~/components/widgets/Hero2';
9 | import Stats from '~/components/widgets/Stats';
10 | import Steps from '~/components/widgets/Steps';
11 | import Team2 from '~/components/widgets/Team2';
12 | import Testimonials2 from '~/components/widgets/Testimonials2';
13 | import {
14 | contactAbout,
15 | faqsAbout,
16 | featuresFourAbout,
17 | featuresFourAboutTwo,
18 | features3About,
19 | hero2About,
20 | statsAbout,
21 | stepsAbout,
22 | testimonials2About,
23 | featuresAbout,
24 | teamAbout,
25 | } from '~/shared/data/pages/about.data';
26 |
27 | export const metadata: Metadata = {
28 | title: `About us`,
29 | };
30 |
31 | const Page = () => {
32 | return (
33 | <>
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | >
46 | );
47 | };
48 |
49 | export default Page;
50 |
--------------------------------------------------------------------------------
/src/components/common/Headline.tsx:
--------------------------------------------------------------------------------
1 | import { HeadlineProps } from '~/shared/types';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | const Headline = ({ header, containerClass, titleClass, subtitleClass }: HeadlineProps) => {
5 | const { title, subtitle, tagline, position } = header;
6 |
7 | return (
8 |
9 | {(title || subtitle || tagline) && (
10 |
18 | {tagline && (
19 |
20 | {tagline}
21 |
22 | )}
23 | {title &&
{title} }
24 | {subtitle && (
25 |
33 | {subtitle}
34 |
35 | )}
36 |
37 | )}
38 |
39 | );
40 | };
41 |
42 | export default Headline;
43 |
--------------------------------------------------------------------------------
/src/components/widgets/Announcement.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import { announcementData } from '~/shared/data/global.data';
3 |
4 | const Announcement = () => {
5 | const { title, callToAction, callToAction2 } = announcementData;
6 |
7 | return (
8 |
37 | );
38 | };
39 |
40 | export default Announcement;
41 |
--------------------------------------------------------------------------------
/src/components/widgets/Team2.tsx:
--------------------------------------------------------------------------------
1 | import Headline from '../common/Headline';
2 | import { TeamProps } from '~/shared/types';
3 | import WidgetWrapper from '../common/WidgetWrapper';
4 | import ItemTeam from '../common/ItemTeam';
5 |
6 | const Team = ({ header, teams, id, hasBackground = false }: TeamProps) => (
7 |
8 | {header && }
9 |
10 |
11 | {teams.map(({ name, occupation, image, items }, index) => (
12 |
13 |
25 |
26 | ))}
27 |
28 |
29 |
30 | );
31 |
32 | export default Team;
33 |
--------------------------------------------------------------------------------
/src/stories/widgets/Content.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 |
3 | import Component from '~/components/widgets/Content';
4 | import { contentHomeOne as mockData } from '~/shared/data/pages/home.data';
5 |
6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7 | const meta = {
8 | title: 'Widgets/Content',
9 | component: Component,
10 | parameters: {
11 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
12 | layout: 'fullscreen',
13 | },
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | tags: ['autodocs'],
16 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
17 | argTypes: {},
18 | } satisfies Meta;
19 |
20 | export default meta;
21 | type Story = StoryObj;
22 |
23 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24 | export const Base: Story = {
25 | args: {
26 | ...mockData,
27 | hasBackground: false,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const Reverse: Story = {
39 | args: {
40 | ...mockData,
41 | isReversed: true,
42 | },
43 | };
44 |
45 | export const Mobile: Story = {
46 | args: {
47 | ...mockData,
48 | },
49 | parameters: {
50 | viewport: {
51 | defaultViewport: 'SMALL',
52 | },
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/src/components/widgets/Team.tsx:
--------------------------------------------------------------------------------
1 | import Headline from '../common/Headline';
2 | import { TeamProps } from '~/shared/types';
3 | import WidgetWrapper from '../common/WidgetWrapper';
4 | import ItemTeam from '../common/ItemTeam';
5 |
6 | const Team = ({ header, teams, id, hasBackground = false }: TeamProps) => (
7 |
8 | {header && }
9 |
10 |
11 | {teams.map(({ name, occupation, image, items }, index) => (
12 |
13 |
25 |
26 | ))}
27 |
28 |
29 |
30 | );
31 |
32 | export default Team;
33 |
--------------------------------------------------------------------------------
/src/stories/widgets/FAQs.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { IconChevronsRight } from '@tabler/icons-react';
3 |
4 | import Component from '~/components/widgets/FAQs';
5 | import { faqs2Home as mockData } from '~/shared/data/pages/home.data';
6 |
7 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
8 | const meta = {
9 | title: 'Widgets/FAQs',
10 | component: Component,
11 | parameters: {
12 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
13 | layout: 'fullscreen',
14 | },
15 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
16 | tags: ['autodocs'],
17 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
18 | argTypes: {},
19 | } satisfies Meta;
20 |
21 | export default meta;
22 | type Story = StoryObj;
23 |
24 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
25 | export const Base: Story = {
26 | args: {
27 | ...mockData,
28 | },
29 | };
30 |
31 | export const WithBackground: Story = {
32 | args: {
33 | ...mockData,
34 | hasBackground: true,
35 | },
36 | };
37 |
38 | export const OneColumn: Story = {
39 | args: {
40 | ...mockData,
41 | columns: 1,
42 | },
43 | };
44 |
45 | export const Mobile: Story = {
46 | args: {
47 | ...mockData,
48 | },
49 | parameters: {
50 | viewport: {
51 | defaultViewport: 'SMALL',
52 | },
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/src/utils/posts.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import matter from 'gray-matter';
3 | import { join } from 'path';
4 |
5 | const BLOG_DIR = join(process.cwd(), 'src/content/blog');
6 |
7 | const load = () => {
8 | const files = fs.readdirSync(BLOG_DIR);
9 |
10 | const posts = Promise.all(
11 | files
12 | .filter((filename) => filename.endsWith('.md'))
13 | .map(async (filename) => {
14 | const slug = filename.replace('.md', '');
15 | return await findPostBySlug(slug);
16 | }),
17 | );
18 |
19 | return posts;
20 | };
21 |
22 | let _posts;
23 |
24 | /** */
25 | export const fetchPosts = async () => {
26 | _posts = _posts || load();
27 |
28 | return await _posts;
29 | };
30 |
31 | /** */
32 | export const findLatestPosts = async ({ count } = {}) => {
33 | const _count = count || 4;
34 | const posts = await fetchPosts();
35 |
36 | return posts ? posts.slice(_count * -1) : [];
37 | };
38 |
39 | /** */
40 | export const findPostBySlug = async (slug) => {
41 | if (!slug) return null;
42 |
43 | try {
44 | const readFile = fs.readFileSync(join(BLOG_DIR, `${slug}.md`), 'utf-8');
45 | const { data: frontmatter, content } = matter(readFile);
46 | return {
47 | slug,
48 | ...frontmatter,
49 | content,
50 | };
51 | } catch (e) {}
52 |
53 | return null;
54 | };
55 |
56 | /** */
57 | export const findPostsByIds = async (ids) => {
58 | if (!Array.isArray(ids)) return [];
59 |
60 | const posts = await fetchPosts();
61 |
62 | return ids.reduce(function (r, id) {
63 | posts.some(function (post) {
64 | return id === post.id && r.push(post);
65 | });
66 | return r;
67 | }, []);
68 | };
69 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 |
3 | import { SITE } from '~/config.js';
4 |
5 | import Hero from '~/components/widgets/Hero';
6 | import SocialProof from '../src/components/widgets/SocialProof';
7 | import Features from '~/components/widgets/Features';
8 | import Content from '~/components/widgets/Content';
9 | import Steps from '~/components/widgets/Steps';
10 | import Testimonials from '~/components/widgets/Testimonials';
11 | import FAQs2 from '~/components/widgets/FAQs2';
12 | import Pricing from '~/components/widgets/Pricing';
13 | import Team from '~/components/widgets/Team';
14 | import CallToAction2 from '~/components/widgets/CallToAction2';
15 | import Contact from '~/components/widgets/Contact';
16 | import {
17 | callToAction2Home,
18 | contactHome,
19 | contentHomeOne,
20 | contentHomeTwo,
21 | faqs2Home,
22 | featuresHome,
23 | heroHome,
24 | pricingHome,
25 | socialProofHome,
26 | stepsHome,
27 | teamHome,
28 | testimonialsHome,
29 | } from '~/shared/data/pages/home.data';
30 |
31 | export const metadata: Metadata = {
32 | title: SITE.title,
33 | };
34 |
35 | export default function Page() {
36 | return (
37 | <>
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | >
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/widgets/Features4.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { FeaturesProps } from '~/shared/types';
3 | import WidgetWrapper from '../common/WidgetWrapper';
4 | import Headline from '../common/Headline';
5 | import ItemGrid from '../common/ItemGrid';
6 |
7 | const Features4 = ({
8 | header,
9 | items,
10 | columns = 2,
11 | image,
12 | isBeforeContent,
13 | isAfterContent,
14 | id,
15 | hasBackground = false,
16 | isImageDisplayed = true,
17 | }: FeaturesProps) => (
18 |
23 | {header && }
24 | {isImageDisplayed && (
25 |
26 | {image && (
27 |
35 | )}
36 |
37 | )}
38 |
48 |
49 | );
50 |
51 | export default Features4;
52 |
--------------------------------------------------------------------------------
/.storybook/preview.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { Preview, ReactRenderer } from '@storybook/react';
3 | import { withThemeByClassName } from '@storybook/addon-themes';
4 | import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
5 |
6 | import { Inter as CustomFont } from 'next/font/google';
7 | import '~/assets/styles/base.css';
8 |
9 | const customFont = CustomFont({ subsets: ['latin'], variable: '--font-custom' });
10 |
11 | const CUSTOM_VIEWPORTS = {
12 | SMALL: {
13 | name: 'Mobile View',
14 | styles: {
15 | width: '360px',
16 | height: '640px',
17 | },
18 | type: 'mobile',
19 | },
20 | MEDIUM: {
21 | name: 'Tablet View',
22 | styles: {
23 | width: '960px',
24 | height: '640px',
25 | },
26 | type: 'tablet',
27 | },
28 | };
29 |
30 | const preview: Preview = {
31 | parameters: {
32 | actions: { argTypesRegex: '^on[A-Z].*' },
33 | controls: {
34 | matchers: {
35 | color: /(background|color)$/i,
36 | date: /Date$/i,
37 | },
38 | },
39 | viewport: {
40 | viewports: {
41 | ...CUSTOM_VIEWPORTS,
42 | ...INITIAL_VIEWPORTS,
43 | },
44 | },
45 | backgrounds: {
46 | disable: true,
47 | grid: {
48 | disable: true,
49 | },
50 | },
51 | },
52 | decorators: [
53 | (Story) => (
54 |
57 |
58 |
59 | ),
60 | withThemeByClassName({
61 | themes: {
62 | light: '',
63 | dark: 'dark',
64 | },
65 | defaultTheme: 'light',
66 | }),
67 | ],
68 | };
69 |
70 | export default preview;
71 |
--------------------------------------------------------------------------------
/src/assets/styles/base.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer components {
6 | .btn {
7 | @apply inline-flex items-center justify-center rounded-md border border-gray-400 hover:border-gray-600 dark:border-slate-500 dark:hover:border-slate-700 bg-white dark:bg-transparent hover:bg-gray-100 dark:hover:bg-slate-700 text-center text-base text-gray-700 dark:text-slate-300 dark:hover:text-white font-medium leading-snug shadow-md hover:shadow-none transition duration-200 ease-in focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-blue-200 py-3 px-6 md:px-8;
8 | }
9 |
10 | .btn-ghost {
11 | @apply border-none bg-transparent text-gray-700 dark:text-gray-400 dark:hover:text-white hover:text-gray-900 shadow-none;
12 | }
13 |
14 | .btn-primary {
15 | @apply border-primary-600 dark:border-primary-700 hover:border-primary-800 dark:hover:border-primary-900 bg-primary-600 dark:bg-primary-700 hover:bg-primary-800 dark:hover:bg-primary-900 font-semibold text-white dark:text-white hover:text-white;
16 | }
17 |
18 | .card {
19 | @apply rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow px-6 py-8 w-full;
20 | }
21 |
22 | .ribbon {
23 | @apply absolute top-[19px] right-[-21px] block w-full rotate-45 bg-green-700 text-center text-[10px] font-bold uppercase leading-5 text-white shadow-[0_3px_10px_-5px_rgba(0,0,0,0.3)] before:absolute before:left-0 before:top-full before:z-[-1] before:border-[3px] before:border-r-transparent before:border-b-transparent before:border-l-green-800 before:border-t-green-800 before:content-[''] after:absolute after:right-0 after:top-full after:z-[-1] after:border-[3px] after:border-l-transparent after:border-b-transparent after:border-r-green-800 after:border-t-green-800 after:content-[''];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/common/ItemTeam.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { twMerge } from 'tailwind-merge';
3 | import { Team } from '~/shared/types';
4 |
5 | const ItemTeam = ({
6 | name,
7 | occupation,
8 | image,
9 | items,
10 | containerClass,
11 | imageClass,
12 | panelClass,
13 | nameClass,
14 | occupationClass,
15 | itemsClass,
16 | }: Team) => {
17 | return (
18 |
19 |
20 |
21 |
{name}
22 |
{occupation}
23 |
24 | {items &&
25 | items.map(
26 | ({ title, href, icon: Icon }, index2) =>
27 | Icon &&
28 | href && (
29 |
33 |
40 |
41 |
42 |
43 | ),
44 | )}
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default ItemTeam;
52 |
--------------------------------------------------------------------------------
/src/components/widgets/FAQs3.tsx:
--------------------------------------------------------------------------------
1 | import Headline from '../common/Headline';
2 | import Collapse from '../common/Collapse';
3 | import { IconMinus, IconPlus } from '@tabler/icons-react';
4 | import { CallToActionType, FAQsProps, Item } from '~/shared/types';
5 | import CTA from '../common/CTA';
6 | import WidgetWrapper from '../common/WidgetWrapper';
7 |
8 | const FAQs3 = ({ header, items, callToAction, id, hasBackground = false }: FAQsProps) => (
9 |
10 |
11 |
12 |
15 | {header && (
16 |
21 | )}
22 | {callToAction && (
23 |
27 | )}
28 |
29 |
30 | }
34 | iconDown={ }
35 | />
36 |
37 |
38 |
39 |
40 | );
41 |
42 | export default FAQs3;
43 |
--------------------------------------------------------------------------------
/src/components/widgets/Steps.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { IconCheck } from '@tabler/icons-react';
3 | import { StepsProps } from '~/shared/types';
4 | import WidgetWrapper from '../common/WidgetWrapper';
5 | import Timeline from '../common/Timeline';
6 | import Headline from '../common/Headline';
7 |
8 | const Steps = ({
9 | id,
10 | header,
11 | items,
12 | isImageDisplayed = true,
13 | image,
14 | isReversed = false,
15 | hasBackground = false,
16 | }: StepsProps) => (
17 |
18 |
23 |
28 | {header && (
29 |
35 | )}
36 |
37 |
38 | {isImageDisplayed && (
39 |
40 | {image && (
41 |
50 | )}
51 |
52 | )}
53 |
54 |
55 | );
56 |
57 | export default Steps;
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@onwidget/tailnext",
3 | "description": "A template to make your website using Next.js + Tailwind CSS.",
4 | "version": "1.0.0-beta.4",
5 | "private": true,
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint",
11 | "postbuild": "next-sitemap",
12 | "prettier": "prettier --write --ignore-unknown .",
13 | "prettier:check": "prettier --check --ignore-unknown .",
14 | "storybook": "storybook dev -p 6006",
15 | "build-storybook": "storybook build"
16 | },
17 | "dependencies": {
18 | "@tabler/icons-react": "^3.12.0",
19 | "gray-matter": "^4.0.3",
20 | "markdown-it": "^14.1.0",
21 | "next": "^14.2.6",
22 | "next-themes": "^0.3.0",
23 | "react": "^18.3.1",
24 | "react-dom": "^18.3.1",
25 | "sharp": "^0.33.5",
26 | "tailwind-merge": "^2.5.2"
27 | },
28 | "devDependencies": {
29 | "@storybook/addon-a11y": "^7.6.10",
30 | "@storybook/addon-essentials": "^7.6.10",
31 | "@storybook/addon-interactions": "^7.6.10",
32 | "@storybook/addon-links": "^7.6.10",
33 | "@storybook/addon-onboarding": "^1.0.10",
34 | "@storybook/addon-themes": "^7.6.10",
35 | "@storybook/addon-viewport": "^7.6.10",
36 | "@storybook/blocks": "^7.6.10",
37 | "@storybook/nextjs": "^7.6.10",
38 | "@storybook/react": "^7.6.10",
39 | "@storybook/test": "^7.6.10",
40 | "@tailwindcss/typography": "^0.5.14",
41 | "@types/markdown-it": "^14.1.2",
42 | "@types/node": "22.5.0",
43 | "@types/react": "18.3.4",
44 | "@types/react-dom": "18.3.0",
45 | "autoprefixer": "^10.4.20",
46 | "eslint": "8.56.0",
47 | "eslint-config-next": "^14.2.6",
48 | "eslint-plugin-storybook": "^0.8.0",
49 | "next-sitemap": "^4.2.3",
50 | "postcss": "^8.4.41",
51 | "prettier": "3.3.3",
52 | "prettier-plugin-tailwindcss": "0.6.6",
53 | "storybook": "^7.6.10",
54 | "tailwindcss": "^3.4.10",
55 | "typescript": "^5.5.4"
56 | },
57 | "engines": {
58 | "node": ">=18.17.0"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/widgets/Hero.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { HeroProps } from '~/shared/types';
3 | import CTA from '../common/CTA';
4 |
5 | const Hero = ({ title, subtitle, tagline, callToAction, callToAction2, image }: HeroProps) => {
6 | return (
7 |
8 |
9 |
10 |
11 | {tagline && (
12 |
13 | {tagline}
14 |
15 | )}
16 | {title && (
17 |
18 | {title}
19 |
20 | )}
21 |
22 | {subtitle &&
{subtitle}
}
23 |
24 | {callToAction && }
25 | {callToAction2 && }
26 |
27 |
28 |
29 | {image && (
30 |
31 |
42 |
43 | )}
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default Hero;
51 |
--------------------------------------------------------------------------------
/src/components/common/Timeline.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from 'tailwind-merge';
2 | import { Timeline as TimelineType } from '~/shared/types';
3 |
4 | const Timeline = ({
5 | id,
6 | items,
7 | defaultIcon: DefaultIcon,
8 | containerClass,
9 | panelClass,
10 | iconClass,
11 | titleClass,
12 | descriptionClass,
13 | }: TimelineType) => {
14 | return (
15 | <>
16 | {items && items.length && (
17 |
18 | {items.map(({ title, description, icon: Icon }, index = 0) => (
19 |
20 |
21 |
26 | {Icon ? (
27 |
28 | ) : DefaultIcon ? (
29 |
30 | ) : null}
31 |
32 |
33 | {index !== items.length - 1 &&
}
34 |
35 |
36 | {title && (
37 |
38 | {title}
39 |
40 | )}
41 | {description && (
42 |
{description}
43 | )}
44 |
45 |
46 | ))}
47 |
48 | )}
49 | >
50 | );
51 | };
52 |
53 | export default Timeline;
54 |
--------------------------------------------------------------------------------
/src/components/common/Collapse.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { IconChevronDown, IconChevronUp } from '@tabler/icons-react';
4 | import useCollapse from '~/hooks/useCollapse';
5 | import { CollapseProps } from '~/shared/types';
6 |
7 | const Collapse = ({ items, classCollapseItem, iconUp, iconDown }: CollapseProps) => {
8 | const { activeIndex, handleSetIndex } = useCollapse();
9 |
10 | return (
11 | <>
12 | {items.map(({ title, description }, index) => (
13 | handleSetIndex(index)}
16 | className="mx-auto max-w-3xl select-none bg-transparent text-base text-gray-700"
17 | >
18 |
19 |
27 |
{title}
28 | {iconDown && iconUp ? (
29 | activeIndex === index ? (
30 | iconUp
31 | ) : (
32 | iconDown
33 | )
34 | ) : activeIndex === index ? (
35 |
36 | ) : (
37 |
38 | )}
39 |
40 | {activeIndex === index && (
41 |
48 | )}
49 |
50 |
51 | ))}
52 | >
53 | );
54 | };
55 |
56 | export default Collapse;
57 |
--------------------------------------------------------------------------------
/src/stories/widgets/Features2.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Title, Subtitle, Description, Primary, Controls, Story, Stories } from '@storybook/blocks';
3 |
4 | import Component from '~/components/widgets/Features2';
5 | import { featuresHome as mockData } from '~/shared/data/pages/home.data';
6 |
7 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
8 | const meta = {
9 | title: 'Widgets/Features2',
10 | component: Component,
11 | parameters: {
12 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
13 | layout: 'fullscreen',
14 | // Offers several doc blocks to help document your components. More info: https://storybook.js.org/docs/writing-docs/doc-blocks
15 | docs: {
16 | page: () => (
17 | <>
18 |
19 |
20 |
21 |
22 |
23 |
24 | >
25 | ),
26 | },
27 | },
28 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
29 | tags: ['autodocs'],
30 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
31 | argTypes: {},
32 | } satisfies Meta;
33 |
34 | export default meta;
35 | type Story = StoryObj;
36 |
37 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
38 | export const Base: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | };
43 |
44 | export const OneColumn: Story = {
45 | args: {
46 | ...mockData,
47 | columns: 1,
48 | },
49 | };
50 |
51 | export const TwoColumns: Story = {
52 | args: {
53 | ...mockData,
54 | columns: 2,
55 | },
56 | };
57 |
58 | export const Mobile: Story = {
59 | args: {
60 | ...mockData,
61 | },
62 | parameters: {
63 | viewport: {
64 | defaultViewport: 'SMALL',
65 | },
66 | },
67 | };
68 |
--------------------------------------------------------------------------------
/src/components/common/ItemTestimonial.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { twMerge } from 'tailwind-merge';
3 | import { Testimonial } from '~/shared/types';
4 | import DividerLine from './DividerLine';
5 |
6 | const ItemTestimonial = ({
7 | name,
8 | job,
9 | testimonial,
10 | image,
11 | isTestimonialUp,
12 | hasDividerLine,
13 | startSlice,
14 | endSlice,
15 | containerClass,
16 | panelClass,
17 | imageClass,
18 | dataClass,
19 | nameJobClass,
20 | nameClass,
21 | jobClass,
22 | testimonialClass,
23 | }: Testimonial) => {
24 | return (
25 |
26 |
27 | {((image && name) || (name && job)) && (
28 | <>
29 |
30 | {image && (
31 |
38 | )}
39 |
40 |
41 | {name &&
{name} }
42 | {job && {job} }
43 |
44 |
45 |
46 | {hasDividerLine &&
}
47 | >
48 | )}
49 |
50 | {testimonial && (
51 |
52 |
53 | {startSlice !== undefined && endSlice !== undefined
54 | ? `" ${testimonial.slice(Number(startSlice), Number(endSlice))}... "`
55 | : `" ${testimonial} "`}
56 |
57 |
58 | )}
59 |
60 |
61 | );
62 | };
63 |
64 | export default ItemTestimonial;
65 |
--------------------------------------------------------------------------------
/src/stories/widgets/Steps.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Title, Subtitle, Description, Primary, Controls, Story, Stories } from '@storybook/blocks';
3 |
4 | import Component from '~/components/widgets/Steps';
5 | import { stepsHome as mockData } from '~/shared/data/pages/home.data';
6 |
7 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
8 | const meta = {
9 | title: 'Widgets/Steps',
10 | component: Component,
11 | parameters: {
12 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
13 | layout: 'fullscreen',
14 | // Offers several doc blocks to help document your components. More info: https://storybook.js.org/docs/writing-docs/doc-blocks
15 | docs: {
16 | page: () => (
17 | <>
18 |
19 |
20 |
21 |
22 |
23 |
24 | >
25 | ),
26 | },
27 | },
28 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
29 | tags: ['autodocs'],
30 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
31 | argTypes: {},
32 | } satisfies Meta;
33 |
34 | export default meta;
35 | type Story = StoryObj;
36 |
37 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
38 | export const Base: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | };
43 |
44 | export const WithBackground: Story = {
45 | args: {
46 | ...mockData,
47 | hasBackground: true,
48 | },
49 | };
50 |
51 | export const Reverse: Story = {
52 | args: {
53 | ...mockData,
54 | isReversed: true,
55 | },
56 | };
57 |
58 | export const NoImage: Story = {
59 | args: {
60 | ...mockData,
61 | isImageDisplayed: false,
62 | },
63 | };
64 |
65 | export const Mobile: Story = {
66 | args: {
67 | ...mockData,
68 | },
69 | parameters: {
70 | viewport: {
71 | defaultViewport: 'SMALL',
72 | },
73 | },
74 | };
75 |
--------------------------------------------------------------------------------
/src/stories/widgets/Features.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Title, Subtitle, Description, Primary, Controls, Story, Stories } from '@storybook/blocks';
3 |
4 | import Component from '~/components/widgets/Features';
5 | import { featuresHome as mockData } from '~/shared/data/pages/home.data';
6 |
7 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
8 | const meta = {
9 | title: 'Widgets/Features',
10 | component: Component,
11 | parameters: {
12 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
13 | layout: 'fullscreen',
14 | // Offers several doc blocks to help document your components. More info: https://storybook.js.org/docs/writing-docs/doc-blocks
15 | docs: {
16 | page: () => (
17 | <>
18 |
19 |
20 |
21 |
22 |
23 |
24 | >
25 | ),
26 | },
27 | },
28 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
29 | tags: ['autodocs'],
30 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
31 | argTypes: {},
32 | } satisfies Meta;
33 |
34 | export default meta;
35 | type Story = StoryObj;
36 |
37 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
38 | export const Base: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | };
43 |
44 | export const WithBackground: Story = {
45 | args: {
46 | ...mockData,
47 | hasBackground: true,
48 | },
49 | };
50 |
51 | export const OneColumn: Story = {
52 | args: {
53 | ...mockData,
54 | columns: 1,
55 | },
56 | };
57 |
58 | export const TwoColumns: Story = {
59 | args: {
60 | ...mockData,
61 | columns: 2,
62 | },
63 | };
64 |
65 | export const Mobile: Story = {
66 | args: {
67 | ...mockData,
68 | },
69 | parameters: {
70 | viewport: {
71 | defaultViewport: 'SMALL',
72 | },
73 | },
74 | };
75 |
--------------------------------------------------------------------------------
/src/stories/widgets/Features3.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Title, Subtitle, Description, Primary, Controls, Story, Stories } from '@storybook/blocks';
3 |
4 | import Component from '~/components/widgets/Features3';
5 | import { features3About as mockData } from '~/shared/data/pages/about.data';
6 |
7 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
8 | const meta = {
9 | title: 'Widgets/Features3',
10 | component: Component,
11 | parameters: {
12 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
13 | layout: 'fullscreen',
14 | // Offers several doc blocks to help document your components. More info: https://storybook.js.org/docs/writing-docs/doc-blocks
15 | docs: {
16 | page: () => (
17 | <>
18 |
19 |
20 |
21 |
22 |
23 |
24 | >
25 | ),
26 | },
27 | },
28 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
29 | tags: ['autodocs'],
30 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
31 | argTypes: {},
32 | } satisfies Meta;
33 |
34 | export default meta;
35 | type Story = StoryObj;
36 |
37 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
38 | export const Base: Story = {
39 | args: {
40 | ...mockData,
41 | },
42 | };
43 |
44 | export const WithBackground: Story = {
45 | args: {
46 | ...mockData,
47 | hasBackground: true,
48 | },
49 | };
50 |
51 | export const OneColumn: Story = {
52 | args: {
53 | ...mockData,
54 | columns: 1,
55 | },
56 | };
57 |
58 | export const TwoColumns: Story = {
59 | args: {
60 | ...mockData,
61 | columns: 2,
62 | },
63 | };
64 |
65 | export const Mobile: Story = {
66 | args: {
67 | ...mockData,
68 | },
69 | parameters: {
70 | viewport: {
71 | defaultViewport: 'SMALL',
72 | },
73 | },
74 | };
75 |
--------------------------------------------------------------------------------
/src/components/widgets/Contact.tsx:
--------------------------------------------------------------------------------
1 | import Form from '../common/Form';
2 | import Headline from '../common/Headline';
3 | import { ContactProps } from '~/shared/types';
4 | import WidgetWrapper from '../common/WidgetWrapper';
5 |
6 | const Contact = ({ header, content, items, form, id, hasBackground = false }: ContactProps) => (
7 |
8 | {header && }
9 |
10 |
11 |
12 | {content &&
{content}
}
13 |
14 | {items &&
15 | items.map(({ title, description, icon: Icon }, index) => (
16 |
17 |
18 | {Icon && }
19 |
20 |
21 |
{title}
22 | {typeof description === 'string' ? (
23 |
24 | {description}
25 |
26 | ) : (
27 | description &&
28 | description.map((desc, index) => (
29 |
30 | {desc}
31 |
32 | ))
33 | )}
34 |
35 |
36 | ))}
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 |
45 | export default Contact;
46 |
--------------------------------------------------------------------------------
/src/components/widgets/Content.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { IconCheck } from '@tabler/icons-react';
3 |
4 | import { ContentProps } from '~/shared/types';
5 | import Headline from '../common/Headline';
6 | import WidgetWrapper from '../common/WidgetWrapper';
7 | import ItemGrid from '../common/ItemGrid';
8 |
9 | const Content = ({
10 | header,
11 | content,
12 | items,
13 | image,
14 | isReversed,
15 | isAfterContent,
16 | id,
17 | hasBackground = false,
18 | }: ContentProps) => (
19 |
24 | {header && }
25 |
26 |
27 |
28 | {content &&
{content}
}
29 |
39 |
40 |
41 | {image && (
42 |
43 |
53 |
54 | )}
55 |
56 |
57 |
58 |
59 | );
60 |
61 | export default Content;
62 |
--------------------------------------------------------------------------------
/src/stories/assets/discord.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/stories/widgets/Features4.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from '@storybook/react';
2 | import { Title, Subtitle, Description, Primary, Controls, Story, Stories } from '@storybook/blocks';
3 |
4 | import Component from '~/components/widgets/Features4';
5 | import { features4Services as mockData } from '~/shared/data/pages/services.data';
6 |
7 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
8 | const meta = {
9 | title: 'Widgets/Features4',
10 | component: Component,
11 | parameters: {
12 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
13 | layout: 'fullscreen',
14 | // Offers several doc blocks to help document your components. More info: https://storybook.js.org/docs/writing-docs/doc-blocks
15 | docs: {
16 | page: () => (
17 | <>
18 |
19 |
20 |
21 |
22 |
23 |
24 | >
25 | ),
26 | },
27 | },
28 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
29 | tags: ['autodocs'],
30 | // More on argTypes: https://storybook.js.org/docs/api/argtypes
31 | argTypes: {},
32 | } satisfies Meta;
33 |
34 | export default meta;
35 | type Story = StoryObj;
36 |
37 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
38 | export const Base: Story = {
39 | args: {
40 | ...mockData,
41 | hasBackground: false,
42 | },
43 | };
44 |
45 | export const WithBackground: Story = {
46 | args: {
47 | ...mockData,
48 | hasBackground: true,
49 | },
50 | };
51 |
52 | export const OneColumn: Story = {
53 | args: {
54 | ...mockData,
55 | columns: 1,
56 | },
57 | };
58 |
59 | export const ThreeColumns: Story = {
60 | args: {
61 | ...mockData,
62 | columns: 3,
63 | },
64 | };
65 |
66 | export const NoImage: Story = {
67 | args: {
68 | ...mockData,
69 | isImageDisplayed: false,
70 | },
71 | };
72 |
73 | export const Mobile: Story = {
74 | args: {
75 | ...mockData,
76 | },
77 | parameters: {
78 | viewport: {
79 | defaultViewport: 'SMALL',
80 | },
81 | },
82 | };
83 |
--------------------------------------------------------------------------------
/src/components/common/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { IconCheck, IconChevronDown, IconChevronUp } from '@tabler/icons-react';
4 | import { useEffect, useState } from 'react';
5 | import { Dropdown as DropdownType, Tab } from '~/shared/types';
6 |
7 | const Dropdown = ({ options, activeTab, onActiveTabSelected, iconUp, iconDown }: DropdownType) => {
8 | const [isDropdownOpen, setIsDropdownOpen] = useState(false);
9 | const [selectedOption, setSelectedOption] = useState(options[activeTab].link?.label as string);
10 |
11 | const dropdownHandler = (e: React.SyntheticEvent) => {
12 | e.stopPropagation();
13 | setIsDropdownOpen(!isDropdownOpen);
14 | };
15 |
16 | const onOptionSelected = (option: Tab, index: number) => {
17 | setSelectedOption(option.link?.label as string);
18 |
19 | // Sends the value to the parent component
20 | onActiveTabSelected(index);
21 | };
22 |
23 | useEffect(() => {
24 | const handler = () => setIsDropdownOpen(false);
25 |
26 | window.addEventListener('click', handler);
27 |
28 | return () => {
29 | window.removeEventListener('click', handler);
30 | };
31 | });
32 |
33 | return (
34 |
35 |
36 |
{selectedOption}
37 | {iconDown && iconUp ? (
38 | isDropdownOpen === false ? (
39 | iconDown
40 | ) : (
41 | iconUp
42 | )
43 | ) : isDropdownOpen === false ? (
44 |
45 | ) : (
46 |
47 | )}
48 |
49 | {isDropdownOpen && (
50 |
51 | {options.map((option: Tab, index) => (
52 |
onOptionSelected(option, index)}
55 | className={`flex cursor-pointer items-center bg-white p-3 text-lg dark:bg-slate-900 ${
56 | activeTab !== index ? 'pl-10' : 'text-primary-600 dark:text-primary-200'
57 | }`}
58 | >
59 | {activeTab === index && } {option.link?.label}
60 |
61 | ))}
62 |
63 | )}
64 |
65 | );
66 | };
67 |
68 | export default Dropdown;
69 |
--------------------------------------------------------------------------------
/src/stories/assets/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/widgets/Comparison.tsx:
--------------------------------------------------------------------------------
1 | import { IconCheck, IconMinus } from '@tabler/icons-react';
2 | import { CallToActionType, ComparisonProps } from '~/shared/types';
3 | import CTA from '../common/CTA';
4 | import Headline from '../common/Headline';
5 | import WidgetWrapper from '../common/WidgetWrapper';
6 |
7 | const Comparison = ({ header, columns, id, hasBackground = false }: ComparisonProps) => (
8 |
9 | {header && }
10 |
11 | {columns.map(({ title, items, callToAction }, index) => (
12 |
20 |
25 | {title}
26 |
27 | {items &&
28 | items.map(({ title: title2 }, index2) => (
29 |
33 | {(title2 as boolean) === true ? (
34 |
35 | ) : (title2 as boolean) === false ? (
36 |
37 | ) : index !== 0 ? (
38 |
{title2}
39 | ) : (
40 |
{title2}
41 | )}
42 |
43 | ))}
44 | {index !== 0 && callToAction && (
45 |
46 | )}
47 |
48 | ))}
49 |
50 |
51 | );
52 |
53 | export default Comparison;
54 |
--------------------------------------------------------------------------------
/src/components/widgets/Hero2.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { HeroProps } from '~/shared/types';
3 | import CTA from '../common/CTA';
4 |
5 | const Hero2 = ({ title, subtitle, tagline, callToAction, callToAction2, image }: HeroProps) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | {tagline && (
13 |
14 | {tagline}
15 |
16 | )}
17 | {title && (
18 |
19 | {title}
20 |
21 | )}
22 |
23 | {subtitle &&
{subtitle}
}
24 |
25 | {callToAction && }
26 | {callToAction2 && }
27 |
28 |
29 |
30 |
31 |
32 |
33 | {image && (
34 |
45 | )}
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default Hero2;
55 |
--------------------------------------------------------------------------------
/app/(blog)/[slug]/page.jsx:
--------------------------------------------------------------------------------
1 | import md from 'markdown-it';
2 | import Image from 'next/image';
3 | import { notFound } from 'next/navigation';
4 |
5 | import { findPostBySlug, findLatestPosts } from '~/utils/posts';
6 |
7 | export const dynamicParams = false;
8 |
9 | const getFormattedDate = (date) => date;
10 |
11 | export async function generateMetadata({ params}) {
12 | const post = await findPostBySlug(params.slug);
13 | if (!post) {
14 | return notFound();
15 | }
16 | return { title: post.title, description: post.description };
17 | }
18 |
19 | export async function generateStaticParams() {
20 | return (await findLatestPosts()).map(({ slug }) => ({ slug }));
21 | }
22 |
23 | export default async function Page({ params }) {
24 | const post = await findPostBySlug(params.slug);
25 |
26 | if (!post) {
27 | return notFound();
28 | }
29 |
30 | return (
31 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/widgets/Footer2.tsx:
--------------------------------------------------------------------------------
1 | import { footerData2 } from '~/shared/data/global.data';
2 |
3 | const Footer2 = () => {
4 | const { links, columns, socials, footNote } = footerData2;
5 |
6 | return (
7 |
8 |
9 | {columns.map(({ title, texts }, index) => (
10 |
14 |
{title}
15 | {texts &&
16 | texts.map((text, index2) => (
17 |
18 | {text}
19 |
20 | ))}
21 |
22 | ))}
23 |
24 |
Social
25 |
26 | {socials.map(({ label, icon: Icon, href }, index) => (
27 |
28 |
33 | {Icon && }
34 |
35 |
36 | ))}
37 |
38 |
39 |
40 |
41 |
42 | {links &&
43 | links.map(({ label, href }, index) => (
44 |
45 |
50 | {label}
51 |
52 | {links.length - 1 !== index && · }
53 |
54 | ))}
55 |
56 | {footNote}
57 |
58 |
59 | );
60 | };
61 |
62 | export default Footer2;
63 |
--------------------------------------------------------------------------------
/src/components/common/ItemGrid.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from 'tailwind-merge';
2 | import type { ItemGrid as ItemGridType } from '~/shared/types';
3 | import CTA from './CTA';
4 |
5 | const ItemGrid = ({
6 | id,
7 | items,
8 | columns,
9 | defaultColumns,
10 | defaultIcon: DefaultIcon,
11 | containerClass,
12 | panelClass,
13 | iconClass,
14 | titleClass,
15 | descriptionClass,
16 | actionClass,
17 | }: ItemGridType) => {
18 | return (
19 | <>
20 | {items && (
21 |
35 | {items.map(({ title, description, icon: Icon, callToAction }, index) => (
36 |
37 |
38 |
39 | {Icon ? (
40 |
41 | ) : DefaultIcon ? (
42 |
43 | ) : null}
44 |
45 |
46 | {title &&
{title} }
47 | {description && (
48 |
51 | {description}
52 |
53 | )}
54 | {callToAction && (
55 |
64 | )}
65 |
66 |
67 |
68 | ))}
69 |
70 | )}
71 | >
72 | );
73 | };
74 |
75 | export default ItemGrid;
76 |
--------------------------------------------------------------------------------
/src/components/widgets/FAQs4.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Headline from '../common/Headline';
4 | import Collapse from '../common/Collapse';
5 | import { IconMinus, IconPlus } from '@tabler/icons-react';
6 | import { FAQsProps, Item, Tab } from '~/shared/types';
7 | import { useState } from 'react';
8 | import useWindowSize from '~/hooks/useWindowSize';
9 | import Dropdown from '../common/Dropdown';
10 | import WidgetWrapper from '../common/WidgetWrapper';
11 |
12 | const FAQs4 = ({ header, tabs, id, hasBackground = false }: FAQsProps) => {
13 | const { width } = useWindowSize();
14 | const [activeTab, setActiveTab] = useState(0);
15 |
16 | const activeTabSelectedHandler = (index: number) => {
17 | setActiveTab(index);
18 | };
19 |
20 | return (
21 |
22 | {header && }
23 |
24 |
25 | {width > 767 ? (
26 |
27 |
28 |
29 | {(tabs as Tab[]).map((tab, index) => {
30 | const onSelectTab = () => {
31 | setActiveTab(index);
32 | };
33 |
34 | return (
35 |
43 | {tab.link?.label}
44 |
45 | );
46 | })}
47 |
48 |
49 |
50 | ) : (
51 |
52 | )}
53 |
54 | {(tabs as Tab[]).map((tab, index) => (
55 |
56 | {activeTab === index && (
57 | }
61 | iconDown={ }
62 | />
63 | )}
64 |
65 | ))}
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default FAQs4;
74 |
--------------------------------------------------------------------------------
/src/components/widgets/Testimonials.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { TestimonialsProps } from '~/shared/types';
3 | import Headline from '../common/Headline';
4 | import WidgetWrapper from '../common/WidgetWrapper';
5 | import CTA from '../common/CTA';
6 | import ItemTestimonial from '../common/ItemTestimonial';
7 |
8 | const Testimonials = ({
9 | header,
10 | testimonials,
11 | callToAction,
12 | isTestimonialUp,
13 | id,
14 | hasBackground = false,
15 | }: TestimonialsProps) => (
16 |
17 | {header && }
18 |
19 |
20 | {testimonials.map(
21 | ({ name, job, testimonial, image, href }, index) =>
22 | testimonial && (
23 |
31 | {!callToAction && href ? (
32 |
33 |
48 |
49 | ) : (
50 |
65 | )}
66 |
67 | ),
68 | )}
69 |
70 |
71 | {callToAction && (
72 |
77 | )}
78 |
79 | );
80 |
81 | export default Testimonials;
82 |
--------------------------------------------------------------------------------
/src/components/widgets/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { footerData } from '~/shared/data/global.data';
2 |
3 | const Footer = () => {
4 | const { title, links, columns, socials, footNote } = footerData;
5 |
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 | {links &&
20 | links.map(({ label, href }, index) => (
21 |
22 |
27 | {label}
28 |
29 | {links.length - 1 !== index && · }
30 |
31 | ))}
32 |
33 |
34 |
35 | {columns.map(({ title, links }, index) => (
36 |
37 |
{title}
38 |
39 | {links &&
40 | links.map(({ label, href }, index2) => (
41 |
42 |
47 | {label}
48 |
49 |
50 | ))}
51 |
52 |
53 | ))}
54 |
55 |
56 |
57 | {socials.map(({ label, icon: Icon, href }, index) => (
58 |
59 |
64 | {Icon && }
65 |
66 |
67 | ))}
68 |
69 | {footNote}
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default Footer;
77 |
--------------------------------------------------------------------------------
/src/components/widgets/CallToAction2.tsx:
--------------------------------------------------------------------------------
1 | import { IconChevronRight } from '@tabler/icons-react';
2 | import { CallToActionProps, Item } from '~/shared/types';
3 |
4 | const Card = ({ title, description, href, form }: Item) => (
5 |
6 |
7 |
8 |
{title}
9 |
{description}
10 |
11 | {href && (
12 |
13 |
14 |
15 | )}
16 |
17 | {form && (
18 |
42 | )}
43 |
44 | );
45 |
46 | const CallToAction2 = ({ title, subtitle, items }: CallToActionProps) => (
47 |
48 |
49 |
50 |
51 |
{title}
52 |
53 | {subtitle}
54 |
55 |
56 |
57 | {items &&
58 | items.map(({ title, description, href, form }, index) => (
59 |
60 | {href ? (
61 |
68 |
69 |
70 | ) : (
71 |
72 | )}
73 |
74 | ))}
75 |
76 |
77 |
78 |
79 | );
80 |
81 | export default CallToAction2;
82 |
--------------------------------------------------------------------------------
/src/components/widgets/Testimonials2.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { TestimonialsProps } from '~/shared/types';
4 | import Headline from '../common/Headline';
5 | import WidgetWrapper from '../common/WidgetWrapper';
6 | import ItemTestimonial from '../common/ItemTestimonial';
7 | import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
8 | import { useState } from 'react';
9 |
10 | const Testimonials2 = ({ header, testimonials, isTestimonialUp, id, hasBackground = false }: TestimonialsProps) => {
11 | const [activeIndex, setActiveIndex] = useState(0);
12 |
13 | const firstIndex = 0;
14 | const lastIndex = testimonials.length - 1;
15 |
16 | const handleGoToPrevious = (index: number) => {
17 | if (activeIndex > firstIndex) {
18 | setActiveIndex(index - 1);
19 | }
20 | };
21 |
22 | const handleGoToNext = (index: number) => {
23 | if (activeIndex < lastIndex) {
24 | setActiveIndex(index + 1);
25 | }
26 | };
27 |
28 | return (
29 |
30 | {header && }
31 |
32 | {testimonials.map(
33 | ({ name, job, testimonial, image }, index) =>
34 | testimonial && (
35 |
40 |
55 |
56 | handleGoToPrevious(index)}>
57 |
64 |
65 | handleGoToNext(index)}>
66 |
73 |
74 |
75 |
76 | ),
77 | )}
78 |
79 |
80 | );
81 | };
82 |
83 | export default Testimonials2;
84 |
--------------------------------------------------------------------------------
/src/shared/data/pages/contact.data.tsx:
--------------------------------------------------------------------------------
1 | import { IconClock, IconHeadset, IconHelp, IconMapPin, IconMessages, IconPhoneCall } from '@tabler/icons-react';
2 | import { ContactProps, FeaturesProps } from '~/shared/types';
3 | import { HeroProps } from '~/shared/types';
4 |
5 | // Hero data on Contact page *******************
6 | export const heroContact: HeroProps = {
7 | title: 'Get in touch with us',
8 | subtitle: (
9 | <>
10 | {`Thank you for considering us for your project! We're excited to hear from you.`} {' '}
11 | {`Our team can assist you in building your dream website.`}
12 | >
13 | ),
14 | tagline: 'Demo Contact Page',
15 | };
16 |
17 | // Contact data on Contact page *******************
18 | export const contact2Contact: ContactProps = {
19 | id: 'contactTwo-on-contact',
20 | hasBackground: true,
21 | header: {
22 | title: 'Contact us',
23 | subtitle: (
24 | <>
25 | Please take a moment to fill out this form.{' '}
26 | {`So we can better understand your needs and get the process started smoothly.`}
27 | >
28 | ),
29 | },
30 | items: [
31 | {
32 | title: 'Our Address',
33 | description: ['1230 Maecenas Street Donec Road', 'New York, EEUU'],
34 | icon: IconMapPin,
35 | },
36 | {
37 | title: 'Contact',
38 | description: ['Mobile: +1 (123) 456-7890', 'Mail: tailnext@gmail.com'],
39 | icon: IconPhoneCall,
40 | },
41 | {
42 | title: 'Working hours',
43 | description: ['Monday - Friday: 08:00 - 17:00', 'Saturday & Sunday: 08:00 - 12:00'],
44 | icon: IconClock,
45 | },
46 | ],
47 | form: {
48 | title: 'Ready to Get Started?',
49 | inputs: [
50 | {
51 | type: 'text',
52 | label: 'First name',
53 | name: 'name',
54 | autocomplete: 'off',
55 | placeholder: 'First name',
56 | },
57 | {
58 | type: 'text',
59 | label: 'Last name',
60 | name: 'lastName',
61 | autocomplete: 'off',
62 | placeholder: 'Last name',
63 | },
64 | {
65 | type: 'email',
66 | label: 'Email address',
67 | name: 'email',
68 | autocomplete: 'on',
69 | placeholder: 'Email address',
70 | },
71 | ],
72 | radioBtns: {
73 | label: 'What is the reason for your contact?',
74 | radios: [
75 | {
76 | label: 'General inquiries',
77 | },
78 | {
79 | label: 'Technical help',
80 | },
81 | {
82 | label: 'Claims',
83 | },
84 | {
85 | label: 'Others',
86 | },
87 | ],
88 | },
89 | textarea: {
90 | cols: 30,
91 | rows: 5,
92 | label: 'How can we help you?',
93 | name: 'textarea',
94 | placeholder: 'Write your message...',
95 | },
96 | checkboxes: [
97 | {
98 | label: 'Have you read our privacy policy?',
99 | value: '',
100 | },
101 | {
102 | label: 'Do you want to receive monthly updates by email?',
103 | value: '',
104 | },
105 | ],
106 | btn: {
107 | title: 'Send Message',
108 | type: 'submit',
109 | },
110 | },
111 | };
112 |
113 | // Feature2 data on Contact page *******************
114 | export const features2Contact: FeaturesProps = {
115 | columns: 3,
116 | header: {
117 | title: 'Support Center',
118 | subtitle: 'Looking for something in particular?',
119 | },
120 | items: [
121 | {
122 | title: 'Have a question?',
123 | description: 'See our frequently asked questions',
124 | icon: IconHelp,
125 | callToAction: {
126 | text: 'Go to FAQ page',
127 | href: '/faqs',
128 | },
129 | },
130 | {
131 | title: 'Chat with us',
132 | description: 'Live chat with our support team',
133 | icon: IconMessages,
134 | callToAction: {
135 | text: 'Write to us',
136 | href: '/',
137 | },
138 | },
139 | {
140 | title: 'Get help',
141 | description: 'Speak to our team today',
142 | icon: IconHeadset,
143 | callToAction: {
144 | text: 'Call us',
145 | href: '/',
146 | },
147 | },
148 | ],
149 | };
150 |
--------------------------------------------------------------------------------
/src/components/widgets/Pricing.tsx:
--------------------------------------------------------------------------------
1 | import { CallToActionType, PricingProps } from '~/shared/types';
2 | import CTA from '../common/CTA';
3 | import Headline from '../common/Headline';
4 | import WidgetWrapper from '../common/WidgetWrapper';
5 | import ItemGrid from '../common/ItemGrid';
6 | import { IconCheck } from '@tabler/icons-react';
7 |
8 | const Pricing = ({ header, prices, id, hasBackground = false }: PricingProps) => (
9 |
10 | {header && }
11 |
12 |
13 | {prices &&
14 | prices.map(
15 | ({ title, subtitle, price, period, items, callToAction, hasRibbon = false, ribbonTitle }, index) => (
16 |
20 | {price && period && (
21 |
22 | {hasRibbon && ribbonTitle && (
23 |
24 |
25 | {ribbonTitle}
26 |
27 |
28 | )}
29 |
30 | {title && (
31 |
32 | {title}
33 |
34 | )}
35 | {subtitle && (
36 |
{subtitle}
37 | )}
38 |
39 |
40 | $
41 | {price}
42 |
43 |
44 | {period}
45 |
46 |
47 | {items && (
48 |
49 |
58 |
59 | )}
60 |
61 | {callToAction && (
62 |
66 | )}
67 |
68 | )}
69 |
70 | ),
71 | )}
72 |
73 |
74 |
75 | );
76 |
77 | export default Pricing;
78 |
--------------------------------------------------------------------------------
/src/components/common/Form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState } from 'react';
4 | import { twMerge } from 'tailwind-merge';
5 | import { FormProps } from '../../shared/types';
6 |
7 | const Form = ({
8 | title,
9 | description,
10 | inputs,
11 | radioBtns,
12 | textarea,
13 | checkboxes,
14 | btn,
15 | btnPosition,
16 | containerClass,
17 | }: FormProps) => {
18 | const [inputValues, setInputValues] = useState([]);
19 | const [radioBtnValue, setRadioBtnValue] = useState('');
20 | const [textareaValues, setTextareaValues] = useState('');
21 | const [checkedState, setCheckedState] = useState(new Array(checkboxes && checkboxes.length).fill(false));
22 |
23 | // Update the value of the entry fields
24 | const changeInputValueHandler = (event: React.ChangeEvent) => {
25 | const { name, value } = event.target;
26 |
27 | setInputValues({
28 | ...inputValues,
29 | [name]: value,
30 | });
31 | };
32 |
33 | // Update checked radio buttons
34 | const changeRadioBtnsHandler = (event: React.ChangeEvent) => {
35 | setRadioBtnValue(event.target.value);
36 | };
37 |
38 | // Update the textarea value
39 | const changeTextareaHandler = (event: React.ChangeEvent) => {
40 | setTextareaValues(event.target.value);
41 | };
42 |
43 | // Update checkbox radio buttons
44 | const changeCheckboxHandler = (index: number) => {
45 | setCheckedState((prevValues) => {
46 | const newValues = [...(prevValues as boolean[])];
47 | newValues.map(() => {
48 | newValues[index] = !checkedState[index];
49 | });
50 | return newValues;
51 | });
52 | };
53 |
54 | return (
55 |
153 | );
154 | };
155 |
156 | export default Form;
157 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tailnext
2 |
3 | **Tailnext** is a free and open-source template to make your website using **[NextJS](https://nextjs.org/) + [Tailwind CSS](https://tailwindcss.com/)**. Ready to start a new project and designed taking into account best practices.
4 |
5 | ## Features
6 |
7 | - ✅ Integration with **Tailwind CSS** supporting **Dark mode**.
8 | - ✅ **Production-ready** scores in [Lighthouse](https://web.dev/measure/) and [PageSpeed Insights](https://pagespeed.web.dev/) reports.
9 | - ✅ **Image optimization** and **Font optimization**.
10 | - ✅ Fast and **SEO friendly blog**.
11 | - ✅ Generation of **project sitemap** and **robots.txt** based on your routes.
12 |
13 |
14 |
15 |
16 |
17 | [](https://onwidget.com)
18 | [](https://github.com/onwidget/tailnext/blob/main/LICENSE.md)
19 | [](https://github.com/onwidget)
20 | [](https://github.com/onwidget/tailnext#contributing)
21 | [](https://snyk.io/test/github/onwidget/tailnext)
22 |
23 |
24 |
25 |
26 | Table of Contents
27 |
28 | - [Demo](#demo)
29 | - [Getting started](#getting-started)
30 | - [Project structure](#project-structure)
31 | - [Commands](#commands)
32 | - [Configuration](#configuration)
33 | - [Deploy](#deploy)
34 | - [Roadmap](#roadmap)
35 | - [Contributing](#contributing)
36 | - [Acknowledgements](#acknowledgements)
37 | - [License](#license)
38 |
39 |
40 |
41 |
42 |
43 | ## Demo
44 |
45 | 📌 [https://tailnext.vercel.app/](https://tailnext.vercel.app/)
46 |
47 |
48 |
49 | ## Getting started
50 |
51 | - Clone: `git clone https://github.com/onwidget/tailnext.git`
52 | - Enter in the directory: `cd tailnext`
53 | - Install dependencies: `npm install`
54 | - Start the development server: `npm run dev`
55 | - View project in local environment: `localhost:3000`
56 |
57 | ### Project structure
58 |
59 | Inside **Tailnext** template, you'll see the following folders and files:
60 |
61 | ```
62 | /
63 | ├── .storybook/
64 | ├── app/
65 | │ ├── (blog)
66 | │ │ ├── [slug]
67 | | | | └── page.js
68 | | | └── blog
69 | | | └── page.js
70 | │ ├── head.js
71 | │ ├── layout.js
72 | │ └── page.js
73 | ├── public/
74 | │ └── favicon.svg
75 | ├── src/
76 | │ ├── assets/
77 | │ │ ├── images/
78 | | | └── styles/
79 | | | └── base.css
80 | │ ├── components/
81 | │ │ ├── atoms/
82 | | | └── widgets/
83 | | | ├── Header.astro
84 | | | ├── Footer.astro
85 | | | └── ...
86 | │ │── content/
87 | │ | └── blog/
88 | │ | ├── demo-post-1.md
89 | │ | └── ...
90 | │ ├── stories/
91 | │ ├── utils/
92 | │ └── config.mjs
93 | ├── package.json
94 | └── ...
95 | ```
96 |
97 | [](https://githubbox.com/onwidget/tailnext/tree/main)
98 |
99 | > **Seasoned next.js expert?** Delete this file. Update `config.mjs` and contents. Have fun!
100 |
101 |
102 |
103 | ### Commands
104 |
105 | All commands are run from the root of the project, from a terminal:
106 |
107 | | Command | Action |
108 | | :-------------------- | :------------------------------------------- |
109 | | `npm install` | Install dependencies |
110 | | `npm run dev` | Starts local dev server at `localhost:3000` |
111 | | `npm run build` | Build your production site to `./dist/` |
112 | | `npm run preview` | Preview your build locally, before deploying |
113 | | `npm run storybook` | Open storybook to view stories by widgets |
114 | | `npm run format` | Format codes with Prettier |
115 | | `npm run lint:eslint` | Run Eslint |
116 |
117 |
118 |
119 | ### Configuration
120 |
121 | Coming soon ..
122 |
123 |
124 |
125 | ### Deploy
126 |
127 | #### Deploy to production (manual)
128 |
129 | You can create an optimized production build with:
130 |
131 | ```shell
132 | npm run build
133 | ```
134 |
135 | Now, your website is ready to be deployed. All generated files are located at
136 | `dist` folder, which you can deploy the folder to any hosting service you
137 | prefer.
138 |
139 | #### Deploy to Netlify
140 |
141 | Clone this repository on own GitHub account and deploy to Netlify:
142 |
143 | [](https://app.netlify.com/start/deploy?repository=https://github.com/onwidget/tailnext.git)
144 |
145 | #### Deploy to Vercel
146 |
147 | Clone this repository on own GitHub account and deploy to Vercel:
148 |
149 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fonwidget%2Ftailnext)
150 |
151 |
152 |
153 | ## Roadmap
154 |
155 | Coming soon ..
156 |
157 |
158 |
159 | ## Contributing
160 |
161 | If you have any idea, suggestions or find any bugs, feel free to open a discussion, an issue or create a pull request.
162 | That would be very useful for all of us and we would be happy to listen and take action.
163 |
164 | ## Acknowledgements
165 |
166 | Initially created by [onWidget](https://onwidget.com) and maintained by a community of [contributors](https://github.com/onwidget/tailnext/graphs/contributors).
167 |
168 | ## License
169 |
170 | **Tailnext** is licensed under the MIT license — see the [LICENSE](https://github.com/onwidget/tailnext/blob/main/LICENSE.md) file for details.
171 |
--------------------------------------------------------------------------------
/src/shared/data/global.data.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | IconBrandFacebook,
3 | IconBrandGithub,
4 | IconBrandInstagram,
5 | IconBrandTwitter,
6 | IconChevronDown,
7 | IconRss,
8 | } from '@tabler/icons-react';
9 | import { AnnouncementProps, FooterProps, HeaderProps } from '../types';
10 |
11 | // Announcement data
12 | export const announcementData: AnnouncementProps = {
13 | title: 'NEW',
14 | callToAction: {
15 | text: 'This template is made with Next.js 14 using the new App Router »',
16 | href: 'https://nextjs.org/blog/next-14',
17 | },
18 | callToAction2: {
19 | text: 'Follow @onWidget on Twitter',
20 | href: 'https://twitter.com/intent/user?screen_name=onwidget',
21 | },
22 | };
23 |
24 | // Header data
25 | export const headerData: HeaderProps = {
26 | links: [
27 | {
28 | label: 'Pages',
29 | icon: IconChevronDown,
30 | links: [
31 | {
32 | label: 'Services',
33 | href: '/services',
34 | },
35 | {
36 | label: 'Pricing',
37 | href: '/pricing',
38 | },
39 | {
40 | label: 'About us',
41 | href: '/about',
42 | },
43 | {
44 | label: 'Contact us',
45 | href: '/contact',
46 | },
47 | {
48 | label: 'FAQs',
49 | href: '/faqs',
50 | },
51 | {
52 | label: 'Terms & Conditions',
53 | href: '/terms',
54 | },
55 | {
56 | label: 'Privacy Policy',
57 | href: '/privacy',
58 | },
59 | ],
60 | },
61 | {
62 | label: 'Blog',
63 | href: '/blog',
64 | },
65 | {
66 | label: 'Contact',
67 | href: '/contact',
68 | },
69 | ],
70 | actions: [
71 | {
72 | text: 'Download',
73 | href: 'https://github.com/onwidget/tailnext',
74 | targetBlank: true,
75 | },
76 | ],
77 | isSticky: true,
78 | showToggleTheme: true,
79 | showRssFeed: false,
80 | position: 'right',
81 | };
82 |
83 | // Footer data
84 | export const footerData: FooterProps = {
85 | title: 'TailNext',
86 | links: [
87 | {
88 | label: 'Terms & Conditions',
89 | href: '/terms',
90 | },
91 | {
92 | label: 'Privacy Policy',
93 | href: '/privacy',
94 | },
95 | ],
96 | columns: [
97 | {
98 | title: 'Product',
99 | links: [
100 | {
101 | label: 'Features',
102 | href: '/',
103 | },
104 | {
105 | label: 'Security',
106 | href: '/',
107 | },
108 | {
109 | label: 'Team',
110 | href: '/',
111 | },
112 | {
113 | label: 'Enterprise',
114 | href: '/',
115 | },
116 | {
117 | label: 'Customer stories',
118 | href: '/',
119 | },
120 | {
121 | label: 'Pricing',
122 | href: '/pricing',
123 | },
124 | {
125 | label: 'Resources',
126 | href: '/',
127 | },
128 | ],
129 | },
130 | {
131 | title: 'Platform',
132 | links: [
133 | {
134 | label: 'Developer API',
135 | href: '/',
136 | },
137 | {
138 | label: 'Partners',
139 | href: '/',
140 | },
141 | ],
142 | },
143 | {
144 | title: 'Support',
145 | links: [
146 | {
147 | label: 'Docs',
148 | href: '/',
149 | },
150 | {
151 | label: 'Community Forum',
152 | href: '/',
153 | },
154 | {
155 | label: 'Professional Services',
156 | href: '/',
157 | },
158 | {
159 | label: 'Skills',
160 | href: '/',
161 | },
162 | {
163 | label: 'Status',
164 | href: '/',
165 | },
166 | ],
167 | },
168 | {
169 | title: 'Company',
170 | links: [
171 | {
172 | label: 'About',
173 | href: '/',
174 | },
175 | {
176 | label: 'Blog',
177 | href: '/blog',
178 | },
179 | {
180 | label: 'Careers',
181 | href: '/',
182 | },
183 | {
184 | label: 'Press',
185 | href: '/',
186 | },
187 | {
188 | label: 'Inclusion',
189 | href: '/',
190 | },
191 | {
192 | label: 'Social Impact',
193 | href: '/',
194 | },
195 | {
196 | label: 'Shop',
197 | href: '/',
198 | },
199 | ],
200 | },
201 | ],
202 | socials: [
203 | { label: 'Twitter', icon: IconBrandTwitter, href: '#' },
204 | { label: 'Instagram', icon: IconBrandInstagram, href: '#' },
205 | { label: 'Facebook', icon: IconBrandFacebook, href: '#' },
206 | { label: 'RSS', icon: IconRss, href: '#' },
207 | { label: 'Github', icon: IconBrandGithub, href: 'https://github.com/onwidget/tailnext' },
208 | ],
209 | footNote: (
210 |
224 | ),
225 | };
226 |
227 | // Footer2 data
228 | export const footerData2: FooterProps = {
229 | links: [
230 | {
231 | label: 'Terms & Conditions',
232 | href: '/terms',
233 | },
234 | {
235 | label: 'Privacy Policy',
236 | href: '/privacy',
237 | },
238 | ],
239 | columns: [
240 | {
241 | title: 'Address',
242 | texts: ['51 Phasellus Avenue Maecenas', 'Aliquam, AQ 52098'],
243 | },
244 | {
245 | title: 'Phone',
246 | texts: ['Reception: +105 123 4567', 'Office: +107 235 7890'],
247 | },
248 | {
249 | title: 'Email',
250 | texts: ['Office: info@example.com', 'Site: https://example.com'],
251 | },
252 | ],
253 | socials: [
254 | { label: 'Twitter', icon: IconBrandTwitter, href: '#' },
255 | { label: 'Instagram', icon: IconBrandInstagram, href: '#' },
256 | { label: 'Facebook', icon: IconBrandFacebook, href: '#' },
257 | { label: 'RSS', icon: IconRss, href: '#' },
258 | { label: 'Github', icon: IconBrandGithub, href: 'https://github.com/onwidget/tailnext' },
259 | ],
260 | footNote: (
261 |
275 | ),
276 | };
277 |
--------------------------------------------------------------------------------
/src/shared/data/pages/faqs.data.tsx:
--------------------------------------------------------------------------------
1 | import { CallToActionProps, FAQsProps } from '~/shared/types';
2 | import { HeroProps } from '~/shared/types';
3 |
4 | // Hero data on FAQs page *******************
5 | export const heroFaqs: HeroProps = {
6 | title: 'Frequently Asked Questions',
7 | subtitle: (
8 | <>
9 |
10 | {`Whether you need help using our Next.js and Tailwind CSS templates, solving problems, or just want some useful tips, our FAQs are here to assist you.`}
11 | {' '}
12 | Explore them to optimize your experience with our website and products.
13 | >
14 | ),
15 | tagline: 'Demo FAQs Page',
16 | };
17 |
18 | // FAQS4 data on FAQs page *******************
19 | export const faqs4Faqs: FAQsProps = {
20 | id: 'faqsFour-on-faqs',
21 | hasBackground: true,
22 | header: {
23 | title: 'Find what you need',
24 | subtitle: 'Get quick answers to your questions: Everything you need in one spot.',
25 | position: 'center',
26 | },
27 | tabs: [
28 | {
29 | link: {
30 | label: 'General',
31 | href: '/tab1',
32 | },
33 | items: [
34 | {
35 | title: 'What do I need to start?',
36 | description: `Nunc mollis tempor quam, non fringilla elit sagittis in. Nullam vitae consectetur mi, a elementum arcu. Sed laoreet, ipsum et vehicula dignissim, leo orci pretium sem, ac condimentum tellus est quis ligula.`,
37 | },
38 | {
39 | title: 'How to install the NextJS + Tailwind CSS template?',
40 | description: `Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer eleifend vestibulum nisl in iaculis. Mauris dictum ac purus vestibulum auctor. Praesent imperdiet lectus et massa faucibus, quis viverra massa rhoncus.`,
41 | },
42 | {
43 | title: "What's something that you completely don't understand?",
44 | description: `Mauris vitae eros a dui varius luctus. Suspendisse rutrum, sapien nec blandit bibendum, justo sapien sollicitudin erat, id aliquam sapien purus quis leo. Aliquam vulputate vestibulum consectetur.`,
45 | },
46 | {
47 | title: "What's an example of when you changed your mind?",
48 | description: `Nunc dapibus lacinia ipsum ut elementum. Integer in pretium sapien. Ut pretium nisl mauris, ut rutrum justo condimentum id. Etiam aliquet, arcu at iaculis laoreet, est arcu egestas sapien, eget sollicitudin odio orci et nunc.`,
49 | },
50 | {
51 | title: 'What is something that you would really like to try again?',
52 | description: `Duis in maximus mauris, id eleifend mauris. Nam a fringilla arcu. Curabitur convallis, tellus non aliquet rhoncus, lacus massa auctor eros, in interdum lectus augue sed augue. Fusce tempor ex id faucibus efficitur.`,
53 | },
54 | {
55 | title: 'If you could only ask one question to each person you meet, what would that question be?',
56 | description: `Nullam imperdiet sapien tincidunt erat dapibus faucibus. Vestibulum a sem nec lorem imperdiet scelerisque non sed lacus. Ut pulvinar id diam vitae auctor. Nam tempus, neque et elementum consectetur, ex ipsum pulvinar risus, vel sodales ligula tortor eu eros.`,
57 | },
58 | ],
59 | },
60 | {
61 | link: {
62 | label: 'Plans, prices and payments',
63 | href: '/tab2',
64 | },
65 | items: [
66 | {
67 | title: 'Which plan is best for me?',
68 | description: `Nunc mollis tempor quam, non fringilla elit sagittis in. Nullam vitae consectetur mi, a elementum arcu. Sed laoreet, ipsum et vehicula dignissim, leo orci pretium sem, ac condimentum tellus est quis ligula.`,
69 | },
70 | {
71 | title: 'What are my payment options?',
72 | description: `Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer eleifend vestibulum nisl in iaculis. Mauris dictum ac purus vestibulum auctor. Praesent imperdiet lectus et massa faucibus, quis viverra massa rhoncus.`,
73 | },
74 | {
75 | title: 'How do I change my plan to a different one?',
76 | description: `Mauris vitae eros a dui varius luctus. Suspendisse rutrum, sapien nec blandit bibendum, justo sapien sollicitudin erat, id aliquam sapien purus quis leo. Aliquam vulputate vestibulum consectetur.`,
77 | },
78 | {
79 | title: 'What happen at the end of my free trial?',
80 | description: `Nunc dapibus lacinia ipsum ut elementum. Integer in pretium sapien. Ut pretium nisl mauris, ut rutrum justo condimentum id. Etiam aliquet, arcu at iaculis laoreet, est arcu egestas sapien, eget sollicitudin odio orci et nunc.`,
81 | },
82 | {
83 | title: 'Can I import data from other tools?',
84 | description: `Duis in maximus mauris, id eleifend mauris. Nam a fringilla arcu. Curabitur convallis, tellus non aliquet rhoncus, lacus massa auctor eros, in interdum lectus augue sed augue. Fusce tempor ex id faucibus efficitur.`,
85 | },
86 | {
87 | title: 'Can I cancel my plan at any time?',
88 | description: `Nullam imperdiet sapien tincidunt erat dapibus faucibus. Vestibulum a sem nec lorem imperdiet scelerisque non sed lacus. Ut pulvinar id diam vitae auctor. Nam tempus, neque et elementum consectetur, ex ipsum pulvinar risus, vel sodales ligula tortor eu eros.`,
89 | },
90 | ],
91 | },
92 | {
93 | link: {
94 | label: 'Others',
95 | href: '/tab3',
96 | },
97 | items: [
98 | {
99 | title: 'How do I download the template?',
100 | description: `In ullamcorper pellentesque ante, nec commodo ex euismod viverra. Phasellus facilisis, justo a bibendum pellentesque, nibh est egestas lectus, volutpat ullamcorper arcu ante ac dolor.`,
101 | },
102 | {
103 | title: 'How do I customize the template?',
104 | description: `Pellentesque semper euismod malesuada. Curabitur quis lectus tortor. Aliquam efficitur pretium tellus, ut sagittis turpis dignissim eget. Etiam scelerisque nec risus eget iaculis. Nunc maximus metus id felis dapibus, sed ullamcorper sapien faucibus.`,
105 | },
106 | {
107 | title: 'Does the template come with any tutorials or instructions?',
108 | description: `Sed sagittis arcu suscipit auctor suscipit. Nam dapibus risus vitae tristique fermentum. In egestas turpis elit, id gravida diam dictum eu. Ut dictum libero ut rhoncus egestas. Ut sit amet tortor blandit, faucibus tellus vitae, consequat purus. Nullam id odio enim.`,
109 | },
110 | {
111 | title: 'Are there any additional fees or charges for using the template?',
112 | description: `Fusce efficitur, augue et vulputate pharetra, augue turpis viverra turpis, id tempor purus eros sed erat. Curabitur blandit eget sem vitae malesuada.`,
113 | },
114 | ],
115 | },
116 | ],
117 | };
118 |
119 | // CallToAction data on FAQs page *******************
120 | export const callToActionFaqs: CallToActionProps = {
121 | id: 'callToAction-on-faqs',
122 | hasBackground: true,
123 | title: 'Still have questions?',
124 | subtitle:
125 | 'Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut in leo odio. Cras finibus ex a ante convallis ullamcorper.',
126 | callToAction: {
127 | text: 'Contact us',
128 | href: '/contact',
129 | },
130 | };
131 |
--------------------------------------------------------------------------------
/src/components/widgets/Header.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useRef, useState } from 'react';
4 | import { IconRss } from '@tabler/icons-react';
5 | import { useOnClickOutside } from '~/hooks/useOnClickOutside';
6 | import ToggleDarkMode from '~/components/atoms/ToggleDarkMode';
7 | import Link from 'next/link';
8 | import Logo from '~/components/atoms/Logo';
9 | import ToggleMenu from '../atoms/ToggleMenu';
10 | import { headerData } from '~/shared/data/global.data';
11 | import CTA from '../common/CTA';
12 | import { CallToActionType } from '~/shared/types';
13 |
14 | const Header = () => {
15 | const { links, actions, isSticky, showToggleTheme, showRssFeed, position } = headerData;
16 |
17 | const ref = useRef(null);
18 |
19 | const updatedIsDropdownOpen =
20 | links &&
21 | links.map(() => {
22 | return false;
23 | });
24 |
25 | const [isDropdownOpen, setIsDropdownOpen] = useState(updatedIsDropdownOpen as boolean[]);
26 | const [isToggleMenuOpen, setIsToggleMenuOpen] = useState(false);
27 |
28 | const handleDropdownOnClick = (index: number) => {
29 | setIsDropdownOpen((prevValues) => {
30 | const newValues = [...(prevValues as boolean[])];
31 | newValues.forEach((value, i) => {
32 | if (value === true) {
33 | newValues[i] = false;
34 | } else {
35 | newValues[i] = i === index;
36 | }
37 | });
38 | return newValues;
39 | });
40 | };
41 |
42 | const handleCloseDropdownOnClick = (index: number) => {
43 | setIsDropdownOpen((prevValues) => {
44 | const newValues = [...(prevValues as boolean[])];
45 | newValues[index] = false;
46 | return newValues;
47 | });
48 | };
49 |
50 | const handleToggleMenuOnClick = () => {
51 | setIsToggleMenuOpen(!isToggleMenuOpen);
52 | };
53 |
54 | useOnClickOutside(ref, () => {
55 | setIsDropdownOpen(updatedIsDropdownOpen as boolean[]);
56 | });
57 |
58 | return (
59 |
178 | );
179 | };
180 |
181 | export default Header;
182 |
--------------------------------------------------------------------------------
/src/shared/types.d.ts:
--------------------------------------------------------------------------------
1 | import { StaticImageData } from 'next/image';
2 | import { ReactElement } from 'react';
3 | import type { TablerIcon } from "@tabler/icons-react"
4 |
5 | type Widget = {
6 | id?: string;
7 | /** Does it have a background? */
8 | hasBackground?: boolean;
9 | };
10 |
11 | type WrapperTagProps = Widget & {
12 | children: React.ReactNode;
13 | containerClass?: string;
14 | };
15 |
16 | type BackgroundProps = {
17 | children?: React.ReactNode;
18 | hasBackground?: boolean;
19 | };
20 |
21 | type Header = {
22 | title?: string | ReactElement;
23 | subtitle?: string | ReactElement;
24 | tagline?: string;
25 | position?: 'center' | 'right' | 'left';
26 | };
27 |
28 | type HeadlineProps = {
29 | header: Header;
30 | containerClass?: string;
31 | titleClass?: string;
32 | subtitleClass?: string;
33 | };
34 |
35 | type Icon = TablerIcon;
36 |
37 | type CallToActionType = {
38 | text?: string;
39 | href: string;
40 | icon?: Icon;
41 | targetBlank?: boolean;
42 | };
43 |
44 | type LinkOrButton = {
45 | callToAction?: CallToActionType;
46 | containerClass?: string;
47 | linkClass?: string;
48 | iconClass?: string;
49 | };
50 |
51 | type Button = {
52 | title: string;
53 | type: 'button' | 'submit' | 'reset';
54 | };
55 |
56 | type Input = {
57 | type: string;
58 | label?: string;
59 | value?: string;
60 | name?: string;
61 | autocomplete?: string;
62 | placeholder?: string;
63 | };
64 |
65 | type Textarea = {
66 | cols?: number;
67 | rows?: number;
68 | label?: string;
69 | name: string;
70 | placeholder?: string;
71 | };
72 |
73 | type Checkbox = {
74 | label: string;
75 | value: string;
76 | };
77 |
78 | type Radio = {
79 | label: string;
80 | };
81 |
82 | type RadioBtn = {
83 | label?: string;
84 | radios: Array;
85 | };
86 |
87 | type SmallForm = {
88 | icon?: Icon;
89 | input: Input;
90 | btn: Button;
91 | };
92 |
93 | type FormProps = {
94 | title?: string;
95 | description?: string;
96 | inputs: Array ;
97 | radioBtns?: RadioBtn;
98 | textarea?: Textarea;
99 | checkboxes?: Array;
100 | btn: Button;
101 | btnPosition?: 'center' | 'right' | 'left';
102 | containerClass?: string;
103 | };
104 |
105 | type Image = {
106 | link?: string;
107 | src: string | StaticImageData;
108 | alt: string;
109 | };
110 |
111 | type Item = {
112 | title?: string | boolean | number;
113 | description?: string | Array;
114 | href?: string;
115 | form?: SmallForm;
116 | icon?: Icon;
117 | callToAction?: CallToActionType;
118 | };
119 |
120 | type ItemGrid = {
121 | id?: string;
122 | items?: Array- ;
123 | columns?: number;
124 | defaultColumns?: number;
125 | defaultIcon?: Icon;
126 | containerClass?: string;
127 | panelClass?: string;
128 | iconClass?: string;
129 | titleClass?: string;
130 | descriptionClass?: string;
131 | actionClass?: string;
132 | };
133 |
134 | type Timeline = {
135 | id?: string;
136 | items?: Array
- ;
137 | defaultIcon?: Icon;
138 | containerClass?: string;
139 | panelClass?: string;
140 | iconClass?: string;
141 | titleClass?: string;
142 | descriptionClass?: string;
143 | };
144 |
145 | type Team = {
146 | name: string;
147 | occupation: string;
148 | image: Image;
149 | items?: Array
- ;
150 | containerClass?: string;
151 | imageClass?: string;
152 | panelClass?: string;
153 | nameClass?: string;
154 | occupationClass?: string;
155 | itemsClass?: string;
156 | };
157 |
158 | type Testimonial = {
159 | testimonial?: string;
160 | startSlice?: number;
161 | endSlice?: number;
162 | isTestimonialUp?: boolean;
163 | hasDividerLine?: boolean;
164 | name?: string;
165 | job?: string;
166 | image?: Image;
167 | href?: string;
168 | containerClass?: string;
169 | panelClass?: string;
170 | imageClass?: string;
171 | dataClass?: string;
172 | nameJobClass?: string;
173 | nameClass?: string;
174 | jobClass?: string;
175 | testimonialClass?: string;
176 | };
177 |
178 | type Link = {
179 | label?: string;
180 | href?: string;
181 | ariaLabel?: string;
182 | icon?: Icon;
183 | };
184 |
185 | type Price = {
186 | title?: string;
187 | subtitle?: string;
188 | description?: string;
189 | price?: number;
190 | period?: string;
191 | items?: Array
- ;
192 | callToAction?: CallToActionType;
193 | hasRibbon?: boolean;
194 | ribbonTitle?: string;
195 | };
196 |
197 | type Column = {
198 | title: string;
199 | items: Array
- ;
200 | callToAction?: CallToActionType;
201 | };
202 |
203 | type MenuLink = Link & {
204 | links?: Array
;
205 | };
206 |
207 | type Links = {
208 | title?: string;
209 | links?: Array ;
210 | texts?: Array;
211 | };
212 |
213 | type Tab = {
214 | link?: Link;
215 | items: Array- ;
216 | };
217 |
218 | type Dropdown = {
219 | options: Tab[];
220 | activeTab: number;
221 | onActiveTabSelected: Function;
222 | iconUp?: ReactElement;
223 | iconDown?: ReactElement;
224 | };
225 |
226 | type ToggleMenuProps = {
227 | handleToggleMenuOnClick: MouseEventHandler
;
228 | isToggleMenuOpen: boolean;
229 | };
230 |
231 | type WindowSize = {
232 | width: number;
233 | height: number;
234 | };
235 |
236 | // WIDGETS
237 | type HeroProps = {
238 | title?: string | ReactElement;
239 | subtitle?: string | ReactElement;
240 | tagline?: string;
241 | callToAction?: CallToActionType;
242 | callToAction2?: CallToActionType;
243 | image?: Image;
244 | };
245 |
246 | type FAQsProps = Widget & {
247 | header?: Header;
248 | items?: Array- ;
249 | columns?: number;
250 | tabs?: Array
;
251 | callToAction?: CallToActionType;
252 | };
253 |
254 | type CollapseProps = {
255 | items: Array- ;
256 | classCollapseItem?: string;
257 | iconUp?: ReactElement;
258 | iconDown?: ReactElement;
259 | };
260 |
261 | type CallToActionProps = Widget & {
262 | title: string;
263 | subtitle: string;
264 | callToAction?: CallToActionType;
265 | items?: Array
- ;
266 | };
267 |
268 | type FeaturesProps = Widget & {
269 | header?: Header;
270 | items?: Array
- ;
271 | /** How many columns should it have? */
272 | columns?: 1 | 2 | 3;
273 | /** Do you want the image to be displayed? */
274 | isImageDisplayed?: boolean;
275 | image?: Image;
276 | isBeforeContent?: boolean;
277 | isAfterContent?: boolean;
278 | };
279 |
280 | type ContentProps = Widget & {
281 | header?: Header;
282 | content?: string;
283 | items?: Array
- ;
284 | image?: Image;
285 | isReversed?: boolean;
286 | isAfterContent?: boolean;
287 | };
288 |
289 | type StepsProps = Widget & {
290 | header?: Header;
291 | items: Array
- ;
292 | /** Do you want the image to be displayed? */
293 | isImageDisplayed?: boolean;
294 | image?: Image;
295 | /** Do you want to reverse the widget? */
296 | isReversed?: boolean;
297 | };
298 |
299 | type TeamProps = Widget & {
300 | header?: Header;
301 | teams: Array
;
302 | };
303 |
304 | type AnnouncementProps = {
305 | title: string;
306 | callToAction?: CallToActionType;
307 | callToAction2?: CallToActionType;
308 | };
309 |
310 | type TestimonialsProps = Widget & {
311 | header?: Header;
312 | testimonials: Array;
313 | isTestimonialUp?: boolean;
314 | hasDividerLine?: boolean;
315 | startSlice?: number;
316 | endSlice?: number;
317 | callToAction?: CallToActionType;
318 | };
319 |
320 | type PricingProps = Widget & {
321 | header?: Header;
322 | prices: Array;
323 | };
324 |
325 | type ComparisonProps = Widget & {
326 | header?: Header;
327 | columns: Array;
328 | };
329 |
330 | type StatsProps = Widget & {
331 | items: Array- ;
332 | };
333 |
334 | type SocialProofProps = Widget & {
335 | images: Array
;
336 | };
337 |
338 | type ContactProps = Widget & {
339 | header?: Header;
340 | content?: string;
341 | items?: Array- ;
342 | form: FormProps;
343 | };
344 |
345 | type FooterProps = {
346 | title?: string;
347 | links?: Array
;
348 | columns: Array;
349 | socials: Array ;
350 | footNote?: string | ReactElement;
351 | theme?: string;
352 | };
353 |
354 | type HeaderProps = {
355 | links?: Array;
356 | actions?: Array;
357 | // actions?: Array;
358 | isSticky?: boolean;
359 | showToggleTheme?: boolean;
360 | showRssFeed?: boolean;
361 | position?: 'center' | 'right' | 'left';
362 | };
363 |
--------------------------------------------------------------------------------