├── 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 | 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 |
11 | 12 | {alt} 20 | 21 |
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 |
17 |

18 | Blog 19 |

20 |
21 |
22 | {posts.map(({ slug, title, image }: { slug: string, title: string, image: string }) => ( 23 |
24 | 25 | {title} 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 | 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 |
9 | {title}{' '} 10 | {callToAction && callToAction.text && callToAction.href && ( 11 | 17 | {callToAction.icon && } {callToAction.text} 18 | 19 | )} 20 | {callToAction2 && callToAction2.text && callToAction2.href && ( 21 | 28 | Follow @onWidget 34 | 35 | )} 36 |
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 | 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 | {image.alt} 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 | {image.alt} 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 | {image.alt} 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 | 40 | {activeIndex === index && ( 41 |
46 |

{description}

47 |
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 | <Subtitle /> 20 | <Description /> 21 | <Primary /> 22 | <Controls exclude={['id', 'header', 'image', 'isImageDisplayed', 'isBeforeContent', 'isAfterContent']} /> 23 | <Stories includePrimary={false} title={'Stories'} /> 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<typeof Component>; 33 | 34 | export default meta; 35 | type Story = StoryObj<typeof meta>; 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 | <div className={twMerge(`select-none`, containerClass)}> 26 | <div className={twMerge(`flex ${isTestimonialUp ? 'flex-col-reverse' : 'flex-col'}`, panelClass)}> 27 | {((image && name) || (name && job)) && ( 28 | <> 29 | <div className={twMerge('flex items-center', dataClass)}> 30 | {image && ( 31 | <Image 32 | src={image.src} 33 | width={248} 34 | height={248} 35 | alt={image.alt} 36 | className={twMerge('object-cover shadow-lg bg-gray-500 dark:bg-slate-700', imageClass)} 37 | /> 38 | )} 39 | 40 | <div className={twMerge('flex flex-col justify-center', nameJobClass)}> 41 | {name && <h3 className={twMerge('font-semibold', nameClass)}>{name}</h3>} 42 | {job && <span className={twMerge('dark:text-slate-400', jobClass)}>{job}</span>} 43 | </div> 44 | </div> 45 | 46 | {hasDividerLine && <DividerLine />} 47 | </> 48 | )} 49 | 50 | {testimonial && ( 51 | <blockquote className={twMerge('flex-auto', testimonialClass)}> 52 | <p className="font-light dark:text-slate-400"> 53 | {startSlice !== undefined && endSlice !== undefined 54 | ? `" ${testimonial.slice(Number(startSlice), Number(endSlice))}... "` 55 | : `" ${testimonial} "`} 56 | </p> 57 | </blockquote> 58 | )} 59 | </div> 60 | </div> 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 | <Title /> 19 | <Subtitle /> 20 | <Description /> 21 | <Primary /> 22 | <Controls exclude={['id', 'header']} /> 23 | <Stories includePrimary={false} title={'Stories'} /> 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<typeof Component>; 33 | 34 | export default meta; 35 | type Story = StoryObj<typeof meta>; 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 | <Title /> 19 | <Subtitle /> 20 | <Description /> 21 | <Primary /> 22 | <Controls exclude={['id', 'header', 'image', 'isImageDisplayed', 'isBeforeContent', 'isAfterContent']} /> 23 | <Stories includePrimary={false} title={'Stories'} /> 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<typeof Component>; 33 | 34 | export default meta; 35 | type Story = StoryObj<typeof meta>; 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 | <Title /> 19 | <Subtitle /> 20 | <Description /> 21 | <Primary /> 22 | <Controls exclude={['id', 'header', 'image', 'isImageDisplayed', 'isBeforeContent', 'isAfterContent']} /> 23 | <Stories includePrimary={false} title={'Stories'} /> 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<typeof Component>; 33 | 34 | export default meta; 35 | type Story = StoryObj<typeof meta>; 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 | <WidgetWrapper id={id ? id : ''} hasBackground={hasBackground} containerClass="max-w-6xl"> 8 | {header && <Headline header={header} titleClass="text-3xl sm:text-5xl" />} 9 | <div className="flex items-stretch justify-center"> 10 | <div className={`grid ${!content && !items ? 'md:grid-cols-1' : 'md:grid-cols-2'}`}> 11 | <div className="h-full pr-6"> 12 | {content && <p className="mt-3 mb-12 text-lg text-gray-600 dark:text-slate-400">{content}</p>} 13 | <ul className="mb-6 md:mb-0"> 14 | {items && 15 | items.map(({ title, description, icon: Icon }, index) => ( 16 | <li key={`item-contact-${index}`} className="flex"> 17 | <div className="flex h-10 w-10 items-center justify-center rounded bg-blue-900 text-gray-50"> 18 | {Icon && <Icon className="h-6 w-6" />} 19 | </div> 20 | <div className="ml-4 rtl:ml-0 rtl:mr-4 mb-4"> 21 | <h3 className="mb-2 text-lg font-medium leading-6 text-gray-900 dark:text-white">{title}</h3> 22 | {typeof description === 'string' ? ( 23 | <p key={`text-description-${index}`} className="text-gray-600 dark:text-slate-400"> 24 | {description} 25 | </p> 26 | ) : ( 27 | description && 28 | description.map((desc, index) => ( 29 | <p key={`text-description-${index}`} className="text-gray-600 dark:text-slate-400"> 30 | {desc} 31 | </p> 32 | )) 33 | )} 34 | </div> 35 | </li> 36 | ))} 37 | </ul> 38 | </div> 39 | <Form {...form} containerClass="card h-fit max-w-2xl mx-auto p-5 md:p-12" btnPosition="center" /> 40 | </div> 41 | </div> 42 | </WidgetWrapper> 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 | <WidgetWrapper 20 | id={id ? id : ''} 21 | hasBackground={hasBackground} 22 | containerClass={`${isAfterContent ? 'py-0 md:py-0 lg:py-0 pb-12 md:pb-16 lg:pb-20' : ''}`} 23 | > 24 | {header && <Headline header={header} titleClass="text-3xl sm:text-5xl" />} 25 | <div className="mx-auto max-w-7xl"> 26 | <div className={`md:flex ${isReversed ? 'md:flex-row-reverse' : ''} md:gap-16`}> 27 | <div className="self-center md:basis-1/2"> 28 | {content && <div className="mb-8 lg:mb-12 text-lg text-gray-600 dark:text-slate-400">{content}</div>} 29 | <ItemGrid 30 | items={items} 31 | columns={1} 32 | defaultIcon={IconCheck} 33 | containerClass="gap-4 md:gap-y-6" 34 | panelClass="flex max-w-full" 35 | titleClass="text-lg font-medium leading-6 text-gray-900 dark:text-white mt-1 mb-2" 36 | descriptionClass="mt-1 text-gray-600 dark:text-slate-400" 37 | iconClass="flex-shrink-0 w-7 h-7 flex items-center justify-center rounded-full bg-primary-900 text-gray-50 mr-4 rtl:mr-0 rtl:ml-4 mt-1 p-1" 38 | /> 39 | </div> 40 | <div aria-hidden="true" className="mt-10 md:mt-0 md:basis-1/2"> 41 | {image && ( 42 | <div className="relative m-auto max-w-4xl"> 43 | <Image 44 | className="mx-auto w-full rounded-lg shadow-lg bg-gray-400 dark:bg-slate-700" 45 | src={image.src} 46 | width={828} 47 | height={828} 48 | alt={image.alt} 49 | sizes="(max-width: 768px) 100vw, 432px" 50 | placeholder="blur" 51 | quality={50} 52 | /> 53 | </div> 54 | )} 55 | </div> 56 | </div> 57 | </div> 58 | </WidgetWrapper> 59 | ); 60 | 61 | export default Content; 62 | -------------------------------------------------------------------------------- /src/stories/assets/discord.svg: -------------------------------------------------------------------------------- 1 | <svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <g clip-path="url(#clip0_10031_177575)"> 3 | <mask id="mask0_10031_177575" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="4" width="33" height="25"> 4 | <path d="M32.5034 4.00195H0.503906V28.7758H32.5034V4.00195Z" fill="white"/> 5 | </mask> 6 | <g mask="url(#mask0_10031_177575)"> 7 | <path d="M27.5928 6.20817C25.5533 5.27289 23.3662 4.58382 21.0794 4.18916C21.0378 4.18154 20.9962 4.20057 20.9747 4.23864C20.6935 4.73863 20.3819 5.3909 20.1637 5.90358C17.7042 5.53558 15.2573 5.53558 12.8481 5.90358C12.6299 5.37951 12.307 4.73863 12.0245 4.23864C12.003 4.20184 11.9614 4.18281 11.9198 4.18916C9.63431 4.58255 7.44721 5.27163 5.40641 6.20817C5.38874 6.21578 5.3736 6.22848 5.36355 6.24497C1.21508 12.439 0.078646 18.4809 0.636144 24.4478C0.638667 24.477 0.655064 24.5049 0.677768 24.5227C3.41481 26.5315 6.06609 27.7511 8.66815 28.5594C8.70979 28.5721 8.75392 28.5569 8.78042 28.5226C9.39594 27.6826 9.94461 26.7968 10.4151 25.8653C10.4428 25.8107 10.4163 25.746 10.3596 25.7244C9.48927 25.3945 8.66058 24.9922 7.86343 24.5354C7.80038 24.4986 7.79533 24.4084 7.85333 24.3653C8.02108 24.2397 8.18888 24.109 8.34906 23.977C8.37804 23.9529 8.41842 23.9478 8.45249 23.963C13.6894 26.3526 19.359 26.3526 24.5341 23.963C24.5682 23.9465 24.6086 23.9516 24.6388 23.9757C24.799 24.1077 24.9668 24.2397 25.1358 24.3653C25.1938 24.4084 25.19 24.4986 25.127 24.5354C24.3298 25.0011 23.5011 25.3945 22.6296 25.7232C22.5728 25.7447 22.5476 25.8107 22.5754 25.8653C23.0559 26.7955 23.6046 27.6812 24.2087 28.5213C24.234 28.5569 24.2794 28.5721 24.321 28.5594C26.9357 27.7511 29.5869 26.5315 32.324 24.5227C32.348 24.5049 32.3631 24.4783 32.3656 24.4491C33.0328 17.5506 31.2481 11.5584 27.6344 6.24623C27.6256 6.22848 27.6105 6.21578 27.5928 6.20817ZM11.1971 20.8146C9.62043 20.8146 8.32129 19.3679 8.32129 17.5913C8.32129 15.8146 9.59523 14.368 11.1971 14.368C12.8115 14.368 14.0981 15.8273 14.0729 17.5913C14.0729 19.3679 12.7989 20.8146 11.1971 20.8146ZM21.8299 20.8146C20.2533 20.8146 18.9541 19.3679 18.9541 17.5913C18.9541 15.8146 20.228 14.368 21.8299 14.368C23.4444 14.368 24.7309 15.8273 24.7057 17.5913C24.7057 19.3679 23.4444 20.8146 21.8299 20.8146Z" fill="#5865F2"/> 8 | </g> 9 | </g> 10 | <defs> 11 | <clipPath id="clip0_10031_177575"> 12 | <rect width="31.9995" height="32" fill="white" transform="translate(0.5)"/> 13 | </clipPath> 14 | </defs> 15 | </svg> 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 | <Title /> 19 | <Subtitle /> 20 | <Description /> 21 | <Primary /> 22 | <Controls exclude={['id', 'header', 'isBeforeContent', 'isAfterContent']} /> 23 | <Stories includePrimary={false} title={'Stories'} /> 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<typeof Component>; 33 | 34 | export default meta; 35 | type Story = StoryObj<typeof meta>; 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<boolean>(false); 9 | const [selectedOption, setSelectedOption] = useState<string>(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 | <div className="relative mt-4 rounded-md border border-gray-400 text-left"> 35 | <div onClick={dropdownHandler} className="flex select-none items-center justify-between rounded-md p-3"> 36 | <div className="text-lg">{selectedOption}</div> 37 | {iconDown && iconUp ? ( 38 | isDropdownOpen === false ? ( 39 | iconDown 40 | ) : ( 41 | iconUp 42 | ) 43 | ) : isDropdownOpen === false ? ( 44 | <IconChevronDown className="h-6 w-6 text-primary-600 dark:text-slate-200" /> 45 | ) : ( 46 | <IconChevronUp className="h-6 w-6 text-primary-600 dark:text-slate-200" /> 47 | )} 48 | </div> 49 | {isDropdownOpen && ( 50 | <div className="absolute w-full translate-y-1 overflow-auto rounded-md border border-gray-400"> 51 | {options.map((option: Tab, index) => ( 52 | <div 53 | key={`option-${index}`} 54 | onClick={() => 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 && <IconCheck className="mr-2 h-5 w-5" />} {option.link?.label} 60 | </div> 61 | ))} 62 | </div> 63 | )} 64 | </div> 65 | ); 66 | }; 67 | 68 | export default Dropdown; 69 | -------------------------------------------------------------------------------- /src/stories/assets/github.svg: -------------------------------------------------------------------------------- 1 | <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <path d="M16.0001 0C7.16466 0 0 7.17472 0 16.0256C0 23.1061 4.58452 29.1131 10.9419 31.2322C11.7415 31.3805 12.0351 30.8845 12.0351 30.4613C12.0351 30.0791 12.0202 28.8167 12.0133 27.4776C7.56209 28.447 6.62283 25.5868 6.62283 25.5868C5.89499 23.7345 4.8463 23.2419 4.8463 23.2419C3.39461 22.2473 4.95573 22.2678 4.95573 22.2678C6.56242 22.3808 7.40842 23.9192 7.40842 23.9192C8.83547 26.3691 11.1514 25.6609 12.0645 25.2514C12.2081 24.2156 12.6227 23.5087 13.0803 23.1085C9.52648 22.7032 5.7906 21.3291 5.7906 15.1886C5.7906 13.4389 6.41563 12.0094 7.43916 10.8871C7.27303 10.4834 6.72537 8.85349 7.59415 6.64609C7.59415 6.64609 8.93774 6.21539 11.9953 8.28877C13.2716 7.9337 14.6404 7.75563 16.0001 7.74953C17.3599 7.75563 18.7297 7.9337 20.0084 8.28877C23.0623 6.21539 24.404 6.64609 24.404 6.64609C25.2749 8.85349 24.727 10.4834 24.5608 10.8871C25.5868 12.0094 26.2075 13.4389 26.2075 15.1886C26.2075 21.3437 22.4645 22.699 18.9017 23.0957C19.4756 23.593 19.9869 24.5683 19.9869 26.0634C19.9869 28.2077 19.9684 29.9334 19.9684 30.4613C19.9684 30.8877 20.2564 31.3874 21.0674 31.2301C27.4213 29.1086 32 23.1037 32 16.0256C32 7.17472 24.8364 0 16.0001 0ZM5.99257 22.8288C5.95733 22.9084 5.83227 22.9322 5.71834 22.8776C5.60229 22.8253 5.53711 22.7168 5.57474 22.6369C5.60918 22.5549 5.7345 22.5321 5.85029 22.587C5.9666 22.6393 6.03284 22.7489 5.99257 22.8288ZM6.7796 23.5321C6.70329 23.603 6.55412 23.5701 6.45291 23.4581C6.34825 23.3464 6.32864 23.197 6.40601 23.125C6.4847 23.0542 6.62937 23.0874 6.73429 23.1991C6.83895 23.3121 6.85935 23.4605 6.7796 23.5321ZM7.31953 24.4321C7.2215 24.5003 7.0612 24.4363 6.96211 24.2938C6.86407 24.1513 6.86407 23.9804 6.96422 23.9119C7.06358 23.8435 7.2215 23.905 7.32191 24.0465C7.41968 24.1914 7.41968 24.3623 7.31953 24.4321ZM8.23267 25.4743C8.14497 25.5712 7.95818 25.5452 7.82146 25.413C7.68156 25.2838 7.64261 25.1004 7.73058 25.0035C7.81934 24.9064 8.00719 24.9337 8.14497 25.0648C8.28381 25.1938 8.3262 25.3785 8.23267 25.4743ZM9.41281 25.8262C9.37413 25.9517 9.19423 26.0088 9.013 25.9554C8.83203 25.9005 8.7136 25.7535 8.75016 25.6266C8.78778 25.5003 8.96848 25.4408 9.15104 25.4979C9.33174 25.5526 9.45044 25.6985 9.41281 25.8262ZM10.7559 25.9754C10.7604 26.1076 10.6067 26.2172 10.4165 26.2196C10.2252 26.2238 10.0704 26.1169 10.0683 25.9868C10.0683 25.8534 10.2185 25.7448 10.4098 25.7416C10.6001 25.7379 10.7559 25.8441 10.7559 25.9754ZM12.0753 25.9248C12.0981 26.0537 11.9658 26.1862 11.7769 26.2215C11.5912 26.2554 11.4192 26.1758 11.3957 26.0479C11.3726 25.9157 11.5072 25.7833 11.6927 25.7491C11.8819 25.7162 12.0512 25.7937 12.0753 25.9248Z" fill="#161614"/> 3 | </svg> 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 | <WidgetWrapper id={id ? id : ''} hasBackground={hasBackground} containerClass=""> 9 | {header && <Headline header={header} titleClass="text-2xl sm:text-3xl" />} 10 | <div className="relative ml-[-1em] flex overflow-x-auto md:pb-12"> 11 | {columns.map(({ title, items, callToAction }, index) => ( 12 | <div 13 | key={`column-content-${index}`} 14 | className={`relative mx-auto w-full min-w-fit max-w-3xl select-none border-r border-solid border-gray-300 px-4 py-4 first-of-type:sticky first-of-type:left-0 first-of-type:z-10 first-of-type:w-auto ${ 15 | hasBackground 16 | ? 'first-of-type:bg-primary-50 first-of-type:dark:bg-slate-800' 17 | : 'first-of-type:bg-white first-of-type:dark:bg-slate-900' 18 | } first-of-type:pl-6 last-of-type:border-none dark:border-slate-500 md:px-5 md:first-of-type:w-full md:first-of-type:pl-5`} 19 | > 20 | <h3 21 | className={`mb-4 border-b border-solid border-gray-300 pb-4 text-lg font-medium uppercase leading-6 text-gray-900 dark:border-slate-500 dark:text-white ${ 22 | index === 0 ? 'text-left' : 'text-center' 23 | }`} 24 | > 25 | {title} 26 | </h3> 27 | {items && 28 | items.map(({ title: title2 }, index2) => ( 29 | <div 30 | key={`column-content-${index2}`} 31 | className={`leading-7 text-gray-600 dark:text-slate-400 ${index === 0 ? 'text-left' : 'text-center'}`} 32 | > 33 | {(title2 as boolean) === true ? ( 34 | <IconCheck className="mt-2 w-full" /> 35 | ) : (title2 as boolean) === false ? ( 36 | <IconMinus className="mt-2 w-full" /> 37 | ) : index !== 0 ? ( 38 | <p className="mt-2">{title2}</p> 39 | ) : ( 40 | <h4 className="mt-2 text-lg">{title2}</h4> 41 | )} 42 | </div> 43 | ))} 44 | {index !== 0 && callToAction && ( 45 | <CTA callToAction={callToAction as CallToActionType} linkClass="btn btn-primary mt-8" /> 46 | )} 47 | </div> 48 | ))} 49 | </div> 50 | </WidgetWrapper> 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 | <section className="mt-[-72px] bg-primary-50 dark:bg-slate-800" id="heroTwo"> 8 | <div className="mx-auto max-w-7xl px-4 pt-[72px] sm:px-6 md:flex md:h-screen 2xl:h-auto"> 9 | <div className="block py-12 text-center md:flex md:py-12 md:text-left lg:py-16"> 10 | <div className="mx-auto flex max-w-5xl basis-[56%] items-center"> 11 | <div className="max-w-3xl pb-12 pr-0 md:py-0 md:pr-8 md:pb-0 lg:pr-16"> 12 | {tagline && ( 13 | <p className="text-base font-semibold uppercase tracking-wide text-primary-600 dark:text-primary-200"> 14 | {tagline} 15 | </p> 16 | )} 17 | {title && ( 18 | <h1 className="leading-tighter font-heading mb-4 px-4 text-5xl font-bold tracking-tighter md:px-0 md:text-[3.48rem]"> 19 | {title} 20 | </h1> 21 | )} 22 | <div className="mx-auto max-w-3xl"> 23 | {subtitle && <p className="mb-8 text-xl font-normal text-gray-600 dark:text-slate-400">{subtitle}</p>} 24 | <div className="flex max-w-none flex-col flex-nowrap justify-center gap-4 sm:flex-row md:m-0 md:justify-start"> 25 | {callToAction && <CTA callToAction={callToAction} linkClass="btn btn-primary" />} 26 | {callToAction2 && <CTA callToAction={callToAction2} linkClass="btn" />} 27 | </div> 28 | </div> 29 | </div> 30 | </div> 31 | <div className="block flex-1 items-center md:flex"> 32 | <div className="relative m-auto h-full max-w-4xl object-cover"> 33 | {image && ( 34 | <Image 35 | className="mx-auto h-full w-auto rounded-md bg-gray-400 object-cover drop-shadow-2xl dark:bg-slate-700" 36 | src={image.src} 37 | alt={image.alt} 38 | width={540} 39 | height={405} 40 | sizes="(min-width: 1920px) 749px, (min-width: 1540px) 43.89vw, (min-width: 1360px) 542px, (min-width: 780px) calc(39.29vw + 16px), calc(96.52vw - 22px)" 41 | loading="eager" 42 | placeholder="blur" 43 | priority 44 | /> 45 | )} 46 | </div> 47 | </div> 48 | </div> 49 | </div> 50 | </section> 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 | <section className="mx-auto py-8 sm:py-16 lg:py-20"> 32 | <article> 33 | <header className={post.image ? 'text-center' : ''}> 34 | <p className="mx-auto max-w-3xl px-4 sm:px-6"> 35 | <time dateTime={post.publishDate}>{getFormattedDate(post.publishDate)}</time> ~{' '} 36 | {/* {Math.ceil(post.readingTime)} min read */} 37 | </p> 38 | <h1 className="leading-tighter font-heading mx-auto mb-8 max-w-3xl px-4 text-4xl font-bold tracking-tighter sm:px-6 md:text-5xl"> 39 | {post.title} 40 | </h1> 41 | {post.image ? ( 42 | <Image 43 | src={post.image} 44 | className="mx-auto mt-4 mb-6 max-w-full bg-gray-400 dark:bg-slate-700 sm:rounded-md lg:max-w-6xl" 45 | sizes="(max-width: 900px) 400px, 900px" 46 | alt={post.description} 47 | loading="eager" 48 | priority 49 | width={900} 50 | height={480} 51 | /> 52 | ) : ( 53 | <div className="mx-auto max-w-3xl px-4 sm:px-6"> 54 | <div className="border-t dark:border-slate-700" /> 55 | </div> 56 | )} 57 | </header> 58 | <div 59 | className="prose-md prose-headings:font-heading prose-headings:leading-tighter container prose prose-lg mx-auto mt-8 max-w-3xl px-6 prose-headings:font-bold prose-headings:tracking-tighter prose-a:text-primary-600 prose-img:rounded-md prose-img:shadow-lg dark:prose-invert dark:prose-headings:text-slate-300 dark:prose-a:text-primary-400 sm:px-6 lg:prose-xl" 60 | dangerouslySetInnerHTML={{ 61 | __html: md({ 62 | html: true, 63 | }).render(post.content), 64 | }} 65 | /> 66 | </article> 67 | </section> 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 | <div className="mx-auto max-w-7xl px-4 sm:px-6"> 8 | <div className="xs:gap-8 grid grid-cols-4 gap-4 gap-y-8 py-8 md:py-12"> 9 | {columns.map(({ title, texts }, index) => ( 10 | <div 11 | key={`item-column-${index}`} 12 | className="col-span-4 sm:col-span-2 md:col-span-2 lg:col-span-1 xl:col-span-1" 13 | > 14 | <div className="mb-2 font-medium text-gray-800 dark:text-gray-300">{title}</div> 15 | {texts && 16 | texts.map((text, index2) => ( 17 | <p key={`item-text-${index2}`} className="text-gray-600 dark:text-slate-400"> 18 | {text} 19 | </p> 20 | ))} 21 | </div> 22 | ))} 23 | <div className="col-span-4 sm:col-span-2 md:col-span-2 lg:col-span-1 xl:col-span-1"> 24 | <div className="mb-2 font-medium text-gray-800 dark:text-gray-300">Social</div> 25 | <ul className="mb-4 -ml-2 rtl:ml-0 rtl:-mr-2 flex md:order-1 md:mb-0"> 26 | {socials.map(({ label, icon: Icon, href }, index) => ( 27 | <li key={`item-social-${index}`}> 28 | <a 29 | className="text-muted inline-flex items-center rounded-lg p-2.5 text-sm hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-700" 30 | aria-label={label} 31 | href={href} 32 | > 33 | {Icon && <Icon className="h-5 w-5" />} 34 | </a> 35 | </li> 36 | ))} 37 | </ul> 38 | </div> 39 | </div> 40 | <div className="text-muted py-6 text-sm text-gray-700 dark:text-slate-400 md:flex md:items-center md:justify-between md:py-8"> 41 | <ul className="mb-4 flex pl-2 rtl:pl-0 rtl:pr-2 md:order-1 md:mb-0"> 42 | {links && 43 | links.map(({ label, href }, index) => ( 44 | <li key={`item-link-${index}`}> 45 | <a 46 | className="duration-150 ease-in-out placeholder:transition hover:text-gray-700 hover:underline dark:text-gray-400" 47 | aria-label={label} 48 | href={href} 49 | > 50 | {label} 51 | </a> 52 | {links.length - 1 !== index && <span className="mr-1 rtl:mr-0 rtl:ml-1"> · </span>} 53 | </li> 54 | ))} 55 | </ul> 56 | {footNote} 57 | </div> 58 | </div> 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 | <div 22 | className={twMerge( 23 | `grid mx-auto gap-8 md:gap-y-12 ${ 24 | (columns || defaultColumns) === 4 25 | ? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2' 26 | : (columns || defaultColumns) === 3 27 | ? 'lg:grid-cols-3 sm:grid-cols-2' 28 | : (columns || defaultColumns) === 2 29 | ? 'sm:grid-cols-2' 30 | : 'max-w-4xl' 31 | }`, 32 | containerClass, 33 | )} 34 | > 35 | {items.map(({ title, description, icon: Icon, callToAction }, index) => ( 36 | <div key={id ? `item-${id}-${index}` : `item-grid-${index}`}> 37 | <div className={(twMerge('flex flex-row max-w-md'), panelClass)}> 38 | <div className="flex justify-center"> 39 | {Icon ? ( 40 | <Icon className={twMerge('w-6 h-6 mr-2 rtl:mr-0 rtl:ml-2', iconClass)} /> 41 | ) : DefaultIcon ? ( 42 | <DefaultIcon className={twMerge('w-6 h-6 mr-2 rtl:mr-0 rtl:ml-2', iconClass)} /> 43 | ) : null} 44 | </div> 45 | <div className="mt-0.5"> 46 | {title && <h3 className={twMerge('text-xl font-bold', titleClass)}>{title}</h3>} 47 | {description && ( 48 | <p 49 | className={twMerge(`text-gray-600 dark:text-slate-400 ${title ? 'mt-3' : ''}`, descriptionClass)} 50 | > 51 | {description} 52 | </p> 53 | )} 54 | {callToAction && ( 55 | <CTA 56 | callToAction={callToAction} 57 | linkClass={twMerge( 58 | `${ 59 | title || description ? 'mt-3' : '' 60 | } text-primary font-bold text-blue-600 hover:underline dark:text-gray-200 cursor-pointer`, 61 | actionClass, 62 | )} 63 | /> 64 | )} 65 | </div> 66 | </div> 67 | </div> 68 | ))} 69 | </div> 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 | <WidgetWrapper id={id ? id : ''} hasBackground={hasBackground} containerClass=""> 22 | {header && <Headline header={header} titleClass="text-3xl sm:text-4xl" />} 23 | <div className="flex items-stretch justify-center"> 24 | <div className="grid w-full md:grid-cols-3 md:items-center md:gap-4"> 25 | {width > 767 ? ( 26 | <div className="block h-full sm:flex sm:items-center sm:justify-between md:mx-4 md:mt-10 md:block md:px-4"> 27 | <div className="flex h-fit w-full justify-center sm:w-auto sm:justify-start"> 28 | <ul> 29 | {(tabs as Tab[]).map((tab, index) => { 30 | const onSelectTab = () => { 31 | setActiveTab(index); 32 | }; 33 | 34 | return ( 35 | <li 36 | key={`tab-${index}`} 37 | className={`mb-5 flex cursor-pointer items-center ${ 38 | activeTab === index ? 'text-primary-600 dark:text-primary-200' : '' 39 | }`} 40 | tabIndex={0} 41 | onClick={onSelectTab} 42 | > 43 | <span className="w-full text-xl hover:underline">{tab.link?.label}</span> 44 | </li> 45 | ); 46 | })} 47 | </ul> 48 | </div> 49 | </div> 50 | ) : ( 51 | <Dropdown options={tabs as Tab[]} activeTab={activeTab} onActiveTabSelected={activeTabSelectedHandler} /> 52 | )} 53 | <div className="mt-4 h-fit md:col-span-2 md:mx-4 md:mt-0 md:px-4"> 54 | {(tabs as Tab[]).map((tab, index) => ( 55 | <div key={`tab-${index}`} className=""> 56 | {activeTab === index && ( 57 | <Collapse 58 | items={tab.items as Item[]} 59 | classCollapseItem="border-b border-solid border-slate-300 dark:border-slate-500 py-5" 60 | iconUp={<IconMinus className="h-6 w-6 text-primary-600 dark:text-slate-200" />} 61 | iconDown={<IconPlus className="h-6 w-6 text-primary-600 dark:text-slate-200" />} 62 | /> 63 | )} 64 | </div> 65 | ))} 66 | </div> 67 | </div> 68 | </div> 69 | </WidgetWrapper> 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 | <WidgetWrapper id={id ? id : ''} hasBackground={hasBackground} containerClass=""> 17 | {header && <Headline header={header} titleClass="text-2xl sm:text-3xl" />} 18 | <div className="flex items-stretch justify-center"> 19 | <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3"> 20 | {testimonials.map( 21 | ({ name, job, testimonial, image, href }, index) => 22 | testimonial && ( 23 | <div 24 | key={`item-testimonial-${index}`} 25 | className={`card max-w-sm h-full ${ 26 | !callToAction && href 27 | ? 'hover:border-primary-600 hover:shadow-lg hover:transition hover:duration-100' 28 | : '' 29 | }`} 30 | > 31 | {!callToAction && href ? ( 32 | <Link href={href} target="_blank" rel="noopener noreferrer"> 33 | <ItemTestimonial 34 | name={name} 35 | job={job} 36 | testimonial={testimonial} 37 | isTestimonialUp={isTestimonialUp} 38 | hasDividerLine={true} 39 | startSlice={0} 40 | endSlice={150} 41 | image={image} 42 | containerClass="h-full" 43 | panelClass="justify-between items-stretch w-full h-full" 44 | nameJobClass="text-left rtl:text-right" 45 | jobClass="text-sm" 46 | imageClass="mr-4 rtl:mr-0 rtl:ml-4 h-10 w-10 rounded-full" 47 | /> 48 | </Link> 49 | ) : ( 50 | <ItemTestimonial 51 | name={name} 52 | job={job} 53 | testimonial={testimonial} 54 | isTestimonialUp={isTestimonialUp} 55 | hasDividerLine={true} 56 | startSlice={0} 57 | endSlice={150} 58 | image={image} 59 | containerClass="h-full" 60 | panelClass="justify-between items-stretch w-full h-full" 61 | nameJobClass="text-left rtl:text-right" 62 | jobClass="text-sm" 63 | imageClass="mr-4 rtl:mr-0 rtl:ml-4 h-10 w-10 rounded-full" 64 | /> 65 | )} 66 | </div> 67 | ), 68 | )} 69 | </div> 70 | </div> 71 | {callToAction && ( 72 | <CTA 73 | callToAction={callToAction} 74 | containerClass="flex justify-center mx-auto w-fit mt-8 md:mt-12" 75 | linkClass="btn" 76 | /> 77 | )} 78 | </WidgetWrapper> 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 | <footer className="relative border-t border-gray-200 dark:border-slate-800"> 8 | <div className="dark:bg-dark pointer-events-none absolute inset-0"></div> 9 | <div className="relative mx-auto max-w-7xl px-4 dark:text-slate-300 sm:px-6"> 10 | <div className="grid grid-cols-12 gap-4 gap-y-8 py-8 sm:gap-8 md:py-12"> 11 | <div className="col-span-12 lg:col-span-4"> 12 | <div className="mb-2"> 13 | <a className="inline-block text-xl font-bold" href="/"> 14 | {title} 15 | </a> 16 | </div> 17 | <div className="text-muted text-sm"> 18 | <ul className="mb-4 flex pr-2 rtl:pr-0 rtl:pl-2 md:order-1 md:mb-0"> 19 | {links && 20 | links.map(({ label, href }, index) => ( 21 | <li key={`item-link-${index}`}> 22 | <a 23 | className="duration-150 ease-in-out placeholder:transition hover:text-gray-700 hover:underline dark:text-gray-400" 24 | aria-label={label} 25 | href={href} 26 | > 27 | {label} 28 | </a> 29 | {links.length - 1 !== index && <span className="mr-1 rtl:mr-0 rtl:ml-1"> · </span>} 30 | </li> 31 | ))} 32 | </ul> 33 | </div> 34 | </div> 35 | {columns.map(({ title, links }, index) => ( 36 | <div key={`item-column-${index}`} className="col-span-6 md:col-span-3 lg:col-span-2"> 37 | <div className="mb-2 font-medium dark:text-gray-300">{title}</div> 38 | <ul className="text-sm"> 39 | {links && 40 | links.map(({ label, href }, index2) => ( 41 | <li key={`item-column-link-${index2}`} className="mb-2"> 42 | <a 43 | className="text-muted transition duration-150 ease-in-out hover:text-gray-700 hover:underline dark:text-gray-400" 44 | aria-label={label} 45 | href={href} 46 | > 47 | {label} 48 | </a> 49 | </li> 50 | ))} 51 | </ul> 52 | </div> 53 | ))} 54 | </div> 55 | <div className="py-6 md:flex md:items-center md:justify-between md:py-8"> 56 | <ul className="mb-4 flex md:order-1 md:ml-4 rtl:md:ml-0 rtl:md:mr-4 md:mb-0"> 57 | {socials.map(({ label, icon: Icon, href }, index) => ( 58 | <li key={`item-social-${index}`}> 59 | <a 60 | className="text-muted inline-flex items-center rounded-lg p-2.5 text-sm hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-700" 61 | aria-label={label} 62 | href={href} 63 | > 64 | {Icon && <Icon className="h-5 w-5" />} 65 | </a> 66 | </li> 67 | ))} 68 | </ul> 69 | {footNote} 70 | </div> 71 | </div> 72 | </footer> 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 | <div className="card mb-6 px-5 py-4"> 6 | <div className="flex items-center justify-between"> 7 | <div className="w-full"> 8 | <h3 className="mb-3 text-xl font-bold text-gray-700 dark:text-white">{title}</h3> 9 | <p className="text-gray-600 dark:text-slate-400">{description}</p> 10 | </div> 11 | {href && ( 12 | <div className="flex h-10 w-10 items-center justify-center"> 13 | <IconChevronRight className="h-6 w-6 text-primary-600 dark:text-slate-200" /> 14 | </div> 15 | )} 16 | </div> 17 | {form && ( 18 | <div className="mt-2"> 19 | <form className="rounded-md border border-gray-400 bg-white shadow-md"> 20 | <div className="flex items-center"> 21 | {form.icon && ( 22 | <span className="rounded-bl-md rtl:rounded-bl-none rtl:rounded-br-md rounded-tl-md rtl:rounded-tl-none rtl:rounded-tr-md border-r-[1px] rtl:border-r-none rtl:border-l-[1px] border-gray-400 px-2 py-2 dark:bg-[#3b3b3b]"> 23 | <form.icon className="h-6 w-6 text-primary-600 dark:text-gray-400" /> 24 | </span> 25 | )} 26 | <input 27 | type={form.input.type} 28 | name={form.input.name} 29 | autoComplete={form.input.autocomplete} 30 | placeholder={form.input.placeholder} 31 | className="w-full py-2 px-4 dark:text-gray-300" 32 | /> 33 | <button 34 | type={form.btn.type} 35 | className="rounded-br-md rtl:rounded-br-none rtl:rounded-bl-md rounded-tr-md rtl:rounded-tr-none rtl:rounded-tl-md border-l-[1px] rtl:border-l-none rtl:border-r-[1px] border-gray-400 bg-primary-600 px-4 py-2 text-white" 36 | > 37 | {form.btn.title} 38 | </button> 39 | </div> 40 | </form> 41 | </div> 42 | )} 43 | </div> 44 | ); 45 | 46 | const CallToAction2 = ({ title, subtitle, items }: CallToActionProps) => ( 47 | <section className="bg-primary-900 text-gray-200" id="callToActionTwo"> 48 | <div className="mx-auto max-w-7xl px-4 py-16 lg:px-8 lg:pt-20"> 49 | <div className="row-gap-10 grid gap-6 md:grid-cols-2"> 50 | <div className="mx-auto md:my-auto md:ml-0 md:pb-6 md:pr-24"> 51 | <h2 className="mb-3 flex justify-center text-6xl font-bold md:justify-start">{title}</h2> 52 | <p className="text-center text-xl text-gray-200 dark:text-slate-300 md:text-left rtl:md:text-right"> 53 | {subtitle} 54 | </p> 55 | </div> 56 | <div className="relative -mb-6"> 57 | {items && 58 | items.map(({ title, description, href, form }, index) => ( 59 | <div key={`call-to-action-item-${index}`}> 60 | {href ? ( 61 | <a 62 | href={href} 63 | className="w-full sm:mb-0" 64 | target="_blank" 65 | rel="noopener noreferrer" 66 | key={`item-cta-${index}`} 67 | > 68 | <Card title={title} description={description} href={href} form={form} /> 69 | </a> 70 | ) : ( 71 | <Card title={title} description={description} href={href} form={form} /> 72 | )} 73 | </div> 74 | ))} 75 | </div> 76 | </div> 77 | </div> 78 | </section> 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<number>(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 | <WidgetWrapper id={id ? id : ''} hasBackground={hasBackground} containerClass=""> 30 | {header && <Headline header={header} titleClass="text-2xl sm:text-3xl" />} 31 | <div className="card flex overflow-hidden mx-auto max-w-6xl"> 32 | {testimonials.map( 33 | ({ name, job, testimonial, image }, index) => 34 | testimonial && ( 35 | <div 36 | key={`item-testimonial-${index}`} 37 | className="mx-auto inline-flex flex-col items-stretch justify-center min-w-full transition-all duration-300 linear max-w-6xl" 38 | style={{ transform: `translate(-${activeIndex * 100}%)` }} 39 | > 40 | <ItemTestimonial 41 | name={name} 42 | job={job} 43 | testimonial={testimonial} 44 | isTestimonialUp={isTestimonialUp} 45 | image={image} 46 | containerClass="flex w-full h-full px-4 py-8 text-center lg:py-16 lg:px-6" 47 | panelClass="w-full md:max-w-md lg:max-w-screen-sm mx-auto" 48 | imageClass="w-6 h-6 rounded-full" 49 | dataClass="mt-8 space-x-3 mx-auto" 50 | nameJobClass="flex flex-row items-center divide-x-2 divide-gray-500 dark:divide-gray-700" 51 | nameClass="pr-3 font-medium text-gray-900 dark:text-white" 52 | jobClass="pl-3 text-sm font-light text-gray-500 dark:text-gray-400" 53 | testimonialClass="text-2xl font-medium text-gray-900 dark:text-white" 54 | /> 55 | <div className="md:absolute md:inset-0 flex items-center justify-center md:justify-between p-4 mb-6 md:mb-0"> 56 | <button onClick={() => handleGoToPrevious(index)}> 57 | <IconChevronLeft 58 | className={`w-12 h-12 mr-4 ${ 59 | activeIndex === firstIndex 60 | ? 'cursor-not-allowed text-gray-400 dark:text-gray-600' 61 | : 'text-black dark:text-white' 62 | }`} 63 | /> 64 | </button> 65 | <button onClick={() => handleGoToNext(index)}> 66 | <IconChevronRight 67 | className={`w-12 h-12 ml-4 ${ 68 | activeIndex === lastIndex 69 | ? 'cursor-not-allowed text-gray-400 dark:text-gray-600' 70 | : 'text-black dark:text-white' 71 | }`} 72 | /> 73 | </button> 74 | </div> 75 | </div> 76 | ), 77 | )} 78 | </div> 79 | </WidgetWrapper> 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 | <span className="hidden md:inline">{`Thank you for considering us for your project! We're excited to hear from you.`}</span>{' '} 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 | <span className="hidden md:inline">{`So we can better understand your needs and get the process started smoothly.`}</span> 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 | <WidgetWrapper id={id ? id : ''} hasBackground={hasBackground} containerClass=""> 10 | {header && <Headline header={header} containerClass="max-w-5xl" titleClass="text-2xl sm:text-3xl" />} 11 | <div className="flex items-stretch justify-center"> 12 | <div className="grid grid-cols-3 gap-3 dark:text-white sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3"> 13 | {prices && 14 | prices.map( 15 | ({ title, subtitle, price, period, items, callToAction, hasRibbon = false, ribbonTitle }, index) => ( 16 | <div 17 | className="col-span-3 mx-auto flex w-full sm:col-span-1 md:col-span-1 lg:col-span-1 xl:col-span-1" 18 | key={`pricing-${index}`} 19 | > 20 | {price && period && ( 21 | <div className="card max-w-sm flex flex-col justify-between text-center"> 22 | {hasRibbon && ribbonTitle && ( 23 | <div className="absolute right-[-5px] rtl:right-auto rtl:left-[-5px] top-[-5px] z-[1] h-[100px] w-[100px] overflow-hidden text-right"> 24 | <span className="absolute top-[19px] right-[-21px] rtl:right-auto rtl:left-[-21px] block w-full rotate-45 rtl:-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-['']"> 25 | {ribbonTitle} 26 | </span> 27 | </div> 28 | )} 29 | <div className="px-2 py-0"> 30 | {title && ( 31 | <h3 className="text-center text-xl font-semibold uppercase leading-6 tracking-wider mb-2"> 32 | {title} 33 | </h3> 34 | )} 35 | {subtitle && ( 36 | <p className="font-light sm:text-lg text-gray-600 dark:text-slate-400">{subtitle}</p> 37 | )} 38 | <div className="my-8"> 39 | <div className="flex items-center justify-center text-center mb-1"> 40 | <span className="text-5xl">$</span> 41 | <span className="text-6xl font-extrabold">{price}</span> 42 | </div> 43 | <span className="text-base leading-6 lowercase text-gray-600 dark:text-slate-400"> 44 | {period} 45 | </span> 46 | </div> 47 | {items && ( 48 | <div className="my-8 md:my-10 space-y-2 text-left"> 49 | <ItemGrid 50 | id={id} 51 | items={items} 52 | columns={1} 53 | defaultIcon={IconCheck} 54 | containerClass="gap-2 md:gap-y-2" 55 | panelClass="flex items-start" 56 | iconClass="w-4 h-4 mt-1.5 mr-3 rtl:mr-0 rtl:ml-3 flex items-center justify-center rounded-full border-2 border-primary-600 bg-primary-600 text-white dark:text-slate-200" 57 | /> 58 | </div> 59 | )} 60 | </div> 61 | {callToAction && ( 62 | <CTA 63 | callToAction={callToAction as CallToActionType} 64 | linkClass={`btn ${hasRibbon ? 'btn-primary' : ''}`} 65 | /> 66 | )} 67 | </div> 68 | )} 69 | </div> 70 | ), 71 | )} 72 | </div> 73 | </div> 74 | </WidgetWrapper> 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<boolean[]>(new Array(checkboxes && checkboxes.length).fill(false)); 22 | 23 | // Update the value of the entry fields 24 | const changeInputValueHandler = (event: React.ChangeEvent<HTMLInputElement>) => { 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<HTMLInputElement>) => { 35 | setRadioBtnValue(event.target.value); 36 | }; 37 | 38 | // Update the textarea value 39 | const changeTextareaHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => { 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 | <form id="contactForm" className={twMerge('', containerClass)}> 56 | {title && <h2 className={`${description ? 'mb-2' : 'mb-4'} text-2xl font-bold`}>{title}</h2>} 57 | {description && <p className="mb-4">{description}</p>} 58 | <div className="mb-6"> 59 | {/* Inputs */} 60 | <div className="mx-0 mb-1 sm:mb-4"> 61 | {inputs && 62 | inputs.map(({ type, label, name, autocomplete, placeholder }, index) => ( 63 | <div key={`item-input-${index}`} className="mx-0 mb-1 sm:mb-4"> 64 | <label htmlFor={name} className="pb-1 text-xs uppercase tracking-wider"> 65 | {label} 66 | </label> 67 | <input 68 | type={type} 69 | id={name} 70 | name={name} 71 | autoComplete={autocomplete} 72 | value={inputValues[index]} 73 | onChange={changeInputValueHandler} 74 | placeholder={placeholder} 75 | className="mb-2 w-full rounded-md border border-gray-400 py-2 pl-2 pr-4 shadow-md dark:text-gray-300 sm:mb-0" 76 | /> 77 | </div> 78 | ))} 79 | </div> 80 | {/* Radio buttons */} 81 | {radioBtns && ( 82 | <div className="mx-0 mb-1 sm:mb-3"> 83 | <span className="pb-1 text-xs uppercase tracking-wider">{radioBtns?.label}</span> 84 | <div className="flex flex-wrap"> 85 | {radioBtns.radios.map(({ label }, index) => ( 86 | <div key={`radio-btn-${index}`} className="mr-4 items-baseline"> 87 | <input 88 | id={label} 89 | type="radio" 90 | name={label} 91 | value={`value${index}`} 92 | checked={radioBtnValue === `value${index}`} 93 | onChange={changeRadioBtnsHandler} 94 | className="cursor-pointer" 95 | /> 96 | <label htmlFor={label} className="ml-2"> 97 | {label} 98 | </label> 99 | </div> 100 | ))} 101 | </div> 102 | </div> 103 | )} 104 | {/* Textarea */} 105 | {textarea && ( 106 | <div className={`mx-0 mb-1 sm:mb-4`}> 107 | <label htmlFor={textarea.name} className="pb-1 text-xs uppercase tracking-wider"> 108 | {textarea.label} 109 | </label> 110 | <textarea 111 | id={textarea.name} 112 | name={textarea.name} 113 | cols={textarea.cols} 114 | rows={textarea.rows} 115 | value={textareaValues} 116 | onChange={(e) => changeTextareaHandler(e)} 117 | placeholder={textarea.placeholder} 118 | className="mb-2 w-full rounded-md border border-gray-400 py-2 pl-2 pr-4 shadow-md dark:text-gray-300 sm:mb-0" 119 | /> 120 | </div> 121 | )} 122 | {/* Checkboxes */} 123 | {checkboxes && ( 124 | <div className="mx-0 mb-1 sm:mb-4"> 125 | {checkboxes.map(({ label }, index) => ( 126 | <div key={`checkbox-${index}`} className="mx-0 my-1 flex items-baseline"> 127 | <input 128 | id={label} 129 | type="checkbox" 130 | name={label} 131 | checked={checkedState[index]} 132 | onChange={() => changeCheckboxHandler(index)} 133 | className="cursor-pointer" 134 | /> 135 | <label htmlFor={label} className="ml-2"> 136 | {label} 137 | </label> 138 | </div> 139 | ))} 140 | </div> 141 | )} 142 | </div> 143 | {btn && ( 144 | <div 145 | className={`${btnPosition === 'left' ? 'text-left' : btnPosition === 'right' ? 'text-right' : 'text-center'}`} 146 | > 147 | <button type={btn.type || 'button'} className="btn btn-primary sm:mb-0"> 148 | {btn.title} 149 | </button> 150 | </div> 151 | )} 152 | </form> 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 | <br> 14 | 15 | <img src="./screenshot.jpg" alt="Tailnext Theme Screenshot"> 16 | 17 | [![onWidget](https://custom-icon-badges.demolab.com/badge/made%20by%20-onWidget-556bf2?style=flat-square&logo=onwidget&logoColor=white&labelColor=101827)](https://onwidget.com) 18 | [![License](https://img.shields.io/github/license/onwidget/tailnext?style=flat-square&color=dddddd&labelColor=000000)](https://github.com/onwidget/tailnext/blob/main/LICENSE.md) 19 | [![Maintained](https://img.shields.io/badge/maintained%3F-yes-brightgreen.svg?style=flat-square)](https://github.com/onwidget) 20 | [![Contributions Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/onwidget/tailnext#contributing) 21 | [![Known Vulnerabilities](https://snyk.io/test/github/onwidget/tailnext/badge.svg?style=flat-square)](https://snyk.io/test/github/onwidget/tailnext) 22 | 23 | <br> 24 | 25 | <details open> 26 | <summary>Table of Contents</summary> 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 | </details> 40 | 41 | <br> 42 | 43 | ## Demo 44 | 45 | 📌 [https://tailnext.vercel.app/](https://tailnext.vercel.app/) 46 | 47 | <br> 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 | [![Edit Tailnext on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](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 | <br> 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 | <br> 118 | 119 | ### Configuration 120 | 121 | Coming soon .. 122 | 123 | <br> 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 | [![Netlify Deploy button](https://www.netlify.com/img/deploy/button.svg)](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 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fonwidget%2Ftailnext) 150 | 151 | <br> 152 | 153 | ## Roadmap 154 | 155 | Coming soon .. 156 | 157 | <br> 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 | <div className="mr-4 rtl:mr-0 rtl:ml-4 text-sm"> 211 | <span className="float-left rtl:float-right mr-1.5 rtl:mr-0 rtl:ml-1.5 h-5 w-5 rounded-sm bg-[url(https://onwidget.com/favicon/favicon-32x32.png)] bg-cover md:-mt-0.5 md:h-6 md:w-6"></span> 212 | <span> 213 | Made by{' '} 214 | <a 215 | className="font-semibold text-slate-900 dark:text-gray-200 hover:text-blue-600 hover:underline dark:hover:text-blue-600" 216 | href="https://onwidget.com/" 217 | > 218 | {' '} 219 | onWidget 220 | </a>{' '} 221 | · All rights reserved. 222 | </span> 223 | </div> 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 | <div className="mr-4 rtl:mr-0 rtl:ml-4 text-sm"> 262 | <span className="float-left rtl:float-right mr-1.5 rtl:mr-0 rtl:ml-1.5 h-5 w-5 rounded-sm bg-[url(https://onwidget.com/favicon/favicon-32x32.png)] bg-cover md:-mt-0.5 md:h-6 md:w-6"></span> 263 | <span> 264 | Made by{' '} 265 | <a 266 | className="font-semibold text-slate-900 dark:text-gray-200 hover:text-blue-600 hover:underline dark:hover:text-blue-600" 267 | href="https://onwidget.com/" 268 | > 269 | {' '} 270 | onWidget 271 | </a>{' '} 272 | · All rights reserved. 273 | </span> 274 | </div> 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 | <span className="hidden md:inline"> 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 | </span>{' '} 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<boolean[]>(updatedIsDropdownOpen as boolean[]); 26 | const [isToggleMenuOpen, setIsToggleMenuOpen] = useState<boolean>(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 | <header 60 | className={`top-0 z-40 mx-auto w-full flex-none bg-white transition-all duration-100 ease-in dark:bg-slate-900 md:bg-white/90 md:backdrop-blur-sm dark:md:bg-slate-900/90 ${ 61 | isSticky ? 'sticky' : 'relative' 62 | } ${isToggleMenuOpen ? 'h-screen md:h-auto' : 'h-auto'}`} 63 | id="header" 64 | > 65 | <div className="mx-auto w-full max-w-7xl md:flex md:justify-between md:py-3.5 md:px-4"> 66 | <div 67 | className={`flex justify-between py-3 px-3 md:py-0 md:px-0 ${ 68 | isToggleMenuOpen 69 | ? 'md:bg-transparent md:dark:bg-transparent md:border-none bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-600' 70 | : '' 71 | }`} 72 | > 73 | <Link 74 | className="flex items-center" 75 | href="/" 76 | onClick={() => 77 | isToggleMenuOpen ? handleToggleMenuOnClick() : setIsDropdownOpen(updatedIsDropdownOpen as boolean[]) 78 | } 79 | > 80 | <Logo /> 81 | </Link> 82 | <div className="flex items-center md:hidden"> 83 | <ToggleMenu handleToggleMenuOnClick={handleToggleMenuOnClick} isToggleMenuOpen={isToggleMenuOpen} /> 84 | </div> 85 | </div> 86 | <nav 87 | className={`${isToggleMenuOpen ? 'block px-3' : 'hidden'} h-screen md:w-full ${ 88 | position === 'right' ? 'justify-end' : position === 'left' ? 'justify-start' : 'justify-center' 89 | } w-auto overflow-y-auto dark:text-slate-200 md:mx-5 md:flex md:h-auto md:items-center md:overflow-visible`} 90 | aria-label="Main navigation" 91 | > 92 | <ul 93 | ref={ref} 94 | className="flex w-full flex-col mt-2 mb-36 md:m-0 text-xl md:w-auto md:flex-row md:self-center md:pt-0 md:text-base" 95 | > 96 | {links && 97 | links.map(({ label, href, icon: Icon, links }, index) => ( 98 | <li key={`item-link-${index}`} className={links?.length ? 'dropdown' : ''}> 99 | {links && links.length ? ( 100 | <> 101 | <button 102 | className="flex items-center px-4 py-3 font-medium transition duration-150 ease-in-out hover:text-gray-900 dark:hover:text-white" 103 | onClick={() => handleDropdownOnClick(index)} 104 | > 105 | {label}{' '} 106 | {Icon && ( 107 | <Icon 108 | className={`${ 109 | isDropdownOpen[index] ? 'rotate-180' : '' 110 | } ml-0.5 rtl:ml-0 rtl:mr-0.5 hidden h-3.5 w-3.5 md:inline`} 111 | /> 112 | )} 113 | </button> 114 | <ul 115 | className={`${ 116 | isDropdownOpen[index] ? 'block' : 'md:hidden' 117 | } rounded pl-4 font-medium drop-shadow-xl md:absolute md:min-w-[200px] md:bg-white/90 md:pl-0 md:backdrop-blur-md dark:md:bg-slate-900/90 md:border md:border-gray-200 md:dark:border-slate-700`} 118 | > 119 | {links.map(({ label: label2, href: href2 }, index2) => ( 120 | <li key={`item-link-${index2}`}> 121 | <Link 122 | className="whitespace-no-wrap block py-2 px-5 first:rounded-t last:rounded-b dark:hover:bg-gray-700 md:hover:bg-gray-200" 123 | href={href2 as string} 124 | onClick={() => 125 | isToggleMenuOpen ? handleToggleMenuOnClick() : handleCloseDropdownOnClick(index) 126 | } 127 | > 128 | {label2} 129 | </Link> 130 | </li> 131 | ))} 132 | </ul> 133 | </> 134 | ) : ( 135 | <Link 136 | className="flex items-center px-4 py-3 font-medium transition duration-150 ease-in-out hover:text-gray-900 dark:hover:text-white" 137 | href={href as string} 138 | onClick={() => (isToggleMenuOpen ? handleToggleMenuOnClick() : handleDropdownOnClick(index))} 139 | > 140 | {label} 141 | </Link> 142 | )} 143 | </li> 144 | ))} 145 | </ul> 146 | </nav> 147 | <div 148 | className={`${ 149 | isToggleMenuOpen ? 'block' : 'hidden' 150 | } fixed bottom-0 left-0 w-full justify-end p-3 md:static md:mb-0 md:flex md:w-auto md:self-center md:p-0 md:bg-transparent md:dark:bg-transparent md:border-none bg-white dark:bg-slate-900 border-t border-gray-200 dark:border-slate-600`} 151 | > 152 | <div className="flex w-full items-center justify-between md:w-auto"> 153 | {showToggleTheme && <ToggleDarkMode />} 154 | {showRssFeed && ( 155 | <Link 156 | className="text-muted inline-flex items-center rounded-lg p-2.5 text-sm hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-700" 157 | aria-label="RSS Feed" 158 | href="" 159 | > 160 | <IconRss className="h-5 w-5" /> 161 | </Link> 162 | )} 163 | {actions && actions.length > 0 && ( 164 | <div className="ml-4 rtl:ml-0 rtl:mr-4 flex w-max flex-wrap justify-end"> 165 | {actions.map((callToAction, index) => ( 166 | <CTA 167 | key={`item-action-${index}`} 168 | callToAction={callToAction as CallToActionType} 169 | linkClass="btn btn-primary m-1 py-2 px-5 text-sm font-semibold shadow-none md:px-6" 170 | /> 171 | ))} 172 | </div> 173 | )} 174 | </div> 175 | </div> 176 | </div> 177 | </header> 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<Radio>; 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<Input>; 97 | radioBtns?: RadioBtn; 98 | textarea?: Textarea; 99 | checkboxes?: Array<Checkbox>; 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<string>; 114 | href?: string; 115 | form?: SmallForm; 116 | icon?: Icon; 117 | callToAction?: CallToActionType; 118 | }; 119 | 120 | type ItemGrid = { 121 | id?: string; 122 | items?: Array<Item>; 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<Item>; 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<Item>; 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<Item>; 192 | callToAction?: CallToActionType; 193 | hasRibbon?: boolean; 194 | ribbonTitle?: string; 195 | }; 196 | 197 | type Column = { 198 | title: string; 199 | items: Array<Item>; 200 | callToAction?: CallToActionType; 201 | }; 202 | 203 | type MenuLink = Link & { 204 | links?: Array<Link>; 205 | }; 206 | 207 | type Links = { 208 | title?: string; 209 | links?: Array<Link>; 210 | texts?: Array<string>; 211 | }; 212 | 213 | type Tab = { 214 | link?: Link; 215 | items: Array<Item>; 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<HTMLButtonElement>; 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<Item>; 249 | columns?: number; 250 | tabs?: Array<Tab>; 251 | callToAction?: CallToActionType; 252 | }; 253 | 254 | type CollapseProps = { 255 | items: Array<Item>; 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<Item>; 266 | }; 267 | 268 | type FeaturesProps = Widget & { 269 | header?: Header; 270 | items?: Array<Item>; 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<Item>; 284 | image?: Image; 285 | isReversed?: boolean; 286 | isAfterContent?: boolean; 287 | }; 288 | 289 | type StepsProps = Widget & { 290 | header?: Header; 291 | items: Array<Item>; 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<Team>; 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<Testimonial>; 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<Price>; 323 | }; 324 | 325 | type ComparisonProps = Widget & { 326 | header?: Header; 327 | columns: Array<Column>; 328 | }; 329 | 330 | type StatsProps = Widget & { 331 | items: Array<Item>; 332 | }; 333 | 334 | type SocialProofProps = Widget & { 335 | images: Array<Image>; 336 | }; 337 | 338 | type ContactProps = Widget & { 339 | header?: Header; 340 | content?: string; 341 | items?: Array<Item>; 342 | form: FormProps; 343 | }; 344 | 345 | type FooterProps = { 346 | title?: string; 347 | links?: Array<Link>; 348 | columns: Array<Links>; 349 | socials: Array<Link>; 350 | footNote?: string | ReactElement; 351 | theme?: string; 352 | }; 353 | 354 | type HeaderProps = { 355 | links?: Array<MenuLink>; 356 | actions?: Array<CallToActionType>; 357 | // actions?: Array<ActionLink>; 358 | isSticky?: boolean; 359 | showToggleTheme?: boolean; 360 | showRssFeed?: boolean; 361 | position?: 'center' | 'right' | 'left'; 362 | }; 363 | --------------------------------------------------------------------------------