OpenSaaS Redesign - Add ShadCN, and redesign OpenSaaS.sh landing page (#447)

* add shadcn and shadcn script

* cleanup

* navbar and announcement

* remove unnecessary documentation

* cleanup

* dark mode switcher

* update hero

* update features

* update clients

* update testimonials

* update faq

* add avatar and use it in hero

* update the demo app

* update pricing page

* update file upload page

* update account page

* update dropdown

* fix mobile menu

* use card for testimonials

* update analytics to use new color tokens

* use sheet for mobile nav bar

* update user table

* update settings page to use shadcn components

* update testimonials to use new design

* add section title component

* update components to use section header

* add gradients

* add secondary muted

* add dynamic navbar

* cleanup

* fix color tokens for the dark mode

* don't scale all the cards

* Examples component

* fix the carousel scrolling into view

* add highlighted feature component

* add features grid

* cleanup

* fix navbar announcment sticking

* fix padding

* cleanup

* cleanup

* cleanup

* cleanup

* cleanup

* more robust on mouse leave for examples carousel

* auto scroll threshold set to 1

* generate app diff fo creating opensaassh

* add orbit

* fix mobile layout for Hero

* update template FeaturesGrid, and fix logos location

* Update the title for the features grid on landing

* update testimoinals layout

* update contentSections to support new testimonials

* update examples carousel

* update examples carouselui

* cleanup

* fix navbar layout when Announcement not present

* add highlighted features in examples

* cleanup

* fix faq component

* fix testimonials UI

* cleanup

* cleanup

* update contentSections

* update the ui limits

* make highlighted feature items centered

* remove inconsistent styles

* remove legacy classnames

* standardize chart cards

* fix dark mode ui issue with dropdown

* make it so that you can't change your own admin status

* fix changes with filtering on user table

* make filtering more intuitive

* fix calendar visibility issues

* remove forms pages

* use type tel for phone number

* button page redo

* clean up breadrcrumb and remove comments from default layout

* clean up navbar

* throttle scroll for navbar

* clean up remaining template items

* clean up

* fix the examples carousel so that it automatically scrolls

* fix demo app page

* fix FeaturesGrid types

* fix highlighted feature

* fix section title

* Replace old icons

* Remove package-lock.json

* fix opensaas issues

* remove all legacy icons

* remove accidental package files

* update throttleWithTrailingInvocation

* refactor the FeaturesGrid

* clean up highlithted feature

* clean up highlithted feature

* fix behavior of ExamplesCarousel

* clean up ExamplesCarousel

* fix wrong copy and layout on the landing for opensaas.sh

* center examples on the page if there are not enough of them

* Fix layout of pricing

* fix wrong link

* color of sidebar on admin

* add new examples

* fix icon layout

* adds specific tokens for opensaas landing

* layout fix

* remove leftover custom svgs

* update layout to better match figma

* use kebab-case for forms

* revert to h3

* png -> webp

* Update layout for bento grid for opensaas

* parse day views only once

* dropdown edit delete to shadcn

* remove the remaining svgs

* make useDebounce generic

* address small pr comments

* update examples carousel

* remove the banner from the hero

* section title subtitle → description

* remove unnecessary files

* address layout comments for opensaas.sh

* add back git star count

* update contentSections to have currently deployed copy

* useRef instaed of class query

* address layout comments for opensaas.sh

* adjust padding for RepoInfo on diff

* fix gradient on template

* Revert "fix gradient on template"

This reverts commit 4b45f7f437.

* fix gradient on template

* add AI Ready highlighted feature

* update landing page features & add LLM copy button and

Introduced a 'Copy URL for LLMs' button to the blog navbar by creating CopyForLlmButton.astro and integrating it into a new MyRightNavBarItems.astro component, replacing the previous theme select. Updated astro.config.mjs to use the new component. In the app template, added an example highlighted feature component to the landing page and updated testimonial avatars. Also enabled Google, GitHub, Discord, and Slack auth providers in main.wasp.

* Update NavBar announcement for Product Hunt launch

Refactored the Announcement component in NavBar to promote the Open SaaS v2.0 Product Hunt launch, including dynamic messaging based on launch date and updated links. Also updated the landing page to import and render new example components.

---------

Co-authored-by: vincanger <70215737+vincanger@users.noreply.github.com>
This commit is contained in:
Fran
2025-07-28 18:29:22 +02:00
committed by GitHub
parent 318047fa8c
commit 2caac6c1de
158 changed files with 6345 additions and 4772 deletions

View File

@@ -1,6 +1,6 @@
--- template/app/README.md
+++ opensaas-sh/app/README.md
@@ -1,12 +1,25 @@
@@ -1,6 +1,8 @@
-# <YOUR_APP_NAME>
+# opensaas.sh (demo) app
@@ -9,6 +9,10 @@
+
+It is deployed to https://opensaas.sh and serves both as a landing page for Open Saas and as a demo app.
## UI Components
@@ -8,9 +10,20 @@
## Development
+### .env files

View File

@@ -1,7 +1,7 @@
src/client/static/avatar-placeholder.webp
src/client/static/da-boi.webp
src/client/static/open-saas-banner.webp
src/landing-page/logos/SalesforceLogo.tsx
src/client/static/open-saas-banner-dark.png
src/client/static/open-saas-banner-light.png
src/landing-page/components/Hero.tsx
src/landing-page/contentSections.ts
src/payment/lemonSqueezy/checkoutUtils.ts
src/payment/lemonSqueezy/paymentDetails.ts
src/payment/lemonSqueezy/paymentProcessor.ts

View File

@@ -119,3 +119,11 @@
httpRoute: (POST, "/payments-webhook")
}
//#endregion
@@ -281,7 +279,6 @@
component: import AdminCalendar from "@src/admin/elements/calendar/CalendarPage"
}
-
route AdminUIButtonsRoute { path: "/admin/ui/buttons", to: AdminUIButtonsPage }
page AdminUIButtonsPage {
authRequired: true,

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
--- template/app/package.json
+++ opensaas-sh/app/package.json
@@ -1,13 +1,17 @@
@@ -1,6 +1,11 @@
{
"name": "opensaas",
"type": "module",
@@ -12,10 +12,11 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.523.0",
"@aws-sdk/s3-presigned-post": "^3.750.0",
"@aws-sdk/s3-request-presigner": "^3.523.0",
"@google-analytics/data": "4.1.0",
"@headlessui/react": "1.7.13",
- "@lemonsqueezy/lemonsqueezy.js": "^3.2.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.7",
"apexcharts": "3.41.0",
@@ -36,6 +41,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.60.0",
"react-hot-toast": "^2.4.1",
+ "react-icons": "^5.5.0",
"react-router-dom": "^6.26.2",
"stripe": "11.15.0",
"tailwind-merge": "^2.6.0",

View File

@@ -1,91 +0,0 @@
--- template/app/src/admin/dashboards/analytics/AnalyticsDashboardPage.tsx
+++ opensaas-sh/app/src/admin/dashboards/analytics/AnalyticsDashboardPage.tsx
@@ -1,4 +1,6 @@
+import { Link } from 'wasp/client/router';
import { type AuthUser } from 'wasp/auth';
+import { useState, useEffect, useMemo } from 'react';
import { useQuery, getDailyStats } from 'wasp/client/operations';
import TotalSignupsCard from './TotalSignupsCard';
import TotalPageViewsCard from './TotalPageViewsCard';
@@ -11,16 +13,58 @@
import { cn } from '../../../client/cn';
const Dashboard = ({ user }: { user: AuthUser }) => {
+ const [isDemoInfoVisible, setIsDemoInfoVisible] = useState(false);
useRedirectHomeUnlessUserIsAdmin({ user });
const { data: stats, isLoading, error } = useQuery(getDailyStats);
+ const didUserCloseDemoInfo = localStorage.getItem('didUserCloseDemoInfo') === 'true';
+
+ useEffect(() => {
+ if (didUserCloseDemoInfo || !stats) {
+ setIsDemoInfoVisible(false);
+ } else if (!didUserCloseDemoInfo && stats) {
+ setIsDemoInfoVisible(true);
+ }
+ }, [stats]);
+
+ const handleDemoInfoClose = () => {
+ try {
+ localStorage.setItem('didUserCloseDemoInfo', 'true');
+ setIsDemoInfoVisible(false);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const sortedSources = useMemo(() => {
+ return stats?.dailyStats?.sources?.slice().sort((a, b) => b.visitors - a.visitors);
+ }, [stats?.dailyStats?.sources]);
+
return (
<DefaultLayout user={user}>
+ {isDemoInfoVisible && (
+ <div className='fixed z-999 bottom-0 mb-2 left-1/2 -translate-x-1/2 lg:mb-4 bg-gray-700 rounded-full px-3.5 py-2 text-sm text-white duration-300 ease-in-out hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-600'>
+ <div className='px-4 flex flex-row gap-2 items-center my-1'>
+ <span className='text-gray-100 text-center'>
+ This is actual data from Stripe test purchases. <br /> Try out purchasing a{' '}
+ <Link to='/pricing' className='underline text-yellow-400'>
+ test product
+ </Link>
+ !
+ </span>
+ <button className=' pl-2.5 text-gray-400 text-xl font-bold' onClick={() => handleDemoInfoClose()}>
+ X
+ </button>
+ </div>
+ </div>
+ )}
<div className='relative'>
- <div className={cn({
- 'opacity-25': !stats,
- })}>
+ <div
+ className={cn({
+ 'opacity-25': !stats,
+ })}
+ >
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 xl:grid-cols-4 2xl:gap-7.5'>
<TotalPageViewsCard
totalPageViews={stats?.dailyStats.totalViews}
@@ -39,7 +83,7 @@
<RevenueAndProfitChart weeklyStats={stats?.weeklyStats} isLoading={isLoading} />
<div className='col-span-12 xl:col-span-8'>
- <SourcesTable sources={stats?.dailyStats?.sources} />
+ <SourcesTable sources={sortedSources} />
</div>
</div>
</div>
@@ -47,9 +91,7 @@
{!stats && (
<div className='absolute inset-0 flex items-start justify-center bg-white/50 dark:bg-boxdark-2/50'>
<div className='rounded-lg bg-white p-8 shadow-lg dark:bg-boxdark'>
- <p className='text-2xl font-bold text-boxdark dark:text-white'>
- No daily stats generated yet
- </p>
+ <p className='text-2xl font-bold text-boxdark dark:text-white'>No daily stats generated yet</p>
<p className='mt-2 text-sm text-bodydark2'>
Stats will appear here once the daily stats job has run
</p>

View File

@@ -1,54 +0,0 @@
--- template/app/src/admin/dashboards/users/UsersDashboardPage.tsx
+++ opensaas-sh/app/src/admin/dashboards/users/UsersDashboardPage.tsx
@@ -1,14 +1,50 @@
import { type AuthUser } from 'wasp/auth';
+import { useState, useEffect } from 'react';
import UsersTable from './UsersTable';
import Breadcrumb from '../../layout/Breadcrumb';
import DefaultLayout from '../../layout/DefaultLayout';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
const Users = ({ user }: { user: AuthUser }) => {
- useRedirectHomeUnlessUserIsAdmin({user})
+ const [isDemoInfoVisible, setIsDemoInfoVisible] = useState(false);
+ useRedirectHomeUnlessUserIsAdmin({user});
+
+ useEffect(() => {
+ try {
+ if (localStorage.getItem('isDemoInfoVisible') === 'false') {
+ // do nothing
+ } else {
+ setIsDemoInfoVisible(true);
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }, []);
+
+ const handleDemoInfoClose = () => {
+ try {
+ localStorage.setItem('isDemoInfoVisible', 'false');
+ setIsDemoInfoVisible(false);
+ } catch (error) {
+ console.error(error);
+ }
+ };
return (
<DefaultLayout user={user}>
+ {/* Floating Demo Announcement */}
+ {isDemoInfoVisible && (
+ <div className='fixed z-999 bottom-0 mb-2 left-1/2 -translate-x-1/2 lg:mb-4 bg-gray-700 rounded-full px-3.5 py-2 text-sm text-white duration-300 ease-in-out hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-600'>
+ <div className='px-4 flex flex-row gap-2 items-center my-1'>
+ <span className='text-gray-100'>
+ You are viewing mock user data only ;)
+ </span>
+ <button className=' pl-2.5 text-gray-400 text-xl font-bold' onClick={() => handleDemoInfoClose()}>
+ X
+ </button>
+ </div>
+ </div>
+ )}
<Breadcrumb pageName='Users' />
<div className='flex flex-col gap-10'>
<UsersTable />

View File

@@ -1,11 +1,11 @@
--- template/app/src/admin/dashboards/users/UsersTable.tsx
+++ opensaas-sh/app/src/admin/dashboards/users/UsersTable.tsx
@@ -207,7 +207,7 @@
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>
@@ -254,7 +254,7 @@
<p className='text-sm text-foreground'>{user.subscriptionStatus}</p>
</div>
<div className='col-span-2 flex items-center'>
- <p className='text-sm text-meta-3'>{user.paymentProcessorUserId}</p>
+ <p className='text-sm text-meta-3'>{user.stripeId}</p>
- <p className='text-sm text-muted-foreground'>{user.paymentProcessorUserId}</p>
+ <p className='text-sm text-muted-foreground'>{user.subscriptionStatus}</p>
</div>
<div className='col-span-1 flex items-center'>
<div className='text-sm text-black dark:text-white'>
<div className='text-sm text-foreground'>

View File

@@ -1,19 +0,0 @@
--- template/app/src/auth/LoginPage.tsx
+++ opensaas-sh/app/src/auth/LoginPage.tsx
@@ -1,8 +1,15 @@
+import { Navigate } from 'react-router-dom';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
-import { LoginForm } from 'wasp/client/auth';
+import { LoginForm, useAuth } from 'wasp/client/auth';
import { AuthPageLayout } from './AuthPageLayout';
export default function Login() {
+ const { data: user } = useAuth();
+
+ if (user) {
+ return <Navigate to='/demo-app' />;
+ }
+
return (
<AuthPageLayout>
<LoginForm />

View File

@@ -0,0 +1,8 @@
--- template/app/src/auth/email-and-pass/emails.ts
+++ opensaas-sh/app/src/auth/email-and-pass/emails.ts
@@ -1,4 +1,4 @@
-import { type GetVerificationEmailContentFn, type GetPasswordResetEmailContentFn } from 'wasp/server/auth';
+import { type GetPasswordResetEmailContentFn, type GetVerificationEmailContentFn } from 'wasp/server/auth';
export const getVerificationEmailContent: GetVerificationEmailContentFn = ({ verificationLink }) => ({
subject: 'Verify your email',

View File

@@ -1,14 +1,14 @@
--- template/app/src/auth/userSignupFields.ts
+++ opensaas-sh/app/src/auth/userSignupFields.ts
@@ -1,8 +1,6 @@
import { z } from 'zod';
@@ -1,7 +1,5 @@
-import { z } from 'zod';
import { defineUserSignupFields } from 'wasp/auth/providers/types';
-const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
-
-const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
+import { z } from 'zod';
const emailDataSchema = z.object({
email: z.string(),
});
@@ -16,10 +14,6 @@
const emailData = emailDataSchema.parse(data);
return emailData.email;

View File

@@ -0,0 +1,84 @@
--- template/app/src/client/Main.css
+++ opensaas-sh/app/src/client/Main.css
@@ -44,6 +44,15 @@
.border-gradient-primary > * {
background: hsl(var(--background));
}
+
+ /* Radial gradient utilities */
+ .bg-radial-gradient {
+ background: radial-gradient(circle at 30% 30%, hsl(var(--card-accent)/0.5) 0%, hsl(var(--accent)/0.15) 100%);
+ }
+
+ .dark .bg-radial-gradient {
+ background: radial-gradient(circle at 30% 30%, hsl(var(--card-subtle)/0.8) 0%, hsl(var(--card)) 100%);
+ }
}
/* Here is an example of how to add a custom font.
@@ -51,6 +60,16 @@
* They are defined first here, then need to be referenced in the tailwind.config.js file
* under `theme.extend.fontFamily`, and then can be used as a tailwind class, e.g. className='font-satoshi'.
*/
+
+/* Satoshi Font Family */
+@font-face {
+ font-family: 'Satoshi';
+ src: url('/fonts/Satoshi-Light.woff2') format('woff2');
+ font-weight: 300;
+ font-style: normal;
+ font-display: swap;
+}
+
@font-face {
font-family: 'Satoshi';
src: url('/fonts/Satoshi-Regular.woff2') format('woff2');
@@ -59,6 +78,30 @@
font-display: swap;
}
+@font-face {
+ font-family: 'Satoshi';
+ src: url('/fonts/Satoshi-Medium.woff2') format('woff2');
+ font-weight: 500;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Satoshi';
+ src: url('/fonts/Satoshi-Bold.woff2') format('woff2');
+ font-weight: bold;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Satoshi';
+ src: url('/fonts/Satoshi-Black.woff2') format('woff2');
+ font-weight: 900;
+ font-style: normal;
+ font-display: swap;
+}
+
/* third-party libraries CSS */
.tableCheckbox:checked ~ div span {
@@ -177,4 +220,17 @@
body {
@apply bg-background text-foreground;
}
+
+ /* Global typography styles */
+ h1, h2, h3, h4, h5, h6 {
+ @apply font-satoshi font-black sm:text-6xl leading-tight;
+ }
+
+ p {
+ @apply font-mono text-base leading-relaxed;
+ }
+}
+
+.navbar-maxwidth-transition {
+ transition: max-width 300ms cubic-bezier(0.4,0,0.2,1);
}

View File

@@ -1,95 +1,168 @@
--- template/app/src/client/components/NavBar/NavBar.tsx
+++ opensaas-sh/app/src/client/components/NavBar/NavBar.tsx
@@ -32,7 +32,7 @@
!isLandingPage,
})}
>
- {isLandingPage && <Announcement />}
+ {/* {isLandingPage && <Announcement />} */}
<nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'>
<div className='flex items-center lg:flex-1'>
<WaspRouterLink
@@ -40,9 +40,7 @@
className='flex items-center -m-1.5 p-1.5 text-gray-900 duration-300 ease-in-out hover:text-yellow-500'
>
<NavLogo />
- {isLandingPage && (
- <span className='ml-2 text-sm font-semibold leading-6 dark:text-white'>Your SaaS</span>
- )}
+ {isLandingPage && <span className='ml-2 text-sm font-semibold leading-6 dark:text-white'>Open SaaS</span>}
</WaspRouterLink>
</div>
<div className='flex lg:hidden'>
@@ -57,13 +55,13 @@
</div>
<div className='hidden lg:flex lg:gap-x-12'>{renderNavigationItems(navigationItems)}</div>
<div className='hidden lg:flex lg:flex-1 gap-3 justify-end items-center'>
- <ul className='flex justify-center items-center gap-2 sm:gap-4'>
+ <ul className='ml-4 flex justify-center items-center gap-2 sm:gap-4'>
<DarkModeSwitcher />
</ul>
{isUserLoading ? null : !user ? (
<WaspRouterLink to={routes.LoginRoute.to} className='text-sm font-semibold leading-6 ml-3'>
- <div className='flex items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white'>
- Log in <BiLogIn size='1.1rem' className='ml-1 mt-[0.1rem]' />
+ <div className='flex justify-end items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white test-sm'>
+ Try the demo App <BiLogIn size='1.1rem' className='ml-1' />
</div>
</WaspRouterLink>
) : (
@@ -78,7 +76,7 @@
<Dialog.Panel className='fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white dark:text-white dark:bg-boxdark px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10'>
<div className='flex items-center justify-between'>
<WaspRouterLink to={routes.LandingPageRoute.to} className='-m-1.5 p-1.5'>
- <span className='sr-only'>Your SaaS</span>
+ <span className='sr-only'>Open SaaS</span>
<NavLogo />
</WaspRouterLink>
<button
@@ -97,7 +95,7 @@
{isUserLoading ? null : !user ? (
<WaspRouterLink to={routes.LoginRoute.to}>
<div className='flex justify-end items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white'>
- Log in <BiLogIn size='1.1rem' className='ml-1' />
+ Try the Demo App{' '} <BiLogIn size='1.1rem' className='ml-1' />
</div>
</WaspRouterLink>
) : (
@@ -140,30 +138,26 @@
});
}
@@ -3,6 +3,7 @@
import { Link as ReactRouterLink } from 'react-router-dom';
import { useAuth } from 'wasp/client/auth';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
+import { Button } from '../../../components/ui/button';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../../components/ui/sheet';
import DropdownUser from '../../../user/DropdownUser';
import { UserMenuItems } from '../../../user/UserMenuItems';
@@ -10,6 +11,7 @@
import { useIsLandingPage } from '../../hooks/useIsLandingPage';
import logo from '../../static/logo.webp';
import DarkModeSwitcher from '../DarkModeSwitcher';
+import RepoInfo from '../RepoInfo';
-const ContestURL = 'https://github.com/wasp-lang/wasp';
+const ContestURL =
+ 'https://docs.opensaas.sh/blog/';
export interface NavigationItem {
name: string;
@@ -39,7 +41,12 @@
return (
<>
{isLandingPage && <Announcement />}
- <header className={cn('sticky top-0 z-50 transition-all duration-300', isScrolled && 'top-4')}>
+ <header
+ className={cn(
+ 'sticky top-0 z-50 transition-all duration-300',
+ isScrolled && 'top-4 mx-4 xl:mx-30 lg:mx-10'
+ )}
+ >
<div
className={cn('transition-all duration-300', {
'mx-4 md:mx-20 pr-2 lg:pr-0 rounded-full shadow-lg bg-background/90 backdrop-blur-lg border border-border':
@@ -49,7 +56,7 @@
>
<nav
className={cn('flex items-center justify-between transition-all duration-300', {
- 'p-3 lg:px-6': isScrolled,
+ 'p-3 px-4 lg:p-4 lg:px-5': isScrolled,
'p-6 lg:px-8': !isScrolled,
})}
aria-label='Global'
@@ -67,7 +74,7 @@
'ml-2 text-xs': isScrolled,
})}
>
- Your SaaS
+ Open SaaS
</span>
)}
</WaspRouterLink>
@@ -99,7 +106,7 @@
<SheetHeader>
<SheetTitle className='flex items-center'>
<WaspRouterLink to={routes.LandingPageRoute.to}>
- <span className='sr-only'>Your SaaS</span>
+ <span className='sr-only'>Open SaaS</span>
<NavLogo isScrolled={false} />
</WaspRouterLink>
</SheetTitle>
@@ -112,9 +119,9 @@
<div className='py-6'>
{isUserLoading ? null : !user ? (
<WaspRouterLink to={routes.LoginRoute.to}>
- <div className='flex justify-end items-center duration-300 ease-in-out text-foreground hover:text-primary transition-colors'>
- Log in <LogIn size='1.1rem' className='ml-1' />
- </div>
+ <Button variant='outline'>
+ <span>Demo App</span> <LogIn className='ml-1' />
+ </Button>
</WaspRouterLink>
) : (
<div className='space-y-2'>
@@ -123,7 +130,14 @@
)}
</div>
<div className='py-6'>
- <DarkModeSwitcher />
+ <ul className='flex items-center justify-between gap-4'>
+ <li>
+ <DarkModeSwitcher />
+ </li>
+ <li>
+ <RepoInfo />
+ </li>
+ </ul>
</div>
</div>
</div>
@@ -133,7 +147,12 @@
<div className='hidden lg:flex lg:flex-1 gap-3 justify-end items-center'>
<ul className='flex justify-center items-center gap-2 sm:gap-4'>
- <DarkModeSwitcher />
+ <li>
+ <RepoInfo />
+ </li>
+ <li>
+ <DarkModeSwitcher />
+ </li>
</ul>
{isUserLoading ? null : !user ? (
<WaspRouterLink
@@ -143,10 +162,10 @@
'text-xs': isScrolled,
})}
>
- <div className='flex items-center duration-300 ease-in-out text-foreground hover:text-primary transition-colors'>
- Log in{' '}
+ <div className='flex items-center duration-300 gap-1 ease-in-out text-foreground hover:text-primary transition-colors'>
+ <span>Demo App</span>
<LogIn
- size={isScrolled ? '1rem' : '1.1rem'}
+ size='1rem'
className={cn('transition-all duration-300', {
'ml-1 mt-[0.1rem]': !isScrolled,
'ml-1': isScrolled,
@@ -246,39 +265,40 @@
'size-6': isScrolled,
})}
src={logo}
- alt='Your SaaS App'
+ alt='Open SaaS App'
/>
);
-const announcementUrl = 'https://github.com/wasp-lang/wasp';
+const announcementUrl = 'https://www.producthunt.com/products/open-saas'
function Announcement() {
+ const launchDate = new Date('2025-07-29T00:00:00-07:00'); // July 29, 2025 PST
+ const today = new Date();
+ const hasLaunched = today.toISOString().slice(0, 10) >= launchDate.toISOString().slice(0, 10);
+
return (
<div className='flex justify-center items-center gap-3 p-3 w-full bg-gradient-to-r from-[#d946ef] to-[#fc0] font-semibold text-white text-center z-49'>
- <p
- onClick={() => window.open(ContestURL, '_blank')}
- className='hidden lg:block cursor-pointer hover:opacity-90 hover:drop-shadow'
- >
- <div className='relative flex justify-center items-center gap-3 p-3 w-full bg-gradient-to-r from-accent to-secondary font-semibold text-primary-foreground text-center z-[51]'>
+ <div className='relative flex justify-center items-center gap-3 p-3 w-full bg-gradient-to-r from-accent to-secondary font-semibold text-primary-foreground text-center tracking-wider z-[51]'>
<a
href={announcementUrl}
target='_blank'
- rel='noopener noreferrer'
- className='hidden lg:block cursor-pointer hover:opacity-90 hover:drop-shadow transition-opacity'
+ className='hidden lg:block hover:opacity-90 hover:drop-shadow transition-opacity'
>
- Support Open-Source Software!
- </p>
+ <p onClick={() => window.open(ContestURL, '_blank')} className='hidden lg:block cursor-pointer hover:opacity-90 hover:drop-shadow'>🍪 THE MOST ANNOYING COOKIE BANNER EVER HACKATHON 🤬</p>
<div className='hidden lg:block self-stretch w-0.5 bg-white'></div>
<div
onClick={() => window.open(ContestURL, '_blank')}
className='hidden lg:block cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
+ 🚀 Open SaaS v2.0 {hasLaunched ? 'is here!' : 'launches tomorrow!'}
</a>
<div className='hidden lg:block self-stretch w-0.5 bg-primary-foreground/20'></div>
<a
href={announcementUrl}
target='_blank'
- rel='noopener noreferrer'
- className='hidden lg:block cursor-pointer rounded-full bg-background/20 px-2.5 py-1 text-xs hover:bg-background/30 transition-colors tracking-wider'
+ className='hidden lg:block opacity-95 cursor-pointer rounded-full bg-background/20 px-2.5 py-1 hover:bg-background/30 transition-colors tracking-wider'
>
- Star Our Repo on Github ⭐️ →
+ Enter here and win prizes!
</div>
<div
onClick={() => window.open(ContestURL, '_blank')}
className='lg:hidden cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
+ {hasLaunched ? 'Check out the Launch 🎉' : 'Get notified! 📆'}
</a>
<a
href={announcementUrl}
target='_blank'
- rel='noopener noreferrer'
className='lg:hidden cursor-pointer rounded-full bg-background/20 px-2.5 py-1 text-xs hover:bg-background/30 transition-colors'
>
- ⭐️ Star the Our Repo on Github and Support Open-Source! ⭐️
+ 🍪 The Most Annoying Cookie Banner Contest! 🤬 →
</div>
- ⭐️ Star the Our Repo and Support Open-Source! ⭐️
+ 🎉 The Open SaaS v2.0 Launch is Live! 🚀
</a>
</div>
);
-}
+}
\ No newline at end of file

View File

@@ -0,0 +1,43 @@
--- template/app/src/client/components/RepoInfo.tsx
+++ opensaas-sh/app/src/client/components/RepoInfo.tsx
@@ -0,0 +1,40 @@
+import { useEffect, useState } from 'react';
+import { FaGithub } from 'react-icons/fa';
+import { Button } from '../../components/ui/button';
+import { formatNumber } from '../../lib/utils';
+
+const RepoInfo = () => {
+ const [repoInfo, setRepoInfo] = useState<null | any>(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchRepoInfo = async () => {
+ try {
+ setIsLoading(true);
+ const response = await fetch('https://api.github.com/repos/wasp-lang/open-saas');
+ const data = await response.json();
+ setRepoInfo(data);
+ } catch (error) {
+ console.error('Error fetching repo info', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchRepoInfo();
+ }, []);
+
+ if (isLoading || !repoInfo) {
+ return null;
+ }
+
+ return (
+ <a href='https://github.com/wasp-lang/open-saas' target='_blank' rel='noopener noreferrer'>
+ <Button variant='ghost' className='rounded-full py-0 pl-2 pr-3 h-8'>
+ <FaGithub />
+ <span className='text-sm leading-none'>{formatNumber(repoInfo.stargazers_count)}</span>
+ </Button>
+ </a>
+ );
+};
+
+export default RepoInfo;

View File

@@ -1,13 +0,0 @@
--- template/app/src/client/components/cookie-consent/Config.ts
+++ opensaas-sh/app/src/client/components/cookie-consent/Config.ts
@@ -97,8 +97,8 @@
// showPreferencesBtn: 'Manage Individual preferences', // (OPTIONAL) Activates the preferences modal
// TODO: Add your own privacy policy and terms and conditions links below.
footer: `
- <a href="<your-url-here>" target="_blank">Privacy Policy</a>
- <a href="<your-url-here>" target="_blank">Terms and Conditions</a>
+ <a>Privacy Policy</a>
+ <a>Terms and Conditions</a>
`,
},
// The showPreferencesBtn activates this modal to manage individual preferences https://cookieconsent.orestbida.com/reference/configuration-reference.html#translation-preferencesmodal

View File

@@ -0,0 +1,32 @@
--- template/app/src/components/ui/button.tsx
+++ opensaas-sh/app/src/components/ui/button.tsx
@@ -5,22 +5,26 @@
import { cn } from '../../lib/utils';
const buttonVariants = cva(
- 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
- default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
+ default: 'bg-primary text-primary-foreground shadow-outer hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
- ghost: 'hover:bg-accent hover:text-accent-foreground',
+ ghost: 'hover:bg-muted hover:text-foreground text-muted-foreground',
link: 'text-primary underline-offset-4 hover:underline',
+ selected: 'border bg-muted text-muted-foreground',
+ outer: 'shadow-outer bg-card text-card-foreground',
+ inner: 'shadow-inner bg-secondary-muted text-secondary-muted-foreground',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
+ iconLg: 'h-12 w-12',
},
},
defaultVariants: {

View File

@@ -0,0 +1,15 @@
--- template/app/src/components/ui/card.tsx
+++ opensaas-sh/app/src/components/ui/card.tsx
@@ -9,7 +9,11 @@
default: 'bg-card text-card-foreground',
accent: 'bg-card-accent text-card-accent-foreground hover:scale-[1.02]',
faded: 'text-card-faded-foreground scale-95 opacity-50',
- bento: 'bg-card-subtle text-card-subtle-foreground hover:scale-[1.02] border-none shadow-none',
+ bento: 'bg-card-subtle text-card-subtle-foreground border-none shadow-none hover:shadow-none',
+ bentoHighlight:
+ 'bg-card-subtle text-card-subtle-foreground border-none shadow-outer dark:shadow-lg hover:shadow-outer',
+ outer: 'bg-card shadow-outer text-card-foreground hover:shadow-outer',
+ inner: 'bg-card shadow-inner text-card-foreground hover:shadow-inner',
},
},
});

View File

@@ -0,0 +1,9 @@
--- template/app/src/file-upload/fileUploading.ts
+++ opensaas-sh/app/src/file-upload/fileUploading.ts
@@ -1,5 +1,5 @@
-import { createFile } from 'wasp/client/operations';
import axios from 'axios';
+import { createFile } from 'wasp/client/operations';
import { ALLOWED_FILE_TYPES, MAX_FILE_SIZE_BYTES } from './validation';
export type FileWithValidType = Omit<File, 'type'> & { type: AllowedFileType };

View File

@@ -1,5 +1,23 @@
--- template/app/src/file-upload/operations.ts
+++ opensaas-sh/app/src/file-upload/operations.ts
@@ -1,14 +1,14 @@
-import * as z from 'zod';
-import { HttpError } from 'wasp/server';
import { type File } from 'wasp/entities';
+import { HttpError } from 'wasp/server';
import {
type CreateFile,
type GetAllFilesByUser,
type GetDownloadFileSignedURL,
} from 'wasp/server/operations';
+import * as z from 'zod';
-import { getUploadFileSignedURLFromS3, getDownloadFileSignedURLFromS3 } from './s3Utils';
import { ensureArgsSchemaOrThrowHttpError } from '../server/validation';
+import { getDownloadFileSignedURLFromS3, getUploadFileSignedURLFromS3 } from './s3Utils';
import { ALLOWED_FILE_TYPES } from './validation';
const createFileInputSchema = z.object({
@@ -37,6 +37,18 @@
userId: context.user.id,
});

View File

@@ -0,0 +1,15 @@
--- template/app/src/file-upload/s3Utils.ts
+++ opensaas-sh/app/src/file-upload/s3Utils.ts
@@ -1,8 +1,8 @@
-import * as path from 'path';
-import { randomUUID } from 'crypto';
-import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
-import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
+import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
+import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
+import { randomUUID } from 'crypto';
+import * as path from 'path';
import { MAX_FILE_SIZE_BYTES } from './validation';
const s3Client = new S3Client({

View File

@@ -0,0 +1,24 @@
--- template/app/src/landing-page/LandingPage.tsx
+++ opensaas-sh/app/src/landing-page/LandingPage.tsx
@@ -1,10 +1,10 @@
+import { Admin, AIReady, Auth, Payments } from './components/Examples';
import ExamplesCarousel from './components/ExamplesCarousel';
import FAQ from './components/FAQ';
import FeaturesGrid from './components/FeaturesGrid';
import Footer from './components/Footer';
import Hero from './components/Hero';
import Testimonials from './components/Testimonials';
-import AIReady from './ExampleHighlightedFeature';
import { examples, faqs, features, footerNavigation, testimonials } from './contentSections';
export default function LandingPage() {
@@ -13,6 +13,9 @@
<main className='isolate'>
<Hero />
<ExamplesCarousel examples={examples} />
+ <Auth />
+ <Payments />
+ <Admin />
<AIReady />
<FeaturesGrid features={features} />
<Testimonials testimonials={testimonials} />

View File

@@ -1,78 +0,0 @@
--- template/app/src/landing-page/components/Clients.tsx
+++ opensaas-sh/app/src/landing-page/components/Clients.tsx
@@ -1,23 +1,64 @@
+import logo from '../../client/static/logo.webp';
import AstroLogo from "../logos/AstroLogo";
-import OpenAILogo from "../logos/OpenAILogo";
import PrismaLogo from "../logos/PrismaLogo";
-import SalesforceLogo from "../logos/SalesforceLogo";
+import OpenAILogo from "../logos/OpenAILogo";
export default function Clients() {
return (
<div className='mt-12 mx-auto max-w-7xl px-6 lg:px-8 flex flex-col items-between gap-y-6'>
- <h2 className='mb-6 text-center font-semibold tracking-wide text-gray-500 dark:text-white'>
- Built with / Used by:
+ <h2 className='mb-6 text-center font-semibold tracking-wide text-gray-500'>
+ Built and Ships with
</h2>
<div className='mx-auto grid max-w-lg grid-cols-2 items-center gap-x-8 gap-y-12 sm:max-w-xl md:grid-cols-4 sm:gap-x-10 sm:gap-y-14 lg:mx-0 lg:max-w-none'>
- {
- [<SalesforceLogo />, <PrismaLogo />, <AstroLogo />, <OpenAILogo />].map((logo, index) => (
- <div key={index} className='flex justify-center col-span-1 max-h-12 w-full object-contain dark:opacity-80'>
- {logo}
- </div>
- ))
- }
+
+ <img
+ className='col-span-1 max-h-12 w-full object-contain grayscale opacity-100'
+ src='https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/512px-React-icon.svg.png'
+ alt='React'
+ height={48}
+ />
+
+ <img
+ className='col-span-1 max-h-12 w-full object-contain grayscale opacity-60 dark:filter dark:brightness-0 dark:invert'
+ src='https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Node.js_logo.svg/590px-Node.js_logo.svg.png'
+ alt='NodeJS'
+ height={48}
+ />
+
+ <img
+ className='col-span-1 max-h-12 w-full object-contain grayscale opacity-80'
+ src={logo}
+ alt='Wasp'
+ height={48}
+ />
+
+ <div className='flex justify-center col-span-1 max-h-12 w-full object-contain grayscale opacity-80'>
+ <PrismaLogo />
+ </div>
+
+ <img
+ className='col-span-1 max-h-12 w-full object-contain grayscale '
+ src='https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Tailwind_CSS_Logo.svg/512px-Tailwind_CSS_Logo.svg.png'
+ alt='Tailwind CSS'
+ height={48}
+ />
+
+ <img
+ className='col-span-1 max-h-12 w-full object-contain grayscale opacity-80 dark:opacity-60 dark:filter dark:brightness-0 dark:invert'
+ src='https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Stripe_Logo%2C_revised_2016.svg/512px-Stripe_Logo%2C_revised_2016.svg.png'
+ alt='Stripe'
+ height={48}
+ />
+
+ <div className='flex justify-center col-span-1 w-full max-h-12 object-contain grayscale opacity-75'>
+ <AstroLogo />
+ </div>
+
+ <div className='flex justify-center col-span-1 w-full max-h-12 object-contain grayscale opacity-80'>
+ <OpenAILogo />
+ </div>
+
</div>
</div>
)

View File

@@ -0,0 +1,26 @@
--- template/app/src/landing-page/components/Examples/AIReady.tsx
+++ opensaas-sh/app/src/landing-page/components/Examples/AIReady.tsx
@@ -0,0 +1,23 @@
+import aiReadyDark from '../../../client/static/assets/aiready-dark.webp';
+import aiReady from '../../../client/static/assets/aiready.webp';
+import HighlightedFeature from '../HighlightedFeature';
+
+export default function AIReady() {
+ return (
+ <HighlightedFeature
+ name='AI Ready'
+ description='Built-in OpenAI integration with LLM-optimized docs and AI-friendly architecture for seamless AI-assisted development.'
+ highlightedComponent={<AIReadyExample />}
+ direction='row-reverse'
+ />
+ );
+}
+
+const AIReadyExample = () => {
+ return (
+ <div className='w-full'>
+ <img src={aiReady} alt='AI Ready' className='dark:hidden' />
+ <img src={aiReadyDark} alt='AI Ready' className='hidden dark:block' />
+ </div>
+ );
+};

View File

@@ -0,0 +1,23 @@
--- template/app/src/landing-page/components/Examples/Admin.tsx
+++ opensaas-sh/app/src/landing-page/components/Examples/Admin.tsx
@@ -0,0 +1,20 @@
+import admin from '../../../client/static/assets/admin.webp';
+import HighlightedFeature from '../HighlightedFeature';
+
+export default function Admin() {
+ return (
+ <HighlightedFeature
+ name='Admin Dashboard'
+ description='Graphs! Tables! Analytics w/ Plausible or Google! All in one place. Ooooooooh.'
+ highlightedComponent={<AdminExample />}
+ />
+ );
+}
+
+const AdminExample = () => {
+ return (
+ <div className='w-full'>
+ <img src={admin} alt='Admin' />
+ </div>
+ );
+};

View File

@@ -0,0 +1,133 @@
--- template/app/src/landing-page/components/Examples/Auth.tsx
+++ opensaas-sh/app/src/landing-page/components/Examples/Auth.tsx
@@ -0,0 +1,130 @@
+import { useState } from 'react';
+import { FaDiscord, FaGithub, FaGoogle, FaSlack } from 'react-icons/fa';
+import { Button } from '../../../components/ui/button';
+import { Card, CardContent, CardHeader } from '../../../components/ui/card';
+import { Input } from '../../../components/ui/input';
+import { DocsUrl } from '../../../shared/common';
+import HighlightedFeature from '../HighlightedFeature';
+
+const SupportedAuthProviders = ['google', 'github', 'discord', 'slack'] as const;
+type AuthProvider = (typeof SupportedAuthProviders)[number];
+
+const providers: { name: AuthProvider; icon: React.ComponentType<{ className?: string }> }[] = [
+ {
+ name: 'google',
+ icon: FaGoogle,
+ },
+ {
+ name: 'github',
+ icon: FaGithub,
+ },
+ {
+ name: 'discord',
+ icon: FaDiscord,
+ },
+ {
+ name: 'slack',
+ icon: FaSlack,
+ },
+];
+
+export default function Auth() {
+ const [selectedProviders, setSelectedProviders] = useState<AuthProvider[]>(['google', 'github']);
+
+ const toggleProvider = (provider: AuthProvider) => {
+ setSelectedProviders((prev) =>
+ prev.includes(provider) ? prev.filter((p) => p !== provider) : [...prev, provider]
+ );
+ };
+
+ return (
+ <HighlightedFeature
+ name='DIY Auth
+Done for you'
+ description={<AuthPlayground toggleProvider={toggleProvider} selectedProviders={selectedProviders} />}
+ highlightedComponent={<AuthExample selectedProviders={selectedProviders} />}
+ tilt='left'
+ className='h-[400px]'
+ />
+ );
+}
+
+const AuthPlayground = ({
+ toggleProvider,
+ selectedProviders,
+}: {
+ toggleProvider: (provider: AuthProvider) => void;
+ selectedProviders: AuthProvider[];
+}) => {
+ return (
+ <div className='flex flex-col gap-2'>
+ <p className='text-muted-foreground'>Pre-configured full-stack Auth that you own.</p>
+ <div className='flex flex-row gap-2'>
+ {providers.map((provider) => {
+ const IconComponent = provider.icon;
+ return (
+ <Button
+ key={provider.name}
+ onClick={() => toggleProvider(provider.name)}
+ size='iconLg'
+ variant={selectedProviders.includes(provider.name) ? 'inner' : 'outer'}
+ >
+ <IconComponent />
+ </Button>
+ );
+ })}
+ </div>
+ </div>
+ );
+};
+
+const Divider = () => {
+ return (
+ <div className='flex flex-row gap-2 items-center'>
+ <div className='flex-1 h-px bg-muted' />
+ <p className=' text-xs'>Or continue with</p>
+ <div className='flex-1 h-px bg-muted' />
+ </div>
+ );
+};
+
+const AuthExample = ({ selectedProviders }: { selectedProviders: AuthProvider[] }) => {
+ return (
+ <Card className='py-10 w-full max-w-md transition-all duration-300 ease-in-out' variant='outer'>
+ <CardHeader>
+ <p className='text-2xl font-bold'>Log in to your account</p>
+ </CardHeader>
+ <CardContent>
+ <div className='flex flex-col gap-4'>
+ <div className='flex flex-col'>
+ {selectedProviders.length > 0 ? <p className='text-xs mb-2'>Log in with</p> : null}
+ <div className='flex flex-row gap-2'>
+ {selectedProviders.map((provider) => {
+ const providerData = providers.find((p) => p.name === provider);
+ const IconComponent = providerData?.icon;
+ return (
+ <a
+ href={DocsUrl + '/guides/authentication/'}
+ key={provider}
+ className='w-full mb-2'
+ target='_blank'
+ >
+ <Button variant='outline' className='w-full'>
+ {IconComponent && <IconComponent className='w-4 h-4' />}
+ </Button>
+ </a>
+ );
+ })}
+ </div>
+ </div>
+ {selectedProviders.length > 0 ? <Divider /> : null}
+ <div className='flex flex-col gap-2'>
+ <Input type='email' placeholder='Email' />
+ <Input type='password' placeholder='Password' />
+ <Button className='mt-2'>Log in</Button>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ );
+};

View File

@@ -0,0 +1,24 @@
--- template/app/src/landing-page/components/Examples/Payments.tsx
+++ opensaas-sh/app/src/landing-page/components/Examples/Payments.tsx
@@ -0,0 +1,21 @@
+import payments from '../../../client/static/assets/payments.webp';
+import HighlightedFeature from '../HighlightedFeature';
+
+export default function Payments() {
+ return (
+ <HighlightedFeature
+ name='Stripe / Lemon Squeezy Integration'
+ description="No SaaS is complete without payments. We've pre-configured checkout and webhooks. Just choose a provider and start cashing out."
+ highlightedComponent={<PaymentsExample />}
+ direction='row-reverse'
+ />
+ );
+}
+
+const PaymentsExample = () => {
+ return (
+ <div className='w-full max-w-lg'>
+ <img src={payments} alt='Payments' />
+ </div>
+ );
+};

View File

@@ -0,0 +1,7 @@
--- template/app/src/landing-page/components/Examples/index.ts
+++ opensaas-sh/app/src/landing-page/components/Examples/index.ts
@@ -0,0 +1,4 @@
+export { default as Admin } from './Admin';
+export { default as AIReady } from './AIReady';
+export { default as Auth } from './Auth';
+export { default as Payments } from './Payments';

View File

@@ -0,0 +1,11 @@
--- template/app/src/landing-page/components/ExamplesCarousel.tsx
+++ opensaas-sh/app/src/landing-page/components/ExamplesCarousel.tsx
@@ -100,7 +100,7 @@
ref={containerRef}
className='relative w-screen left-1/2 -translate-x-1/2 flex flex-col items-center my-16'
>
- <h2 className='mb-6 text-center font-semibold tracking-wide text-muted-foreground'>Used by:</h2>
+ <h2 className='mb-6 text-lg text-center font-semibold tracking-wide text-muted-foreground'>Used by:</h2>
<div className='w-full max-w-full overflow-hidden'>
<div
className='flex overflow-x-auto no-scrollbar scroll-smooth pb-10 pt-4 gap-4 px-4 snap-x snap-mandatory'

View File

@@ -1,28 +0,0 @@
--- template/app/src/landing-page/components/FAQ.tsx
+++ opensaas-sh/app/src/landing-page/components/FAQ.tsx
@@ -7,20 +7,20 @@
export default function FAQ({ faqs }: { faqs: FAQ[] }) {
return (
- <div className='mt-32 mx-auto max-w-2xl divide-y divide-gray-900/10 dark:divide-gray-200/10 px-6 pb-8 sm:pb-24 sm:pt-12 lg:max-w-7xl lg:px-8 lg:py-32'>
+ <div className='mt-32 mx-auto max-w-2xl divide-y divide-gray-900/10 px-6 pb-8 sm:pb-24 sm:pt-12 lg:max-w-7xl lg:px-8 lg:py-32'>
<h2 className='text-2xl font-bold leading-10 tracking-tight text-gray-900 dark:text-white'>
Frequently asked questions
</h2>
- <dl className='mt-10 space-y-8 divide-y divide-gray-900/10'>
+ <dl className='mt-10 space-y-8 divide-y divide-gray-900/10 dark:divide-gray-100/10'>
{faqs.map((faq) => (
<div key={faq.id} className='pt-8 lg:grid lg:grid-cols-12 lg:gap-8'>
<dt className='text-base font-semibold leading-7 text-gray-900 lg:col-span-5 dark:text-white'>
{faq.question}
</dt>
- <dd className='flex items-center justify-start gap-2 mt-4 lg:col-span-7 lg:mt-0'>
- <p className='text-base leading-7 text-gray-600 dark:text-white'>{faq.answer}</p>
+ <dd className='mt-4 lg:col-span-7 lg:mt-0'>
+ <p className='text-base leading-7 text-gray-600 dark:text-gray-400'>{faq.answer}</p>
{faq.href && (
- <a href={faq.href} className='text-base leading-7 text-yellow-500 hover:text-yellow-600'>
+ <a href={faq.href} className='mt-4 text-base leading-7 text-yellow-500 hover:text-yellow-600'>
Learn more →
</a>
)}

View File

@@ -1,43 +0,0 @@
--- template/app/src/landing-page/components/Features.tsx
+++ opensaas-sh/app/src/landing-page/components/Features.tsx
@@ -10,25 +10,27 @@
<div id='features' className='mx-auto mt-48 max-w-7xl px-6 lg:px-8'>
<div className='mx-auto max-w-2xl text-center'>
<p className='mt-2 text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl dark:text-white'>
- The <span className='text-yellow-500'>Best</span> Features
+ <span className='text-yellow-500'>100%</span> Open-Source
</p>
- <p className='mt-6 text-lg leading-8 text-gray-600 dark:text-white'>
- Don't work harder.
- <br /> Work smarter.
+ <p className='mt-6 text-lg leading-8 text-gray-600 dark:text-gray-400'>
+ No vendor lock-in.
+ <br /> Deploy anywhere.
</p>
</div>
<div className='mx-auto mt-16 max-w-2xl sm:mt-20 lg:mt-24 lg:max-w-4xl'>
<dl className='grid max-w-xl grid-cols-1 gap-x-8 gap-y-10 lg:max-w-none lg:grid-cols-2 lg:gap-y-16'>
{features.map((feature) => (
- <div key={feature.name} className='relative pl-16'>
- <dt className='text-base font-semibold leading-7 text-gray-900 dark:text-white'>
- <div className='absolute left-0 top-0 flex h-10 w-10 items-center justify-center border border-yellow-400 bg-yellow-100/50 dark:bg-boxdark rounded-lg'>
- <div className='text-2xl'>{feature.icon}</div>
- </div>
- {feature.name}
- </dt>
- <dd className='mt-2 text-base leading-7 text-gray-600 dark:text-white'>{feature.description}</dd>
- </div>
+ <a href={feature.href} className='group'>
+ <div key={feature.name} className='relative pl-16'>
+ <dt className='text-base font-semibold leading-7 text-gray-900 dark:text-white group-hover:underline'>
+ <div className='absolute left-0 top-0 flex h-10 w-10 items-center justify-center border border-yellow-400 bg-yellow-100/50 dark:bg-boxdark rounded-lg group-hover:border-yellow-500'>
+ <div className='text-2xl group-hover:opacity-80 '>{feature.icon}</div>
+ </div>
+ {feature.name}
+ </dt>
+ <dd className='mt-2 text-base leading-7 text-gray-600 dark:text-gray-400'>{feature.description}</dd>
+ </div>
+ </a>
))}
</dl>
</div>

View File

@@ -0,0 +1,153 @@
--- template/app/src/landing-page/components/FeaturesGrid.tsx
+++ opensaas-sh/app/src/landing-page/components/FeaturesGrid.tsx
@@ -4,13 +4,15 @@
import { Feature } from './Features';
import SectionTitle from './SectionTitle';
-export interface GridFeature extends Omit<Feature, 'icon'> {
+export interface GridFeature extends Omit<Feature, 'icon' | 'name'> {
+ name?: string;
icon?: React.ReactNode;
emoji?: string;
direction?: 'col' | 'row' | 'col-reverse' | 'row-reverse';
align?: 'center' | 'left';
size: 'small' | 'medium' | 'large';
fullWidthIcon?: boolean;
+ highlight?: boolean;
}
interface FeaturesGridProps {
@@ -45,7 +47,8 @@
direction = 'col',
align = 'center',
size = 'medium',
- fullWidthIcon = true,
+ fullWidthIcon = false,
+ highlight = false,
}: GridFeature) {
const gridFeatureSizeToClasses: Record<GridFeature['size'], string> = {
small: 'col-span-1',
@@ -60,49 +63,103 @@
'col-reverse': 'flex-col-reverse',
};
+ const mobileOrderClass = highlight ? 'order-last md:order-none' : '';
+
const gridFeatureCard = (
<Card
className={cn(
'h-full min-h-[140px] transition-all duration-300 hover:shadow-lg cursor-pointer',
- gridFeatureSizeToClasses[size]
+ gridFeatureSizeToClasses[size],
+ mobileOrderClass,
+ highlight && 'bg-radial-gradient'
)}
- variant='bento'
+ variant={highlight ? 'bentoHighlight' : 'bento'}
>
- <CardContent className='p-4 h-full flex flex-col justify-center items-center'>
+ <CardContent
+ className={cn(
+ 'py-8 px-4 h-full flex flex-col justify-center items-center',
+ fullWidthIcon && direction === 'col-reverse'
+ ? 'p-0 pt-8 pb-0 justify-end'
+ : fullWidthIcon && 'p-0 pb-8 pt-0 justify-start'
+ )}
+ >
{fullWidthIcon && (icon || emoji) ? (
- <div className='w-full flex justify-center items-center mb-3'>
- {icon ? icon : emoji ? <span className='text-4xl'>{emoji}</span> : null}
+ <div className='flex flex-col w-full'>
+ <div
+ className={cn(
+ 'w-full flex items-center justify-center',
+ direction === 'col-reverse' ? 'order-2 mt-6' : 'order-1'
+ )}
+ >
+ {icon ? (
+ <div className='w-full flex justify-center'>{icon}</div>
+ ) : emoji ? (
+ <span className='text-4xl'>{emoji}</span>
+ ) : null}
+ </div>
+ {fullWidthIcon && (icon || emoji) && (
+ <CardTitle
+ className={cn('text-center text-2xl', direction === 'col-reverse' ? 'order-1' : 'order-2')}
+ >
+ {name}
+ </CardTitle>
+ )}
+ <CardDescription
+ className={cn(
+ 'text-xs leading-relaxed px-8',
+ 'text-center',
+ direction === 'col-reverse' ? 'order-1' : 'order-2'
+ )}
+ >
+ {description}
+ </CardDescription>
</div>
) : (
<div
className={cn(
- 'flex items-center gap-3',
+ 'flex items-center',
+ (icon || emoji) && 'gap-3',
directionToClass[direction],
align === 'center' ? 'justify-center items-center' : 'justify-start'
)}
>
- <div className='flex h-10 w-10 items-center justify-center rounded-lg'>
- {icon ? icon : emoji ? <span className='text-2xl'>{emoji}</span> : null}
- </div>
- <CardTitle className={cn(align === 'center' ? 'text-center' : 'text-left')}>{name}</CardTitle>
+ {(icon || emoji) && (
+ <div className='flex h-10 w-10 items-center justify-center rounded-lg'>
+ {icon ? icon : <span className='text-2xl'>{emoji}</span>}
+ </div>
+ )}
+ <CardTitle
+ className={cn(
+ highlight ? 'text-4xl font-bold' : 'text-lg',
+ align === 'center' ? 'text-center' : 'text-left'
+ )}
+ >
+ {name}
+ </CardTitle>
</div>
)}
- {fullWidthIcon && (icon || emoji) && <CardTitle className='text-center mb-2'>{name}</CardTitle>}
- <CardDescription
- className={cn(
- 'text-xs leading-relaxed',
- fullWidthIcon || direction === 'col' || align === 'center' ? 'text-center' : 'text-left'
- )}
- >
- {description}
- </CardDescription>
+ {!fullWidthIcon && (
+ <CardDescription
+ className={cn(
+ 'text-xs leading-relaxed px-8',
+ direction === 'col' || align === 'center' ? 'text-center' : 'text-left'
+ )}
+ >
+ {description}
+ </CardDescription>
+ )}
</CardContent>
</Card>
);
if (href) {
return (
- <a href={href} target='_blank' rel='noopener noreferrer' className={gridFeatureSizeToClasses[size]}>
+ <a
+ href={href}
+ target='_blank'
+ rel='noopener noreferrer'
+ className={cn(gridFeatureSizeToClasses[size], mobileOrderClass)}
+ >
{gridFeatureCard}
</a>
);

View File

@@ -1,11 +0,0 @@
--- template/app/src/landing-page/components/Footer.tsx
+++ opensaas-sh/app/src/landing-page/components/Footer.tsx
@@ -13,7 +13,7 @@
<div className='mx-auto mt-6 max-w-7xl px-6 lg:px-8 dark:bg-boxdark-2'>
<footer
aria-labelledby='footer-heading'
- className='relative border-t border-gray-900/10 dark:border-gray-200/10 py-24 sm:mt-32'
+ className='relative border-t border-gray-900/10 dark:border-gray-100/10 py-24 sm:mt-32'
>
<h2 id='footer-heading' className='sr-only'>
Footer

View File

@@ -1,93 +0,0 @@
--- template/app/src/landing-page/components/Hero.tsx
+++ opensaas-sh/app/src/landing-page/components/Hero.tsx
@@ -1,7 +1,25 @@
-import openSaasBannerWebp from '../../client/static/open-saas-banner.webp';
-import { DocsUrl } from '../../shared/common';
+import { useState, useEffect } from 'react';
+import { AiFillGithub } from 'react-icons/ai';
+import { DocsUrl, GithubUrl } from '../../shared/common';
export default function Hero() {
+ const [repoInfo, setRepoInfo] = useState<null | any>(null);
+
+ useEffect(() => {
+ const fetchRepoInfo = async () => {
+ try {
+ const response = await fetch(
+ 'https://api.github.com/repos/wasp-lang/open-saas'
+ );
+ const data = await response.json();
+ setRepoInfo(data);
+ } catch (error) {
+ console.error('Error fetching repo info', error);
+ }
+ };
+ fetchRepoInfo();
+ }, []);
+
return (
<div className='relative pt-14 w-full'>
<TopGradient />
@@ -9,31 +27,47 @@
<div className='py-24 sm:py-32'>
<div className='mx-auto max-w-8xl px-6 lg:px-8'>
<div className='lg:mb-18 mx-auto max-w-3xl text-center'>
- <h1 className='text-4xl font-bold text-gray-900 sm:text-6xl dark:text-white'>
- Some <span className='italic'>cool</span> words about your product
+ <h1 className='text-4xl font-bold text-gray-900 sm:text-6xl dark:text-white tracking-tight'>
+ The <span className='italic'>free</span> SaaS template with
+ superpowers
</h1>
- <p className='mt-6 mx-auto max-w-2xl text-lg leading-8 text-gray-600 dark:text-white'>
- With some more exciting words about your product!
+ <p className='mt-6 mx-auto max-w-2xl text-lg leading-8 text-gray-600 dark:text-gray-400'>
+ An open-source, feature-rich, full-stack React + NodeJS
+ template that manages features for you.
</p>
<div className='mt-10 flex items-center justify-center gap-x-6'>
<a
href={DocsUrl}
- className='rounded-md px-3.5 py-2.5 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-200 hover:ring-2 hover:ring-yellow-300 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:text-white'
+ className='rounded-md px-6 py-4 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-200 hover:ring-2 hover:ring-yellow-300 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:text-white'
>
Get Started <span aria-hidden='true'>→</span>
</a>
+ <a
+ href={GithubUrl}
+ className='group relative flex items-center justify-center rounded-md bg-gray-100 px-6 py-4 text-sm font-semibold shadow-sm ring-1 ring-inset ring-gray-200 dark:bg-gray-700 hover:ring-2 hover:ring-yellow-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
+ >
+ {/* <AiFillGithub size='1.25rem' className='mr-2' /> */}
+ View the Repo
+ {repoInfo!! && (
+ <>
+ <span className='absolute -top-3 -right-7 inline-flex items-center gap-x-1 rounded-full ring-1 group-hover:ring-2 ring-inset ring-yellow-300 bg-yellow-100 px-2 py-1 text-sm font-medium text-yellow-800'>
+ <AiFillGithub size='1rem' />
+ {repoInfo.stargazers_count}
+ </span>
+ </>
+ )}
+ </a>
</div>
</div>
- <div className='mt-14 flow-root sm:mt-14'>
- <div className='-m-2 flex justify-center rounded-xl lg:-m-4 lg:rounded-2xl lg:p-4'>
- <img
- src={openSaasBannerWebp}
- alt='App screenshot'
- width={1000}
- height={530}
- loading='lazy'
- className='rounded-md shadow-2xl ring-1 ring-gray-900/10'
- />
+ <div className='mt-14 flow-root sm:mt-14 '>
+ <div className='-m-2 mx-auto rounded-xl lg:-m-4 lg:rounded-2xl lg:p-4'>
+ <iframe
+ className=' mx-auto w-full md:w-[85%] aspect-[4/3] shadow-2xl'
+ src='https://cards.producthunt.com/cards/posts/436467?v=1'
+ // width={850}
+ // height={689}
+ allowFullScreen
+ ></iframe>
</div>
</div>
</div>

View File

@@ -0,0 +1,85 @@
--- template/app/src/landing-page/components/Hero/Hero.tsx
+++ opensaas-sh/app/src/landing-page/components/Hero/Hero.tsx
@@ -0,0 +1,82 @@
+import { ArrowRight } from 'lucide-react';
+import { Link as ReactRouterLink } from 'react-router-dom';
+import { useAuth } from 'wasp/client/auth';
+import { Link as WaspRouterLink, routes } from 'wasp/client/router';
+import { Button } from '../../../components/ui/button';
+import { DocsUrl } from '../../../shared/common';
+import Orbit from './Orbit';
+
+export default function Hero() {
+ const { data: user } = useAuth();
+
+ return (
+ <div className='relative pt-32 w-full'>
+ <TopGradient />
+ <BottomGradient />
+ <div className='flex flex-col lg:flex-row max-w-7xl mx-auto'>
+ <div className='py-24 sm:py-32'>
+ <div className='max-w-8xl px-6 lg:px-8'>
+ <div className='lg:mb-18 mx-auto max-w-3xl text-center md:text-left'>
+ <h1 className='text-5xl font-extrabold text-foreground md:text-6xl'>
+ The <span className='italic'>free</span> SaaS template with{' '}
+ <span className='text-gradient-primary font-black'>superpowers</span>
+ </h1>
+ <p className='mt-6 max-w-2xl text-lg leading-8 text-muted-foreground font-mono'>
+ An open-source, feature-rich, full-stack React + NodeJS template that manages features for
+ you.
+ </p>
+ <div className='mt-10 flex items-center justify-center md:justify-start gap-x-6'>
+ <Button size='lg' variant='outline' asChild>
+ <WaspRouterLink to={user ? routes.DemoAppRoute.to : routes.LoginRoute.to}>
+ Try the Demo App
+ </WaspRouterLink>
+ </Button>
+ <Button size='lg' variant='default' asChild>
+ <ReactRouterLink to={DocsUrl}>
+ Get Started
+ <ArrowRight />
+ </ReactRouterLink>
+ </Button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className='hidden lg:block '>
+ <Orbit />
+ </div>
+ </div>
+ </div>
+ );
+}
+
+function TopGradient() {
+ return (
+ <div
+ className='absolute top-0 right-0 -z-10 transform-gpu overflow-hidden w-full blur-3xl sm:top-0'
+ aria-hidden='true'
+ >
+ <div
+ className='aspect-[1020/880] w-[70rem] flex-none sm:right-1/4 sm:translate-x-1/2 dark:hidden bg-gradient-to-tr from-amber-400 to-purple-300 opacity-10'
+ style={{
+ clipPath: 'polygon(80% 20%, 90% 55%, 50% 100%, 70% 30%, 20% 50%, 50% 0)',
+ }}
+ />
+ </div>
+ );
+}
+
+function BottomGradient() {
+ return (
+ <div
+ className='absolute inset-x-0 top-[calc(100%-40rem)] sm:top-[calc(100%-65rem)] -z-10 transform-gpu overflow-hidden blur-3xl'
+ aria-hidden='true'
+ >
+ <div
+ className='relative aspect-[1020/880] sm:-left-3/4 sm:translate-x-1/4 dark:hidden bg-gradient-to-br from-amber-400 to-purple-300 opacity-10 w-[90rem]'
+ style={{
+ clipPath: 'ellipse(80% 30% at 80% 50%)',
+ }}
+ />
+ </div>
+ );
+}

View File

@@ -0,0 +1,220 @@
--- template/app/src/landing-page/components/Hero/Orbit.tsx
+++ opensaas-sh/app/src/landing-page/components/Hero/Orbit.tsx
@@ -0,0 +1,217 @@
+import { useEffect, useState } from 'react';
+import logo from '../../../client/static/logo.webp';
+import nodeLogoDark from '../../../client/static/logos/nodejs-dark.webp';
+import nodeLogo from '../../../client/static/logos/nodejs-light.webp';
+import stripeLogoDark from '../../../client/static/logos/stripe-dark.webp';
+import stripeLogo from '../../../client/static/logos/stripe-light.webp';
+import tailwindLogoDark from '../../../client/static/logos/tailwind-dark.webp';
+import tailwindLogo from '../../../client/static/logos/tailwind-light.webp';
+import { cn } from '../../../lib/utils';
+import AstroLogo from '../../logos/AstroLogo';
+import OpenAILogo from '../../logos/OpenAILogo';
+import PrismaLogo from '../../logos/PrismaLogo';
+import ReactLogo from '../../logos/ReactLogo';
+import ShadCNLogo from '../../logos/ShadCNLogo';
+
+interface LogoConfig {
+ id: string;
+ component: React.ComponentType;
+ circleIndex: number;
+ position: number;
+ size?: number;
+}
+
+const ImageLogo = ({
+ src,
+ alt,
+ className,
+ dark,
+}: {
+ src: string;
+ alt: string;
+ className?: string;
+ dark?: boolean;
+}) => {
+ return (
+ <img
+ src={src}
+ alt={alt}
+ className={cn('w-8 h-8', dark ? 'dark:block hidden' : 'dark:hidden', className)}
+ />
+ );
+};
+
+const logoConfigs: LogoConfig[] = [
+ {
+ id: 'wasp',
+ component: () => <ImageLogo src={logo} alt='Wasp Logo' className='w-8 h-8 dark:block' dark={false} />,
+ circleIndex: 1,
+ position: 0,
+ },
+ { id: 'openai', component: OpenAILogo, circleIndex: 1, position: 120, size: 24 },
+ { id: 'astro', component: AstroLogo, circleIndex: 1, position: 270, size: 24 },
+ { id: 'prisma', component: PrismaLogo, circleIndex: 2, position: 90, size: 24 },
+ { id: 'shadcn', component: ShadCNLogo, circleIndex: 2, position: 210, size: 24 },
+ {
+ id: 'tailwind-light',
+ component: () => (
+ <ImageLogo src={tailwindLogo} alt='Tailwind CSS Logo' className='w-8 h-8 dark:hidden' dark={false} />
+ ),
+ circleIndex: 2,
+ position: 330,
+ },
+ {
+ id: 'tailwind-dark',
+ component: () => <ImageLogo src={tailwindLogoDark} alt='Tailwind CSS Logo' dark={true} />,
+ circleIndex: 2,
+ position: 330,
+ },
+ {
+ id: 'stripe-light',
+ component: () => <ImageLogo src={stripeLogo} alt='Stripe Logo' dark={false} />,
+ circleIndex: 3,
+ position: 180,
+ },
+ {
+ id: 'stripe-dark',
+ component: () => <ImageLogo src={stripeLogoDark} alt='Stripe Logo' dark={true} />,
+ circleIndex: 3,
+ position: 180,
+ },
+ { id: 'react', component: ReactLogo, circleIndex: 3, position: 300, size: 32 },
+ {
+ id: 'node-light',
+ component: () => <ImageLogo src={nodeLogo} alt='Node.js Logo' dark={false} />,
+ circleIndex: 3,
+ position: 50,
+ },
+ {
+ id: 'node-dark',
+ component: () => <ImageLogo src={nodeLogoDark} alt='Node.js Logo' dark={true} />,
+ circleIndex: 3,
+ position: 50,
+ },
+];
+
+const circles = [
+ { radius: 120, rotationSpeed: 0.5, circleRotationSpeed: 0.2 }, // Innermost circle
+ { radius: 180, rotationSpeed: 0.3, circleRotationSpeed: 0.15 }, // Second circle
+ { radius: 240, rotationSpeed: 0.2, circleRotationSpeed: 0.1 }, // Third circle
+ { radius: 300, rotationSpeed: 0.1, circleRotationSpeed: 0.05 }, // Outermost circle
+];
+
+const gradients = [];
+
+export default function Orbit() {
+ const [rotation, setRotation] = useState(0);
+
+ useEffect(() => {
+ const animate = () => {
+ setRotation((prev) => prev + 0.5);
+ };
+
+ const interval = setInterval(animate, 50);
+ return () => clearInterval(interval);
+ }, []);
+
+ const getLogoPosition = (circleIndex: number, position: number, rotation: number) => {
+ const circle = circles[circleIndex];
+ const totalRotation = rotation * circle.rotationSpeed + position;
+ const radians = (totalRotation * Math.PI) / 180;
+
+ return {
+ x: Math.cos(radians) * circle.radius,
+ y: Math.sin(radians) * circle.radius,
+ };
+ };
+
+ return (
+ <div className='relative w-[500px] h-[500px] flex items-center justify-center'>
+ <div className='absolute flex items-center gap-2 flex-col'>
+ <p className='text-6xl font-black font-sans text-gradient-primary'>100%</p>
+ <p className='text-lg text-muted-foreground'>Open Source</p>
+ </div>
+
+ {circles.map((circle, circleIndex) => {
+ const gradients = [
+ `conic-gradient(from ${rotation * circle.circleRotationSpeed}deg,
+ hsl(var(--primary) / 0.15),
+ hsl(var(--primary) / 0),
+ hsl(var(--primary) / 0),
+ hsl(var(--primary) / 0.15),
+ hsl(var(--primary) / 0),
+ hsl(var(--primary) / 0.05),
+ hsl(var(--primary) / 0.15)
+ )`,
+ `conic-gradient(from ${rotation * circle.circleRotationSpeed + 45}deg,
+ hsl(var(--primary) / 0.12),
+ hsl(var(--primary) / 0),
+ hsl(var(--primary) / 0.08),
+ hsl(var(--primary) / 0),
+ hsl(var(--primary) / 0.2),
+ hsl(var(--primary) / 0),
+ hsl(var(--primary) / 0.12)
+ )`,
+ `conic-gradient(from ${rotation * circle.circleRotationSpeed + 90}deg,
+ hsl(var(--primary) / 0.1),
+ hsl(var(--primary) / 0.18),
+ hsl(var(--primary) / 0.1),
+ hsl(var(--primary) / 0),
+ hsl(var(--primary) / 0.15),
+ hsl(var(--primary) / 0),
+ hsl(var(--primary) / 0.1)
+ )`,
+ `conic-gradient(from ${rotation * circle.circleRotationSpeed + 135}deg,
+ hsl(var(--primary) / 0.08),
+ hsl(var(--primary) / 0),
+ hsl(var(--primary) / 0.16),
+ hsl(var(--primary) / 0.06),
+ hsl(var(--primary) / 0),
+ hsl(var(--primary) / 0.12),
+ hsl(var(--primary) / 0.08)
+ )`,
+ ];
+
+ return (
+ <div
+ key={circleIndex}
+ className='absolute rounded-full'
+ style={{
+ width: circle.radius * 2,
+ height: circle.radius * 2,
+ background: gradients[circleIndex],
+ mask: `radial-gradient(circle at center, transparent ${circle.radius - 2}px, black ${
+ circle.radius - 1
+ }px, black ${circle.radius}px, transparent ${circle.radius + 1}px)`,
+ WebkitMask: `radial-gradient(circle at center, transparent ${circle.radius - 2}px, black ${
+ circle.radius - 1
+ }px, black ${circle.radius}px, transparent ${circle.radius + 1}px)`,
+ }}
+ />
+ );
+ })}
+
+ {logoConfigs.map((logoConfig) => {
+ const { x, y } = getLogoPosition(logoConfig.circleIndex, logoConfig.position, rotation);
+ const LogoComponent = logoConfig.component;
+ const logoSize = logoConfig.size || 32;
+
+ return (
+ <div
+ key={logoConfig.id}
+ className='absolute z-20'
+ style={{
+ left: '50%',
+ top: '50%',
+ transform: `translate(calc(${x}px - ${logoSize / 2}px), calc(${y}px - ${logoSize / 2}px))`,
+ width: logoSize,
+ height: logoSize,
+ }}
+ >
+ <LogoComponent />
+ </div>
+ );
+ })}
+ </div>
+ );
+}

View File

@@ -0,0 +1,6 @@
--- template/app/src/landing-page/components/Hero/index.ts
+++ opensaas-sh/app/src/landing-page/components/Hero/index.ts
@@ -0,0 +1,3 @@
+import Hero from './Hero';
+
+export default Hero;

View File

@@ -0,0 +1,47 @@
--- template/app/src/landing-page/components/HighlightedFeature.tsx
+++ opensaas-sh/app/src/landing-page/components/HighlightedFeature.tsx
@@ -6,6 +6,8 @@
direction?: 'row' | 'row-reverse';
highlightedComponent: React.ReactNode;
tilt?: 'left' | 'right';
+ className?: string;
+ url?: string;
}
/**
@@ -18,6 +20,8 @@
direction = 'row',
highlightedComponent,
tilt,
+ className,
+ url,
}: FeatureProps) => {
const tiltToClass: Record<Required<FeatureProps>['tilt'], string> = {
left: 'rotate-1',
@@ -27,12 +31,14 @@
return (
<div
className={cn(
- 'max-w-6xl mx-auto flex flex-col items-center my-50 gap-x-20 gap-y-10 justify-between px-8 md:px-4 transition-all duration-300 ease-in-out',
+ 'max-w-6xl mx-auto flex flex-col md:items-center my-50 gap-x-20 gap-y-10 justify-between px-8 md:px-4 transition-all duration-300 ease-in-out',
direction === 'row' ? 'md:flex-row' : 'md:flex-row-reverse'
)}
>
<div className='flex-col flex-1'>
- <h2 className='text-4xl font-bold mb-2'>{name}</h2>
+ <a href={url} target='_blank' rel='noopener noreferrer'>
+ <h2 className='text-4xl font-bold mb-2'>{name}</h2>
+ </a>
{typeof description === 'string' ? (
<p className='text-muted-foreground'>{description}</p>
) : (
@@ -42,7 +48,8 @@
<div
className={cn(
'flex flex-1 my-10 transition-transform duration-300 ease-in-out w-full items-center justify-center',
- tilt && tiltToClass[tilt]
+ tilt && tiltToClass[tilt],
+ className
)}
>
{highlightedComponent}

View File

@@ -0,0 +1,44 @@
--- template/app/src/landing-page/components/Testimonials.tsx
+++ opensaas-sh/app/src/landing-page/components/Testimonials.tsx
@@ -1,4 +1,3 @@
-import { useState } from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardTitle } from '../../components/ui/card';
import SectionTitle from './SectionTitle';
@@ -11,17 +10,12 @@
}
export default function Testimonials({ testimonials }: { testimonials: Testimonial[] }) {
- const [isExpanded, setIsExpanded] = useState(false);
- const shouldShowExpand = testimonials.length > 5;
- const mobileItemsToShow = 3;
- const itemsToShow = shouldShowExpand && !isExpanded ? mobileItemsToShow : testimonials.length;
-
return (
<div className='mx-auto mt-32 max-w-7xl sm:mt-56 sm:px-6 lg:px-8'>
<SectionTitle title='What Our Users Say' />
<div className='relative w-full z-10 px-4 md:px-0 columns-1 md:columns-2 lg:columns-3 gap-2 md:gap-6'>
- {testimonials.slice(0, itemsToShow).map((testimonial, idx) => (
+ {testimonials.map((testimonial, idx) => (
<div key={idx} className='break-inside-avoid mb-6'>
<Card className='flex flex-col justify-between'>
<CardContent className='p-6'>
@@ -52,17 +46,6 @@
</div>
))}
</div>
-
- {shouldShowExpand && (
- <div className='flex justify-center mt-8 md:hidden'>
- <button
- onClick={() => setIsExpanded(!isExpanded)}
- className='px-6 py-3 text-sm font-medium text-primary bg-primary/10 hover:bg-primary/20 rounded-lg transition-colors duration-200'
- >
- {isExpanded ? 'Show Less' : `Show ${testimonials.length - mobileItemsToShow} More`}
- </button>
- </div>
- )}
</div>
);
}

View File

@@ -1,171 +0,0 @@
--- template/app/src/landing-page/contentSections.ts
+++ opensaas-sh/app/src/landing-page/contentSections.ts
@@ -1,75 +1,132 @@
import type { NavigationItem } from '../client/components/NavBar/NavBar';
-import { routes } from 'wasp/client/router';
-import { DocsUrl, BlogUrl } from '../shared/common';
-import daBoiAvatar from '../client/static/da-boi.webp';
-import avatarPlaceholder from '../client/static/avatar-placeholder.webp';
+import { DocsUrl, BlogUrl, GithubUrl } from '../shared/common';
export const landingPageNavigationItems: NavigationItem[] = [
{ name: 'Features', to: '#features' },
- { name: 'Pricing', to: routes.PricingPageRoute.to },
{ name: 'Documentation', to: DocsUrl },
{ name: 'Blog', to: BlogUrl },
];
export const features = [
{
- name: 'Cool Feature #1',
- description: 'Describe your cool feature here.',
+ name: 'Open-Source Philosophy',
+ description:
+ 'The repo and framework are 100% open-source, and so are the services wherever possible. Still missing something? Contribute!',
icon: '🤝',
href: DocsUrl,
},
{
- name: 'Cool Feature #2',
- description: 'Describe your cool feature here.',
+ name: 'DIY Auth, Done For You',
+ description: 'Pre-configured full-stack Auth that you own. No 3rd-party services or hidden fees.',
icon: '🔐',
- href: DocsUrl,
+ href: DocsUrl + '/guides/authentication/',
},
{
- name: 'Cool Feature #3',
- description: 'Describe your cool feature here.',
+ name: 'Full-stack Type Safety',
+ description:
+ 'Full support for TypeScript with auto-generated types that span the whole stack. Nothing to configure!',
icon: '🥞',
href: DocsUrl,
},
{
- name: 'Cool Feature #4',
- description: 'Describe your cool feature here.',
+ name: 'Stripe / Lemon Squeezy Integration',
+ description: "No SaaS is complete without payments. We've pre-configured checkout and webhooks. Just choose a provider and start cashing out.",
icon: '💸',
+ href: DocsUrl + '/guides/payments-integration/',
+ },
+ {
+ name: 'Admin Dashboard',
+ description: 'Graphs! Tables! Analytics w/ Plausible or Google! All in one place. Ooooooooooh.',
+ icon: '📈',
+ href: DocsUrl + '/general/admin-dashboard/',
+ },
+ {
+ name: 'Email Sending',
+ description:
+ 'Email sending built-in. Combine it with the cron jobs feature to easily send emails to your customers.',
+ icon: '📧',
+ href: DocsUrl + '/guides/email-sending/',
+ },
+ {
+ name: 'OpenAI API Implemented',
+ description: 'Have a sweet AI-powered app concept? Get your idea shipped to potential customers in days!',
+ icon: '🤖',
+ href: DocsUrl,
+ },
+ {
+ name: 'File Uploads with AWS',
+ description: 'File upload examples with AWS S3 presigned URLs are included and fully documented!',
+ icon: '📁',
+ href: DocsUrl + '/guides/file-uploading/',
+ },
+ {
+ name: 'Blog w/ Astro',
+ description:
+ 'Built-in blog with the Astro framework. Write your posts in Markdown, and watch your SEO performance take off.',
+ icon: '📝',
+ href: DocsUrl + '/start/guided-tour/',
+ },
+ {
+ name: 'Deploy Anywhere. Easily.',
+ description:
+ 'No vendor lock-in because you own all your code. Deploy yourself, or let Wasp deploy it for you with a single command.',
+ icon: '🚀 ',
+ href: DocsUrl + '/guides/deploying/',
+ },
+ {
+ name: 'E2E Tests w/ Playwright',
+ description: 'Move fast without breaking too much. Tests and a CI pipeline w/ GitHub Actions are set up for you.',
+ icon: '🧪',
+ href: DocsUrl + '/guides/tests/',
+ },
+ {
+ name: 'Complete Documentation & Support',
+ description: "We don't leave you hanging. We have detailed docs and a Discord community to help!",
+ icon: '🫂',
href: DocsUrl,
},
];
export const testimonials = [
{
- name: 'Da Boi',
- role: 'Wasp Mascot',
- avatarSrc: daBoiAvatar,
- socialUrl: 'https://twitter.com/wasplang',
- quote: "I don't even know how to code. I'm just a plushie.",
- },
- {
- name: 'Mr. Foobar',
- role: 'Founder @ Cool Startup',
- avatarSrc: avatarPlaceholder,
- socialUrl: '',
- quote: 'This product makes me cooler than I already am.',
- },
- {
- name: 'Jamie',
- role: 'Happy Customer',
- avatarSrc: avatarPlaceholder,
- socialUrl: '#',
- quote: 'My cats love it!',
+ name: 'Max Khamrovskyi',
+ role: 'Senior Eng @ Red Hat',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1719397191205179392/V_QrGPSO_400x400.jpg',
+ socialUrl: 'https://twitter.com/maksim36ua',
+ quote: 'I used Wasp to build and sell my AI-augmented SaaS app for marketplace vendors within two months!',
+ },
+ {
+ name: 'Tim Skaggs',
+ role: 'Founder @ Antler US',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1802196804236091392/ZG0OE_fO_400x400.jpg',
+ socialUrl: 'https://twitter.com/tskaggs',
+ quote: 'Nearly done with a MVP in 3 days of part-time work... and deployed on Fly.io in 10 minutes.',
+ },
+ {
+ name: 'Jonathan Cocharan',
+ role: 'Entrepreneur',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1910056203863883776/jtfVWaEG_400x400.jpg',
+ socialUrl: 'https://twitter.com/JonathanCochran',
+ quote:
+ 'In just 6 nights... my SaaS app is live 🎉! Huge thanks to the amazing @wasplang community 🙌 for their guidance along the way. These tools are incredibly efficient 🤯!',
},
];
export const faqs = [
{
id: 1,
- question: 'Whats the meaning of life?',
- answer: '42.',
- href: 'https://en.wikipedia.org/wiki/42_(number)',
+ question: 'Why is this SaaS Template free and open-source?',
+ answer:
+ 'We believe the best product is made when the community puts their heads together. We also believe a quality starting point for a web app should be free and available to everyone. Our hope is that together we can create the best SaaS template out there and bring our ideas to customers quickly.',
+ },
+ {
+ id: 2,
+ question: "What's Wasp?",
+ href: 'https://wasp-lang.dev',
+ answer: "It's the fastest way to develop full-stack React + NodeJS + Prisma apps and it's what gives this template superpowers. Wasp relies on React, NodeJS, and Prisma to define web components and server queries and actions. Wasp's secret sauce is its compiler which takes the client, server code, and config file and outputs the client app, server app and deployment code, supercharging the development experience. Combined with this template, you can build a SaaS app in record time.",
},
];
export const footerNavigation = {
app: [
+ { name: 'Github', href: GithubUrl },
{ name: 'Documentation', href: DocsUrl },
{ name: 'Blog', href: BlogUrl },
],

View File

@@ -0,0 +1,250 @@
--- template/app/src/landing-page/contentSections.tsx
+++ opensaas-sh/app/src/landing-page/contentSections.tsx
@@ -0,0 +1,247 @@
+import { routes } from 'wasp/client/router';
+import type { NavigationItem } from '../client/components/NavBar/NavBar';
+import blog from '../client/static/assets/blog.webp';
+import email from '../client/static/assets/email.webp';
+import fileupload from '../client/static/assets/fileupload.webp';
+import ai from '../client/static/assets/openapi.webp';
+import kivo from '../client/static/examples/kivo.webp';
+import messync from '../client/static/examples/messync.webp';
+import microinfluencerClub from '../client/static/examples/microinfluencers.webp';
+import promptpanda from '../client/static/examples/promptpanda.webp';
+import reviewradar from '../client/static/examples/reviewradar.webp';
+import scribeist from '../client/static/examples/scribeist.webp';
+import searchcraft from '../client/static/examples/searchcraft.webp';
+import logo from '../client/static/logo.webp';
+import { BlogUrl, DocsUrl, GithubUrl } from '../shared/common';
+import { GridFeature } from './components/FeaturesGrid';
+
+export const landingPageNavigationItems: NavigationItem[] = [
+ { name: 'Features', to: '#features' },
+ { name: 'Documentation', to: DocsUrl },
+ { name: 'Blog', to: BlogUrl },
+];
+export const features: GridFeature[] = [
+ {
+ description: 'Have a sweet AI-powered app concept? Get your idea shipped to potential customers in days!',
+ icon: <img src={ai} alt='AI illustration' />,
+ href: DocsUrl,
+ size: 'medium',
+ fullWidthIcon: true,
+ align: 'left',
+ },
+ {
+ name: 'Full-stack Type Safety',
+ description:
+ 'Full support for TypeScript with auto-generated types that span the whole stack. Nothing to configure!',
+ emoji: '🥞',
+ href: DocsUrl,
+ size: 'medium',
+ },
+ {
+ description: 'File upload examples with AWS S3 presigned URLs are included and fully documented!',
+ icon: <img src={fileupload} alt='File upload illustration' className='w-full h-auto' />,
+ href: DocsUrl + '/guides/file-uploading/',
+ size: 'medium',
+ fullWidthIcon: true,
+ },
+ {
+ name: 'Email Sending',
+ description:
+ 'Email sending built-in. Combine it with the cron jobs feature to easily send emails to your customers.',
+ icon: <img src={email} alt='Email sending illustration' />,
+ href: DocsUrl + '/guides/email-sending/',
+ size: 'medium',
+ fullWidthIcon: true,
+ direction: 'col-reverse',
+ },
+ {
+ name: 'Open SaaS',
+ description: 'Try the demo app',
+ icon: <img src={logo} alt='Wasp Logo' />,
+ href: routes.LoginRoute.to,
+ size: 'medium',
+ highlight: true,
+ },
+ {
+ name: 'Blog w/ Astro',
+ description:
+ 'Built-in blog with the Astro framework. Write your posts in Markdown, and watch your SEO performance take off.',
+ icon: <img src={blog} alt='Blog illustration' />,
+ href: DocsUrl + '/start/guided-tour/',
+ size: 'medium',
+ fullWidthIcon: true,
+ },
+ {
+ name: 'Deploy Anywhere. Easily.',
+ description:
+ 'No vendor lock-in because you own all your code. Deploy yourself, or let Wasp deploy it for you with a single command.',
+ emoji: '🚀',
+ href: DocsUrl + '/guides/deploying/',
+ size: 'medium',
+ },
+ {
+ name: 'Complete Documentation & Support',
+ description: 'And a Discord community to help!',
+ href: DocsUrl,
+ size: 'small',
+ },
+ {
+ name: 'E2E Tests w/ Playwright',
+ description: 'Tests and a CI pipeline w/ GitHub Actions',
+ href: DocsUrl + '/guides/tests/',
+ size: 'small',
+ },
+ {
+ name: 'Open-Source Philosophy',
+ description:
+ 'The repo and framework are 100% open-source, and so are the services wherever possible. Still missing something? Contribute!',
+ emoji: '🤝',
+ href: DocsUrl,
+ size: 'medium',
+ },
+];
+export const testimonials = [
+ {
+ name: 'Max Khamrovskyi',
+ role: 'Senior Eng @ Red Hat',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1719397191205179392/V_QrGPSO_400x400.jpg',
+ socialUrl: 'https://twitter.com/maksim36ua',
+ quote:
+ 'I used Wasp to build and sell my AI-augmented SaaS app for marketplace vendors within two months!',
+ },
+ {
+ name: 'Jonathan Cocharan',
+ role: 'Entrepreneur',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1910056203863883776/jtfVWaEG_400x400.jpg',
+ socialUrl: 'https://twitter.com/JonathanCochran',
+ quote:
+ 'In just 6 nights... my SaaS app is live 🎉! Huge thanks to the amazing @wasplang community 🙌 for their guidance along the way. These tools are incredibly efficient 🤯!',
+ },
+ {
+ name: 'Billy Howell',
+ role: 'Entrepreneur',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1877734205561430016/jjpG4mS6_400x400.jpg',
+ socialUrl: 'https://twitter.com/billyjhowell',
+ quote:
+ "Congrats! I am loving Wasp. It's really helped me, a self-taught coder increase my confidence. I feel like I've finally found the perfect, versatile stack for all my projects instead of trying out a new one each time.",
+ },
+ {
+ name: 'Tim Skaggs',
+ role: 'Founder @ Antler US',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1802196804236091392/ZG0OE_fO_400x400.jpg',
+ socialUrl: 'https://twitter.com/tskaggs',
+ quote: 'Nearly done with a MVP in 3 days of part-time work... and deployed on Fly.io in 10 minutes.',
+ },
+ {
+ name: 'Cam Blackwood',
+ role: 'Founder @ Microinfluencer.club',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1927721707164377089/it8oCAkf_400x400.jpg',
+ socialUrl: 'https://twitter.com/CamBlackwood95',
+ quote: 'Setting up a full stack SaaS in 1 minute with WaspLang.',
+ },
+ {
+ name: 'JLegendz',
+ role: 'Enterpreneur',
+ avatarSrc:
+ 'https://cdn.discordapp.com/avatars/1003468772251811921/a_c6124fcbee5621d1ad9cca83a102c4a9.png?size=80',
+ socialUrl: 'https://discord.com/channels/686873244791210014/1080864617347162122/1246388561020850188',
+ quote:
+ "Just randomly wanted to say that I've been loving working with Wasp so far. The open-saas template is great starting point and great way to learn how Wasp works. The documentation is superb and I see the GitHub is super active. The team is super responsive and the ai kapa rocks! So thanks for the work you all are doing. Ive done plenty of with react in the past but Im a front end person. With wasp though, I'm managing my db, back end functions, actions, queries, all with so much ease. I occasionally get stuck on an issue but within a day or two, and thanks to a couple of AI assistants, I get through it. So thank you!",
+ },
+ {
+ name: 'Dimitrios Mastrogiannis',
+ role: 'Founder @ Kivo.dev',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1771240035020324864/bcNSr-dA_400x400.jpg',
+ socialUrl: 'https://twitter.com/dmastroyiannis',
+ quote: "Without Wasp & Open SaaS, Kivo.dev wouldn't exist",
+ },
+ {
+ name: 'Alex Ionascu',
+ role: 'Entrepreneur',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1356710335001018376/HEfgRu8X_400x400.jpg',
+ socialUrl: 'https://twitter.com/alexandrionascu',
+ quote: "Wasp is like hot water. You don't realise how much you need it unless you try it.",
+ },
+ {
+ name: 'Emm Ajayi',
+ role: 'Enterpreneur',
+ avatarSrc:
+ 'https://media2.dev.to/dynamic/image/width=320,height=320,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1215082%2F860c57b3-ac41-420b-9420-b6efda743596.jpg',
+ socialUrl: 'https://dev.to/wasp/our-web-framework-reached-9000-stars-on-github-9000-jij#comment-2dech',
+ quote:
+ "This is exactly the framework I've been dreaming of ever since I've been waiting to fully venture into the JS Backend Dev world. I believe Wasp will go above 50k stars this year. The documentation alone gives me the confidence that this is my permanent Nodejs framework and I'm staying with Wasp. Phenomenal work by the team... Please keep up your amazing spirits. Thank you",
+ },
+];
+
+export const faqs = [
+ {
+ id: 1,
+ question: 'Why is this SaaS Template free and open-source?',
+ answer:
+ 'We believe the best product is made when the community puts their heads together. We also believe a quality starting point for a web app should be free and available to everyone. Our hope is that together we can create the best SaaS template out there and bring our ideas to customers quickly.',
+ },
+ {
+ id: 2,
+ question: "What's Wasp?",
+ href: 'https://wasp-lang.dev',
+ answer:
+ "It's the fastest way to develop full-stack React + NodeJS + Prisma apps and it's what gives this template superpowers. Wasp relies on React, NodeJS, and Prisma to define web components and server queries and actions. Wasp's secret sauce is its compiler which takes the client, server code, and config file and outputs the client app, server app and deployment code, supercharging the development experience. Combined with this template, you can build a SaaS app in record time.",
+ },
+];
+export const footerNavigation = {
+ app: [
+ { name: 'Github', href: GithubUrl },
+ { name: 'Documentation', href: DocsUrl },
+ { name: 'Blog', href: BlogUrl },
+ ],
+ company: [
+ { name: 'About', href: 'https://wasp.sh' },
+ { name: 'Privacy', href: '#' },
+ { name: 'Terms of Service', href: '#' },
+ ],
+};
+
+export const examples = [
+ {
+ name: 'Microinfluencers',
+ description: 'microinfluencer.club',
+ imageSrc: microinfluencerClub,
+ href: 'https://microinfluencer.club',
+ },
+ {
+ name: 'Kivo',
+ description: 'kivo.dev',
+ imageSrc: kivo,
+ href: 'https://kivo.dev',
+ },
+ {
+ name: 'Searchcraft',
+ description: 'searchcraft.io',
+ imageSrc: searchcraft,
+ href: 'https://www.searchcraft.io',
+ },
+ {
+ name: 'Scribeist',
+ description: 'scribeist.com',
+ imageSrc: scribeist,
+ href: 'https://scribeist.com',
+ },
+ {
+ name: 'Messync',
+ description: 'messync.com',
+ imageSrc: messync,
+ href: 'https://messync.com',
+ },
+ {
+ name: 'Prompt Panda',
+ description: 'promptpanda.io',
+ imageSrc: promptpanda,
+ href: 'https://promptpanda.io',
+ },
+ {
+ name: 'Review Radar',
+ description: 'reviewradar.ai',
+ imageSrc: reviewradar,
+ href: 'https://reviewradar.ai',
+ },
+];

View File

@@ -0,0 +1,16 @@
--- template/app/src/landing-page/logos/PrismaLogo.tsx
+++ opensaas-sh/app/src/landing-page/logos/PrismaLogo.tsx
@@ -1,11 +1,11 @@
export default function PrismaLogo() {
return (
- <svg width={48} height={48} viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'>
+ <svg width={32} height={32} viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'>
<path
className='dark:fill-white'
fill='#545454'
d='M25.21,24.21,12.739,27.928a.525.525,0,0,1-.667-.606L16.528,5.811a.43.43,0,0,1,.809-.094l8.249,17.661A.6.6,0,0,1,25.21,24.21Zm2.139-.878L17.8,2.883h0A1.531,1.531,0,0,0,16.491,2a1.513,1.513,0,0,0-1.4.729L4.736,19.648a1.592,1.592,0,0,0,.018,1.7l5.064,7.909a1.628,1.628,0,0,0,1.83.678l14.7-4.383a1.6,1.6,0,0,0,1-2.218Z'
/>
</svg>
- )
+ );
}

View File

@@ -0,0 +1,15 @@
--- template/app/src/landing-page/logos/ReactLogo.tsx
+++ opensaas-sh/app/src/landing-page/logos/ReactLogo.tsx
@@ -0,0 +1,12 @@
+export default function ReactLogo() {
+ return (
+ <svg width={32} height={32} xmlns='http://www.w3.org/2000/svg' viewBox='-11.5 -10.23174 23 20.46348'>
+ <circle cx='0' cy='0' r='2.05' fill='currentColor' className='dark:fill-white' />
+ <g stroke='currentColor' strokeWidth='1' fill='none' className='dark:stroke-white'>
+ <ellipse rx='11' ry='4.2' />
+ <ellipse rx='11' ry='4.2' transform='rotate(60)' />
+ <ellipse rx='11' ry='4.2' transform='rotate(120)' />
+ </g>
+ </svg>
+ );
+}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
--- template/app/src/landing-page/logos/ShadCNLogo.tsx
+++ opensaas-sh/app/src/landing-page/logos/ShadCNLogo.tsx
@@ -0,0 +1,29 @@
+export default function ShadCNLogo() {
+ return (
+ <svg width={32} height={32} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'>
+ <rect width='256' height='256' fill='none'></rect>
+ <line
+ x1='208'
+ y1='128'
+ x2='128'
+ y2='208'
+ fill='none'
+ stroke='currentColor'
+ strokeLinecap='round'
+ strokeLinejoin='round'
+ strokeWidth='32'
+ ></line>
+ <line
+ x1='192'
+ y1='40'
+ x2='40'
+ y2='192'
+ fill='none'
+ stroke='currentColor'
+ strokeLinecap='round'
+ strokeLinejoin='round'
+ strokeWidth='32'
+ ></line>
+ </svg>
+ );
+}

View File

@@ -0,0 +1,15 @@
--- template/app/src/lib/utils.ts
+++ opensaas-sh/app/src/lib/utils.ts
@@ -4,3 +4,12 @@
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+
+export function formatNumber(number: number) {
+ if (number >= 1_000_000) {
+ return (number / 1_000_000).toFixed(1) + 'M';
+ }
+ if (number >= 1_000) {
+ return (number / 1_000).toFixed(1) + 'K';
+ }
+}

View File

@@ -1,49 +1,29 @@
--- template/app/src/payment/PricingPage.tsx
+++ opensaas-sh/app/src/payment/PricingPage.tsx
@@ -7,6 +7,7 @@
import { cn } from '../client/cn';
@@ -10,6 +10,7 @@
import { PaymentPlanId, paymentPlans, prettyPaymentPlanName, SubscriptionStatus } from './plans';
const bestDealPaymentPlanId: PaymentPlanId = PaymentPlanId.Pro;
+const PaymentsDocsURL = 'https://docs.opensaas.sh/guides/payments-integration/';
interface PaymentPlanCard {
name: string;
@@ -105,16 +106,24 @@
Pick your <span className='text-yellow-500'>pricing</span>
@@ -109,8 +110,16 @@
</h2>
</div>
- <p className='mx-auto mt-6 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-white'>
<p className='mx-auto mt-6 max-w-2xl text-center text-lg leading-8 text-muted-foreground'>
- Choose between Stripe and LemonSqueezy as your payment provider. Just add your Product IDs! Try it
- out below with test credit card number <br />
- <span className='px-2 py-1 bg-gray-100 rounded-md text-gray-500'>4242 4242 4242 4242 4242</span>
- </p>
+ <div className='mx-auto mt-6 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-white flex flex-wrap items-center justify-center space-x-2'>
+ <p>
+ Choose between
+ <a href={PaymentsDocsURL} target='_blank' className='text-purple-400 drop-shadow-sm'> Stripe </a>
+ and
+ <a href={PaymentsDocsURL} target='_blank' className='text-yellow-500 drop-shadow-sm'> Lemon Squeezy </a>
+ as your payment provider. Just add your Product IDs! Try it out below with test credit card number
+ <br />
+ <span className='px-2 py-1 bg-gray-100 rounded-md text-gray-500'>4242 4242 4242 4242 4242</span>
+ </p>
+ </div>
+
{errorMessage && (
<div className='mt-8 p-4 bg-red-100 text-red-600 rounded-md dark:bg-red-200 dark:text-red-800'>
{errorMessage}
</div>
)}
+
<div className='isolate mx-auto mt-16 grid max-w-md grid-cols-1 gap-y-8 lg:gap-x-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3'>
{Object.values(PaymentPlanId).map((planId) => (
<div
@@ -201,7 +210,7 @@
)}
disabled={isPaymentLoading}
>
- {!!user ? 'Buy plan' : 'Log in to buy plan'}
+ {!!user ? 'Buy Plan' : 'Log in to buy plan'}
</button>
)}
</div>
+ Choose between{' '}
+ <a href={PaymentsDocsURL} target='_blank' rel='noopener noreferrer'>
+ Stripe
+ </a>{' '}
+ and{' '}
+ <a href={PaymentsDocsURL} target='_blank' rel='noopener noreferrer'>
+ LemonSqueezy
+ </a>{' '}
+ as your payment provider. Just add your Product IDs! Try it out below with test credit card number{' '}
+ <br />
<span className='px-2 py-1 bg-muted rounded-md text-muted-foreground font-mono text-sm'>
4242 4242 4242 4242 4242
</span>

View File

@@ -3,5 +3,4 @@
@@ -1,2 +1,3 @@
export const DocsUrl = 'https://docs.opensaas.sh';
export const BlogUrl = 'https://docs.opensaas.sh/blog';
+export const GithubUrl = 'https://github.com/wasp-lang/open-saas';
\ No newline at end of file
+export const GithubUrl = 'https://github.com/wasp-lang/opensaas';

View File

@@ -1,22 +1,11 @@
--- template/app/tailwind.config.cjs
+++ opensaas-sh/app/tailwind.config.cjs
@@ -8,7 +8,8 @@
theme: {
extend: {
fontFamily: {
- satoshi: ['Satoshi', 'system-ui', 'sans-serif'],
+ sans: ['ui-monospace', 'Liberation Mono', 'Menlo', 'monospace'],
+ satoshi: ['Satoshi', 'sans-serif'],
},
colors: {
current: 'currentColor',
@@ -246,6 +247,9 @@
'spin-2': 'spin 2s linear infinite',
'spin-3': 'spin 3s linear infinite',
},
+ aspectRatio: {
+ '4/3': '4 / 3',
+ },
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
@@ -277,6 +277,8 @@
'7': '-5px 0 0 #313D4A, 5px 0 0 #313D4A',
'8': '1px 0 0 #313D4A, -1px 0 0 #313D4A, 0 1px 0 #313D4A, 0 -1px 0 #313D4A, 0 3px 13px rgb(0 0 0 / 8%)',
default: '0px 8px 13px -3px rgba(0, 0, 0, 0.07)',
+ outer: '0px 1px 3px 0px hsl(var(--background)/.8) inset, 0px 0.5px 1px 0px hsl(var(--background)/.8) inset, 0px -1px 3px 0px hsl(var(--primary)/.3) inset, 0px -0.5px 1px 0px hsl(var(--primary)/.15) inset, 0px -2px 4.8px 0px hsl(var(--background)/.05), 0px 1px 2px 0px hsl(var(--background)/.1), 0px 2px 4px 0px hsl(var(--background)/.1), 0px 4px 8px 0px hsl(var(--background)/.15);',
+ inner: '0px 1px 3px 0px hsl(var(--background)/.8), 0px 0.5px 1px 0px hsl(var(--background)/.5), 0px -1px 3px 0px hsl(var(--primary)/.5), 0px -0.5px 1px 0px hsl(var(--primary)/.5), 0px -1px 4px 0px hsl(var(--background)/.06) inset, 0px -2px 4.8px 0px hsl(var(--background)/.06) inset, 0px 1px 2px 0px hsl(var(--background)/.06) inset, 0px 2px 4px 0px hsl(var(--background)/.06) inset, 0px 4px 8px 0px hsl(var(--background)/.06) inset',
card: '0px 1px 3px rgba(0, 0, 0, 0.12)',
'card-2': '0px 1px 2px rgba(0, 0, 0, 0.05)',
switcher: '0px 2px 4px rgba(0, 0, 0, 0.2), inset 0px 2px 2px #FFFFFF, inset 0px -1px 1px rgba(0, 0, 0, 0.1)',

View File

@@ -40,9 +40,9 @@ export default defineConfig({
},
components: {
SiteTitle: './src/components/MyHeader.astro',
ThemeSelect: './src/components/MyThemeSelect.astro',
// We customized ThemeSelect to include a "Copy URL for LLMs" button
ThemeSelect: './src/components/MyRightNavBarItems.astro',
Head: './src/components/HeadWithOGImage.astro',
PageTitle: './src/components/TitleWithBannerImage.astro',
},
social: {
github: 'https://github.com/wasp-lang/open-saas',

View File

@@ -0,0 +1,68 @@
---
---
<copy-llm-button>
<button type="button" class="flex items-center justify-center">
<span id="copy-llm-check" class="hidden w-[1.2em]">✅</span>
<svg id="copy-llm-icon" xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
<span class="ml-2 text-sm">Copy URL for LLMs</span>
</button>
</copy-llm-button>
<style>
button {
color: var(--sl-color-text-primary);
font-weight: 600;
text-decoration: none;
background-color: transparent;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
font-family: inherit;
font-size: var(--sl-text-body);
line-height: 1.5;
}
button:hover {
opacity: 0.8;
}
</style>
<script>
class CopyLlmButton extends HTMLElement {
constructor() {
super();
const button = this.querySelector('button');
if (!button) return;
const llmsUrl = 'https://docs.opensaas.sh/llms-full.txt';
button.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(llmsUrl);
const originalIcon = button.querySelector('svg');
const checkIcon = button.querySelector('#copy-llm-check');
if (!originalIcon || !checkIcon) return;
checkIcon.classList.remove('hidden');
originalIcon.classList.add('hidden');
setTimeout(() => {
originalIcon.classList.remove('hidden');
checkIcon.classList.add('hidden');
}, 2000);
} catch (err) {
console.error('Failed to copy URL: ', err);
button.textContent = 'Error!';
setTimeout(() => {
button.textContent = 'Copy URL for LLMs';
}, 2000);
}
});
}
}
customElements.define('copy-llm-button', CopyLlmButton);
</script>

View File

@@ -1,11 +1,16 @@
---
import StarlightThemeSelect from '@astrojs/starlight/components/ThemeSelect.astro'
import CopyForLlmButton from './CopyForLlmButton.astro'
import type { Props } from '@astrojs/starlight/props'
---
<StarlightThemeSelect {...Astro.props}>
<slot />
</StarlightThemeSelect>
<div class="flex items-center justify-center gap-2">
<CopyForLlmButton />
<span aria-hidden="true" style="display: inline-block; width: 1px; height: 2em; background: var(--sl-color-gray-5); margin: 0 0.5rem;"></span>
<StarlightThemeSelect {...Astro.props}>
<slot />
</StarlightThemeSelect>
</div>
<style>
div {

View File

@@ -2,6 +2,10 @@
Built with [Wasp](https://wasp.sh), based on the [Open Saas](https://opensaas.sh) template.
## UI Components
This template includes [ShadCN UI](https://ui.shadcn.com/) v2 for beautiful, accessible React components. See [SHADCN_SETUP.md](./SHADCN_SETUP.md) for details on how to use ShadCN components in your app.
## Development
### Running locally

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.cjs",
"css": "src/client/Main.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "src/components",
"utils": "src/lib/utils",
"ui": "src/components/ui",
"lib": "src/lib",
"hooks": "src/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -275,35 +275,12 @@ page AdminSettingsPage {
component: import AdminSettings from "@src/admin/elements/settings/SettingsPage"
}
route AdminChartsRoute { path: "/admin/chart", to: AdminChartsPage }
page AdminChartsPage {
authRequired: true,
component: import AdminCharts from "@src/admin/elements/charts/ChartsPage"
}
route AdminFormElementsRoute { path: "/admin/forms/form-elements", to: AdminFormElementsPage }
page AdminFormElementsPage {
authRequired: true,
component: import AdminForms from "@src/admin/elements/forms/FormElementsPage"
}
route AdminFormLayoutsRoute { path: "/admin/forms/form-layouts", to: AdminFormLayoutsPage }
page AdminFormLayoutsPage {
authRequired: true,
component: import AdminForms from "@src/admin/elements/forms/FormLayoutsPage"
}
route AdminCalendarRoute { path: "/admin/calendar", to: AdminCalendarPage }
page AdminCalendarPage {
authRequired: true,
component: import AdminCalendar from "@src/admin/elements/calendar/CalendarPage"
}
route AdminUIAlertsRoute { path: "/admin/ui/alerts", to: AdminUIAlertsPage }
page AdminUIAlertsPage {
authRequired: true,
component: import AdminUI from "@src/admin/elements/ui-elements/AlertsPage"
}
route AdminUIButtonsRoute { path: "/admin/ui/buttons", to: AdminUIButtonsPage }
page AdminUIButtonsPage {

View File

@@ -7,12 +7,26 @@
"@aws-sdk/s3-request-presigner": "^3.523.0",
"@google-analytics/data": "4.1.0",
"@headlessui/react": "1.7.13",
"@hookform/resolvers": "^5.1.1",
"@lemonsqueezy/lemonsqueezy.js": "^3.2.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.7",
"apexcharts": "3.41.0",
"clsx": "^2.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"headlessui": "^0.0.0",
"lucide-react": "^0.525.0",
"node-fetch": "3.3.0",
"openai": "^4.55.3",
"prettier": "3.1.1",
@@ -20,15 +34,16 @@
"react": "^18.2.0",
"react-apexcharts": "1.4.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.60.0",
"react-hot-toast": "^2.4.1",
"react-icons": "4.11.0",
"react-router-dom": "^6.26.2",
"stripe": "11.15.0",
"tailwind-merge": "^2.2.1",
"tailwind-merge": "^2.6.0",
"tailwindcss": "^3.2.7",
"tailwindcss-animate": "^1.0.7",
"vanilla-cookieconsent": "^3.0.1",
"wasp": "file:.wasp/out/sdk/wasp",
"zod": "^3.23.8"
"zod": "^3.25.76"
},
"devDependencies": {
"@faker-js/faker": "8.3.1",

View File

@@ -1,14 +1,14 @@
import { type AuthUser } from 'wasp/auth';
import { useQuery, getDailyStats } from 'wasp/client/operations';
import TotalSignupsCard from './TotalSignupsCard';
import { getDailyStats, useQuery } from 'wasp/client/operations';
import { cn } from '../../../client/cn';
import DefaultLayout from '../../layout/DefaultLayout';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
import RevenueAndProfitChart from './RevenueAndProfitChart';
import SourcesTable from './SourcesTable';
import TotalPageViewsCard from './TotalPageViewsCard';
import TotalPayingUsersCard from './TotalPayingUsersCard';
import TotalRevenueCard from './TotalRevenueCard';
import RevenueAndProfitChart from './RevenueAndProfitChart';
import SourcesTable from './SourcesTable';
import DefaultLayout from '../../layout/DefaultLayout';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
import { cn } from '../../../client/cn';
import TotalSignupsCard from './TotalSignupsCard';
const Dashboard = ({ user }: { user: AuthUser }) => {
useRedirectHomeUnlessUserIsAdmin({ user });
@@ -18,9 +18,11 @@ const Dashboard = ({ user }: { user: AuthUser }) => {
return (
<DefaultLayout user={user}>
<div className='relative'>
<div className={cn({
'opacity-25': !stats,
})}>
<div
className={cn({
'opacity-25': !stats,
})}
>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 xl:grid-cols-4 2xl:gap-7.5'>
<TotalPageViewsCard
totalPageViews={stats?.dailyStats.totalViews}
@@ -45,12 +47,10 @@ const Dashboard = ({ user }: { user: AuthUser }) => {
</div>
{!stats && (
<div className='absolute inset-0 flex items-start justify-center bg-white/50 dark:bg-boxdark-2/50'>
<div className='rounded-lg bg-white p-8 shadow-lg dark:bg-boxdark'>
<p className='text-2xl font-bold text-boxdark dark:text-white'>
No daily stats generated yet
</p>
<p className='mt-2 text-sm text-bodydark2'>
<div className='absolute inset-0 flex items-start justify-center bg-background/50'>
<div className='rounded-lg bg-card p-8 shadow-lg'>
<p className='text-2xl font-bold text-foreground'>No daily stats generated yet</p>
<p className='mt-2 text-sm text-muted-foreground'>
Stats will appear here once the daily stats job has run
</p>
</div>

View File

@@ -1,5 +1,5 @@
import { ApexOptions } from 'apexcharts';
import React, { useState, useMemo, useEffect } from 'react';
import { useEffect, useMemo, useState } from 'react';
import ReactApexChart from 'react-apexcharts';
import { type DailyStatsProps } from '../../../analytics/stats';
@@ -175,7 +175,12 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => {
}, [dailyRevenueArray]);
useEffect(() => {
if (!!daysOfWeekArr && daysOfWeekArr?.length > 0 && !!dailyRevenueArray && dailyRevenueArray?.length > 0) {
if (
!!daysOfWeekArr &&
daysOfWeekArr?.length > 0 &&
!!dailyRevenueArray &&
dailyRevenueArray?.length > 0
) {
setChartOptions({
...options,
xaxis: {
@@ -184,7 +189,7 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => {
},
yaxis: {
...options.yaxis,
// get the min & max values to the neareast hundred
// get the min & max values to the neareast hundred
max: Math.ceil(Math.max(...dailyRevenueArray) / 100) * 100,
min: Math.floor(Math.min(...dailyRevenueArray) / 100) * 100,
},
@@ -193,7 +198,7 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => {
}, [daysOfWeekArr, dailyRevenueArray]);
return (
<div className='col-span-12 rounded-sm border border-stroke bg-white px-5 pt-7.5 pb-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-8'>
<div className='col-span-12 rounded-sm border border-border bg-card px-5 pt-7.5 pb-5 shadow-default sm:px-7.5 xl:col-span-8'>
<div className='flex flex-wrap items-start justify-between gap-3 sm:flex-nowrap'>
<div className='flex w-full flex-wrap gap-3 sm:gap-5'>
<div className='flex min-w-47.5'>
@@ -202,7 +207,7 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => {
</span>
<div className='w-full'>
<p className='font-semibold text-primary'>Total Profit</p>
<p className='text-sm font-medium'>Last 7 Days</p>
<p className='text-sm font-medium text-muted-foreground'>Last 7 Days</p>
</div>
</div>
<div className='flex min-w-47.5'>
@@ -211,19 +216,19 @@ const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => {
</span>
<div className='w-full'>
<p className='font-semibold text-secondary'>Total Revenue</p>
<p className='text-sm font-medium'>Last 7 Days</p>
<p className='text-sm font-medium text-muted-foreground'>Last 7 Days</p>
</div>
</div>
</div>
<div className='flex w-full max-w-45 justify-end'>
<div className='inline-flex items-center rounded-md bg-whiter p-1.5 dark:bg-meta-4'>
<button className='rounded bg-white py-1 px-3 text-xs font-medium text-black shadow-card hover:bg-white hover:shadow-card dark:bg-boxdark dark:text-white dark:hover:bg-boxdark'>
<div className='inline-flex items-center rounded-md bg-muted p-1.5'>
<button className='rounded bg-background py-1 px-3 text-xs font-medium text-foreground shadow-card hover:bg-background hover:shadow-card'>
Day
</button>
<button className='rounded py-1 px-3 text-xs font-medium text-black hover:bg-white hover:shadow-card dark:text-white dark:hover:bg-boxdark'>
<button className='rounded py-1 px-3 text-xs font-medium text-muted-foreground hover:bg-background hover:shadow-card'>
Week
</button>
<button className='rounded py-1 px-3 text-xs font-medium text-black hover:bg-white hover:shadow-card dark:text-white dark:hover:bg-boxdark'>
<button className='rounded py-1 px-3 text-xs font-medium text-muted-foreground hover:bg-background hover:shadow-card'>
Month
</button>
</div>

View File

@@ -2,11 +2,11 @@ import { type PageViewSource } from 'wasp/entities';
const SourcesTable = ({ sources }: { sources: PageViewSource[] | undefined }) => {
return (
<div className='rounded-sm border border-stroke bg-white px-5 pt-6 pb-2.5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:pb-1'>
<h4 className='mb-6 text-xl font-semibold text-black dark:text-white'>Top Sources</h4>
<div className='rounded-sm border border-border bg-card px-5 pt-6 pb-2.5 shadow-default sm:px-7.5 xl:pb-1'>
<h4 className='mb-6 text-xl font-semibold text-foreground'>Top Sources</h4>
<div className='flex flex-col'>
<div className='grid grid-cols-3 rounded-sm bg-gray-2 dark:bg-meta-4 '>
<div className='grid grid-cols-3 rounded-sm bg-gray-2 '>
<div className='p-2.5 xl:p-5'>
<h5 className='text-sm font-medium uppercase xsm:text-base'>Source</h5>
</div>
@@ -20,23 +20,23 @@ const SourcesTable = ({ sources }: { sources: PageViewSource[] | undefined }) =>
{sources && sources.length > 0 ? (
sources.map((source) => (
<div className='grid grid-cols-3 border-b border-stroke dark:border-strokedark'>
<div className='grid grid-cols-3 border-b border-border'>
<div className='flex items-center gap-3 p-2.5 xl:p-5'>
<p className='text-black dark:text-white'>{source.name}</p>
<p className='text-foreground'>{source.name}</p>
</div>
<div className='flex items-center justify-center p-2.5 xl:p-5'>
<p className='text-black dark:text-white'>{source.visitors}</p>
<p className='text-foreground'>{source.visitors}</p>
</div>
<div className='hidden items-center justify-center p-2.5 sm:flex xl:p-5'>
<p className='text-black dark:text-white'>--</p>
<p className='text-foreground'>--</p>
</div>
</div>
))
) : (
<div className='flex items-center justify-center p-2.5 xl:p-5'>
<p className='text-black dark:text-white'>No data to display</p>
<p className='text-foreground'>No data to display</p>
</div>
)}
</div>

View File

@@ -1,5 +1,6 @@
import { ArrowDown, ArrowUp, Eye } from 'lucide-react';
import { cn } from '../../../client/cn';
import { UpArrow, DownArrow } from '../../../client/icons/icons-arrows';
import { Card, CardContent, CardHeader } from '../../../components/ui/card';
type PageViewsStats = {
totalPageViews: number | undefined;
@@ -7,48 +8,41 @@ type PageViewsStats = {
};
const TotalPageViewsCard = ({ totalPageViews, prevDayViewsChangePercent }: PageViewsStats) => {
const isDeltaPositive = parseInt(prevDayViewsChangePercent || '') > 0;
const prevDayViewsChangePercentValue = parseInt(prevDayViewsChangePercent || '');
const isDeltaPositive = prevDayViewsChangePercentValue > 0;
return (
<div className='rounded-sm border border-stroke bg-white py-6 px-7.5 shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='flex h-11.5 w-11.5 items-center justify-center rounded-full bg-meta-2 dark:bg-meta-4'>
<svg
className='fill-primary dark:fill-white'
width='22'
height='16'
viewBox='0 0 22 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M11 15.1156C4.19376 15.1156 0.825012 8.61876 0.687512 8.34376C0.584387 8.13751 0.584387 7.86251 0.687512 7.65626C0.825012 7.38126 4.19376 0.918762 11 0.918762C17.8063 0.918762 21.175 7.38126 21.3125 7.65626C21.4156 7.86251 21.4156 8.13751 21.3125 8.34376C21.175 8.61876 17.8063 15.1156 11 15.1156ZM2.26876 8.00001C3.02501 9.27189 5.98126 13.5688 11 13.5688C16.0188 13.5688 18.975 9.27189 19.7313 8.00001C18.975 6.72814 16.0188 2.43126 11 2.43126C5.98126 2.43126 3.02501 6.72814 2.26876 8.00001Z'
fill=''
/>
<path
d='M11 10.9219C9.38438 10.9219 8.07812 9.61562 8.07812 8C8.07812 6.38438 9.38438 5.07812 11 5.07812C12.6156 5.07812 13.9219 6.38438 13.9219 8C13.9219 9.61562 12.6156 10.9219 11 10.9219ZM11 6.625C10.2437 6.625 9.625 7.24375 9.625 8C9.625 8.75625 10.2437 9.375 11 9.375C11.7563 9.375 12.375 8.75625 12.375 8C12.375 7.24375 11.7563 6.625 11 6.625Z'
fill=''
/>
</svg>
</div>
<Card>
<CardHeader>
<div className='flex h-11.5 w-11.5 items-center justify-center rounded-full bg-muted'>
<Eye className='size-6' />
</div>
</CardHeader>
<div className='mt-4 flex items-end justify-between'>
<CardContent className='flex justify-between'>
<div>
<h4 className='text-title-md font-bold text-black dark:text-white'>{totalPageViews}</h4>
<span className='text-sm font-medium'>Total page views</span>
<h4 className='text-title-md font-bold text-foreground'>{totalPageViews}</h4>
<span className='text-sm font-medium text-muted-foreground'>Total page views</span>
</div>
{prevDayViewsChangePercent && parseInt(prevDayViewsChangePercent) !== 0 && (
<span
className={cn('flex items-center gap-1 text-sm font-medium', {
'text-meta-3': isDeltaPositive,
'text-meta-5': !isDeltaPositive,
})}
>
{prevDayViewsChangePercent}%{parseInt(prevDayViewsChangePercent) > 0 ? <UpArrow /> : <DownArrow />}
</span>
)}
</div>
</div>
<span
className={cn('flex items-center gap-1 text-sm font-medium', {
'text-success':
isDeltaPositive && prevDayViewsChangePercent && prevDayViewsChangePercentValue !== 0,
'text-destructive':
!isDeltaPositive && prevDayViewsChangePercent && prevDayViewsChangePercentValue !== 0,
'text-muted-foreground': !prevDayViewsChangePercent || prevDayViewsChangePercentValue === 0,
})}
>
{prevDayViewsChangePercent && prevDayViewsChangePercentValue !== 0
? `${prevDayViewsChangePercent}%`
: '-'}
{prevDayViewsChangePercent &&
prevDayViewsChangePercentValue !== 0 &&
(isDeltaPositive ? <ArrowUp /> : <ArrowDown />)}
</span>
</CardContent>
</Card>
);
};

View File

@@ -1,7 +1,8 @@
import { ArrowDown, ArrowUp, ShoppingBag } from 'lucide-react';
import { useMemo } from 'react';
import { cn } from '../../../client/cn';
import { UpArrow, DownArrow } from '../../../client/icons/icons-arrows';
import { type DailyStatsProps } from '../../../analytics/stats';
import { cn } from '../../../client/cn';
import { Card, CardContent, CardHeader } from '../../../components/ui/card';
const TotalPayingUsersCard = ({ dailyStats, isLoading }: DailyStatsProps) => {
const isDeltaPositive = useMemo(() => {
@@ -9,44 +10,33 @@ const TotalPayingUsersCard = ({ dailyStats, isLoading }: DailyStatsProps) => {
}, [dailyStats]);
return (
<div className='rounded-sm border border-stroke bg-white py-6 px-7.5 shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='flex h-11.5 w-11.5 items-center justify-center rounded-full bg-meta-2 dark:bg-meta-4'>
<svg
className='fill-primary dark:fill-white'
width='22'
height='22'
viewBox='0 0 22 22'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M21.1063 18.0469L19.3875 3.23126C19.2157 1.71876 17.9438 0.584381 16.3969 0.584381H5.56878C4.05628 0.584381 2.78441 1.71876 2.57816 3.23126L0.859406 18.0469C0.756281 18.9063 1.03128 19.7313 1.61566 20.3844C2.20003 21.0375 2.99066 21.3813 3.85003 21.3813H18.1157C18.975 21.3813 19.8 21.0031 20.35 20.3844C20.9 19.7656 21.2094 18.9063 21.1063 18.0469ZM19.2157 19.3531C18.9407 19.6625 18.5625 19.8344 18.15 19.8344H3.85003C3.43753 19.8344 3.05941 19.6625 2.78441 19.3531C2.50941 19.0438 2.37191 18.6313 2.44066 18.2188L4.12503 3.43751C4.19378 2.71563 4.81253 2.16563 5.56878 2.16563H16.4313C17.1532 2.16563 17.7719 2.71563 17.875 3.43751L19.5938 18.2531C19.6282 18.6656 19.4907 19.0438 19.2157 19.3531Z'
fill=''
/>
<path
d='M14.3345 5.29375C13.922 5.39688 13.647 5.80938 13.7501 6.22188C13.7845 6.42813 13.8189 6.63438 13.8189 6.80625C13.8189 8.35313 12.547 9.625 11.0001 9.625C9.45327 9.625 8.1814 8.35313 8.1814 6.80625C8.1814 6.6 8.21577 6.42813 8.25015 6.22188C8.35327 5.80938 8.07827 5.39688 7.66577 5.29375C7.25327 5.19063 6.84077 5.46563 6.73765 5.87813C6.6689 6.1875 6.63452 6.49688 6.63452 6.80625C6.63452 9.2125 8.5939 11.1719 11.0001 11.1719C13.4064 11.1719 15.3658 9.2125 15.3658 6.80625C15.3658 6.49688 15.3314 6.1875 15.2626 5.87813C15.1595 5.46563 14.747 5.225 14.3345 5.29375Z'
fill=''
/>
</svg>
</div>
<Card>
<CardHeader>
<div className='flex h-11.5 w-11.5 items-center justify-center rounded-full bg-muted'>
<ShoppingBag className='size-6' />
</div>
</CardHeader>
<div className='mt-4 flex items-end justify-between'>
<CardContent className='flex justify-between'>
<div>
<h4 className='text-title-md font-bold text-black dark:text-white'>{dailyStats?.paidUserCount}</h4>
<span className='text-sm font-medium'>Total Paying Users</span>
<h4 className='text-title-md font-bold text-foreground'>{dailyStats?.paidUserCount}</h4>
<span className='text-sm font-medium text-muted-foreground'>Total Paying Users</span>
</div>
<span
className={cn('flex items-center gap-1 text-sm font-medium', {
'text-meta-3': isDeltaPositive,
'text-meta-5': !isDeltaPositive,
'text-success': isDeltaPositive && !isLoading,
'text-destructive': !isDeltaPositive && !isLoading && dailyStats?.paidUserDelta !== 0,
'text-muted-foreground': isLoading || !dailyStats?.paidUserDelta,
})}
>
{isLoading ? '...' : dailyStats?.paidUserDelta !== 0 ? dailyStats?.paidUserDelta : '-'}
{dailyStats?.paidUserDelta !== 0 ? isDeltaPositive ? <UpArrow /> : <DownArrow /> : null}
{isLoading ? '...' : dailyStats?.paidUserDelta ?? '-'}
{!isLoading &&
(dailyStats?.paidUserDelta ?? 0) !== 0 &&
(isDeltaPositive ? <ArrowUp /> : <ArrowDown />)}
</span>
</div>
</div>
</CardContent>
</Card>
);
};

View File

@@ -1,61 +1,55 @@
import { ArrowDown, ArrowUp, ShoppingCart } from 'lucide-react';
import { useMemo } from 'react';
import { UpArrow, DownArrow } from '../../../client/icons/icons-arrows';
import { type DailyStatsProps } from '../../../analytics/stats';
import { Card, CardContent, CardHeader } from '../../../components/ui/card';
import { cn } from '../../../lib/utils';
const TotalRevenueCard = ({dailyStats, weeklyStats, isLoading}: DailyStatsProps) => {
const TotalRevenueCard = ({ dailyStats, weeklyStats, isLoading }: DailyStatsProps) => {
const isDeltaPositive = useMemo(() => {
if (!weeklyStats) return false;
return (weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) > 0;
return weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue > 0;
}, [weeklyStats]);
const deltaPercentage = useMemo(() => {
if ( !weeklyStats || weeklyStats.length < 2 || isLoading) return;
if ( weeklyStats[1]?.totalRevenue === 0 || weeklyStats[0]?.totalRevenue === 0 ) return 0;
if (!weeklyStats || weeklyStats.length < 2 || isLoading) return;
if (weeklyStats[1]?.totalRevenue === 0 || weeklyStats[0]?.totalRevenue === 0) return 0;
weeklyStats.sort((a, b) => b.id - a.id);
const percentage = ((weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) / weeklyStats[1]?.totalRevenue) * 100;
const percentage =
((weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) / weeklyStats[1]?.totalRevenue) * 100;
return Math.floor(percentage);
}, [weeklyStats]);
return (
<div className='rounded-sm border border-stroke bg-white py-6 px-7.5 shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='flex h-11.5 w-11.5 items-center justify-center rounded-full bg-meta-2 dark:bg-meta-4'>
<svg
className='fill-primary dark:fill-white'
width='20'
height='22'
viewBox='0 0 20 22'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M11.7531 16.4312C10.3781 16.4312 9.27808 17.5312 9.27808 18.9062C9.27808 20.2812 10.3781 21.3812 11.7531 21.3812C13.1281 21.3812 14.2281 20.2812 14.2281 18.9062C14.2281 17.5656 13.0937 16.4312 11.7531 16.4312ZM11.7531 19.8687C11.2375 19.8687 10.825 19.4562 10.825 18.9406C10.825 18.425 11.2375 18.0125 11.7531 18.0125C12.2687 18.0125 12.6812 18.425 12.6812 18.9406C12.6812 19.4219 12.2343 19.8687 11.7531 19.8687Z'
fill=''
/>
<path
d='M5.22183 16.4312C3.84683 16.4312 2.74683 17.5312 2.74683 18.9062C2.74683 20.2812 3.84683 21.3812 5.22183 21.3812C6.59683 21.3812 7.69683 20.2812 7.69683 18.9062C7.69683 17.5656 6.56245 16.4312 5.22183 16.4312ZM5.22183 19.8687C4.7062 19.8687 4.2937 19.4562 4.2937 18.9406C4.2937 18.425 4.7062 18.0125 5.22183 18.0125C5.73745 18.0125 6.14995 18.425 6.14995 18.9406C6.14995 19.4219 5.73745 19.8687 5.22183 19.8687Z'
fill=''
/>
<path
d='M19.0062 0.618744H17.15C16.325 0.618744 15.6031 1.23749 15.5 2.06249L14.95 6.01562H1.37185C1.0281 6.01562 0.684353 6.18749 0.443728 6.46249C0.237478 6.73749 0.134353 7.11562 0.237478 7.45937C0.237478 7.49374 0.237478 7.49374 0.237478 7.52812L2.36873 13.9562C2.50623 14.4375 2.9531 14.7812 3.46873 14.7812H12.9562C14.2281 14.7812 15.3281 13.8187 15.5 12.5469L16.9437 2.26874C16.9437 2.19999 17.0125 2.16562 17.0812 2.16562H18.9375C19.35 2.16562 19.7281 1.82187 19.7281 1.37499C19.7281 0.928119 19.4187 0.618744 19.0062 0.618744ZM14.0219 12.3062C13.9531 12.8219 13.5062 13.2 12.9906 13.2H3.7781L1.92185 7.56249H14.7094L14.0219 12.3062Z'
fill=''
/>
</svg>
</div>
<Card>
<CardHeader>
<div className='flex h-11.5 w-11.5 items-center justify-center rounded-full bg-muted'>
<ShoppingCart className='size-6' />
</div>
</CardHeader>
<div className='mt-4 flex items-end justify-between'>
<CardContent className='flex justify-between'>
<div>
<h4 className='text-title-md font-bold text-black dark:text-white'>${dailyStats?.totalRevenue}</h4>
<span className='text-sm font-medium'>Total Revenue</span>
<h4 className='text-title-md font-bold text-foreground'>${dailyStats?.totalRevenue}</h4>
<span className='text-sm font-medium text-muted-foreground'>Total Revenue</span>
</div>
<span className='flex items-center gap-1 text-sm font-medium text-meta-3'>
{isLoading ? '...' : !!deltaPercentage ? deltaPercentage + '%' : '-'}
{!!deltaPercentage ? isDeltaPositive ? <UpArrow /> : <DownArrow /> : null}
<span
className={cn('flex items-center gap-1 text-sm font-medium', {
'text-success': isDeltaPositive && !isLoading && deltaPercentage !== 0,
'text-destructive': !isDeltaPositive && !isLoading && deltaPercentage !== 0,
'text-muted-foreground': isLoading || !deltaPercentage || deltaPercentage === 0,
})}
>
{isLoading ? '...' : deltaPercentage && deltaPercentage !== 0 ? `${deltaPercentage}%` : '-'}
{!isLoading &&
deltaPercentage &&
deltaPercentage !== 0 &&
(isDeltaPositive ? <ArrowUp /> : <ArrowDown />)}
</span>
</div>
</div>
</CardContent>
</Card>
);
};

View File

@@ -1,7 +1,8 @@
import { ArrowUp, UsersRound } from 'lucide-react';
import { useMemo } from 'react';
import { cn } from '../../../client/cn';
import { UpArrow } from '../../../client/icons/icons-arrows';
import { type DailyStatsProps } from '../../../analytics/stats';
import { cn } from '../../../client/cn';
import { Card, CardContent, CardHeader } from '../../../components/ui/card';
const TotalSignupsCard = ({ dailyStats, isLoading }: DailyStatsProps) => {
const isDeltaPositive = useMemo(() => {
@@ -9,48 +10,31 @@ const TotalSignupsCard = ({ dailyStats, isLoading }: DailyStatsProps) => {
}, [dailyStats]);
return (
<div className='rounded-sm border border-stroke bg-white py-6 px-7.5 shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='flex h-11.5 w-11.5 items-center justify-center rounded-full bg-meta-2 dark:bg-meta-4'>
<svg
className='fill-primary dark:fill-white'
width='22'
height='18'
viewBox='0 0 22 18'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M7.18418 8.03751C9.31543 8.03751 11.0686 6.35313 11.0686 4.25626C11.0686 2.15938 9.31543 0.475006 7.18418 0.475006C5.05293 0.475006 3.2998 2.15938 3.2998 4.25626C3.2998 6.35313 5.05293 8.03751 7.18418 8.03751ZM7.18418 2.05626C8.45605 2.05626 9.52168 3.05313 9.52168 4.29063C9.52168 5.52813 8.49043 6.52501 7.18418 6.52501C5.87793 6.52501 4.84668 5.52813 4.84668 4.29063C4.84668 3.05313 5.9123 2.05626 7.18418 2.05626Z'
fill=''
/>
<path
d='M15.8124 9.6875C17.6687 9.6875 19.1468 8.24375 19.1468 6.42188C19.1468 4.6 17.6343 3.15625 15.8124 3.15625C13.9905 3.15625 12.478 4.6 12.478 6.42188C12.478 8.24375 13.9905 9.6875 15.8124 9.6875ZM15.8124 4.7375C16.8093 4.7375 17.5999 5.49375 17.5999 6.45625C17.5999 7.41875 16.8093 8.175 15.8124 8.175C14.8155 8.175 14.0249 7.41875 14.0249 6.45625C14.0249 5.49375 14.8155 4.7375 15.8124 4.7375Z'
fill=''
/>
<path
d='M15.9843 10.0313H15.6749C14.6437 10.0313 13.6468 10.3406 12.7874 10.8563C11.8593 9.61876 10.3812 8.79376 8.73115 8.79376H5.67178C2.85303 8.82814 0.618652 11.0625 0.618652 13.8469V16.3219C0.618652 16.975 1.13428 17.4906 1.7874 17.4906H20.2468C20.8999 17.4906 21.4499 16.9406 21.4499 16.2875V15.4625C21.4155 12.4719 18.9749 10.0313 15.9843 10.0313ZM2.16553 15.9438V13.8469C2.16553 11.9219 3.74678 10.3406 5.67178 10.3406H8.73115C10.6562 10.3406 12.2374 11.9219 12.2374 13.8469V15.9438H2.16553V15.9438ZM19.8687 15.9438H13.7499V13.8469C13.7499 13.2969 13.6468 12.7469 13.4749 12.2313C14.0937 11.7844 14.8499 11.5781 15.6405 11.5781H15.9499C18.0812 11.5781 19.8343 13.3313 19.8343 15.4625V15.9438H19.8687Z'
fill=''
/>
</svg>
</div>
<Card>
<CardHeader>
<div className='flex h-11.5 w-11.5 items-center justify-center rounded-full bg-muted'>
<UsersRound className='size-6' />
</div>
</CardHeader>
<div className='mt-4 flex items-end justify-between'>
<CardContent className='flex justify-between'>
<div>
<h4 className='text-title-md font-bold text-black dark:text-white'>{dailyStats?.userCount}</h4>
<span className='text-sm font-medium'>Total Signups</span>
<h4 className='text-title-md font-bold text-foreground'>{dailyStats?.userCount}</h4>
<span className='text-sm font-medium text-muted-foreground'>Total Signups</span>
</div>
<span
className={cn('flex items-center gap-1 text-sm font-medium', {
'text-meta-3': isDeltaPositive,
'text-meta-5': !isDeltaPositive,
'text-success': isDeltaPositive && !isLoading,
'text-destructive': !isDeltaPositive && !isLoading && dailyStats?.userDelta !== 0,
'text-muted-foreground': isLoading || !dailyStats?.userDelta,
})}
>
{isLoading ? '...' : isDeltaPositive ? dailyStats?.userDelta : '-'}
{!!dailyStats && isDeltaPositive && <UpArrow />}
{isLoading ? '...' : dailyStats?.userDelta ?? '-'}
{!isLoading && (dailyStats?.userDelta ?? 0) > 0 && <ArrowUp />}
</span>
</div>
</div>
</CardContent>
</Card>
);
};

View File

@@ -1,117 +1,31 @@
import { useEffect, useRef, useState } from 'react';
import { cn } from '../../../client/cn';
const DropdownDefault = () => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const trigger = useRef<any>(null);
const dropdown = useRef<any>(null);
// close on click outside
useEffect(() => {
const clickHandler = ({ target }: MouseEvent) => {
if (!dropdown.current) return;
if (!dropdownOpen || dropdown.current.contains(target) || trigger.current.contains(target)) return;
setDropdownOpen(false);
};
document.addEventListener('click', clickHandler);
return () => document.removeEventListener('click', clickHandler);
});
// close if the esc key is pressed
useEffect(() => {
const keyHandler = ({ keyCode }: KeyboardEvent) => {
if (!dropdownOpen || keyCode !== 27) return;
setDropdownOpen(false);
};
document.addEventListener('keydown', keyHandler);
return () => document.removeEventListener('keydown', keyHandler);
});
import { Ellipsis, SquarePen, Trash2 } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../../../components/ui/dropdown-menu';
const DropdownEditDelete = () => {
return (
<div className='relative'>
<button ref={trigger} onClick={() => setDropdownOpen(!dropdownOpen)}>
<svg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M2.25 11.25C3.49264 11.25 4.5 10.2426 4.5 9C4.5 7.75736 3.49264 6.75 2.25 6.75C1.00736 6.75 0 7.75736 0 9C0 10.2426 1.00736 11.25 2.25 11.25Z'
fill='#98A6AD'
/>
<path
d='M9 11.25C10.2426 11.25 11.25 10.2426 11.25 9C11.25 7.75736 10.2426 6.75 9 6.75C7.75736 6.75 6.75 7.75736 6.75 9C6.75 10.2426 7.75736 11.25 9 11.25Z'
fill='#98A6AD'
/>
<path
d='M15.75 11.25C16.9926 11.25 18 10.2426 18 9C18 7.75736 16.9926 6.75 15.75 6.75C14.5074 6.75 13.5 7.75736 13.5 9C13.5 10.2426 14.5074 11.25 15.75 11.25Z'
fill='#98A6AD'
/>
</svg>
</button>
<div
ref={dropdown}
onFocus={() => setDropdownOpen(true)}
onBlur={() => setDropdownOpen(false)}
className={cn(
'absolute right-0 top-full z-40 w-40 space-y-1 rounded-sm border border-stroke bg-white p-1.5 shadow-default dark:border-strokedark dark:bg-boxdark',
{
block: dropdownOpen,
hidden: !dropdownOpen,
}
)}
>
<button className='flex w-full items-center gap-2 rounded-sm py-1.5 px-4 text-left text-sm hover:bg-gray dark:hover:bg-meta-4'>
<svg
className='fill-current'
width='16'
height='16'
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g clipPath='url(#clip0_62_9787)'>
<path
d='M15.55 2.97499C15.55 2.77499 15.475 2.57499 15.325 2.42499C15.025 2.12499 14.725 1.82499 14.45 1.52499C14.175 1.24999 13.925 0.974987 13.65 0.724987C13.525 0.574987 13.375 0.474986 13.175 0.449986C12.95 0.424986 12.75 0.474986 12.575 0.624987L10.875 2.32499H2.02495C1.17495 2.32499 0.449951 3.02499 0.449951 3.89999V14C0.449951 14.85 1.14995 15.575 2.02495 15.575H12.15C13 15.575 13.725 14.875 13.725 14V5.12499L15.35 3.49999C15.475 3.34999 15.55 3.17499 15.55 2.97499ZM8.19995 8.99999C8.17495 9.02499 8.17495 9.02499 8.14995 9.02499L6.34995 9.62499L6.94995 7.82499C6.94995 7.79999 6.97495 7.79999 6.97495 7.77499L11.475 3.27499L12.725 4.49999L8.19995 8.99999ZM12.575 14C12.575 14.25 12.375 14.45 12.125 14.45H2.02495C1.77495 14.45 1.57495 14.25 1.57495 14V3.87499C1.57495 3.62499 1.77495 3.42499 2.02495 3.42499H9.72495L6.17495 6.99999C6.04995 7.12499 5.92495 7.29999 5.87495 7.49999L4.94995 10.3C4.87495 10.5 4.92495 10.675 5.02495 10.85C5.09995 10.95 5.24995 11.1 5.52495 11.1H5.62495L8.49995 10.15C8.67495 10.1 8.84995 9.97499 8.97495 9.84999L12.575 6.24999V14ZM13.5 3.72499L12.25 2.49999L13.025 1.72499C13.225 1.92499 14.05 2.74999 14.25 2.97499L13.5 3.72499Z'
fill=''
/>
</g>
<defs>
<clipPath id='clip0_62_9787'>
<rect width='16' height='16' fill='white' />
</clipPath>
</defs>
</svg>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button>
<Ellipsis className='size-4' />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-40'>
<DropdownMenuItem>
<SquarePen className='size-4 mr-2' />
Edit
</button>
<button className='flex w-full items-center gap-2 rounded-sm py-1.5 px-4 text-left text-sm hover:bg-gray dark:hover:bg-meta-4'>
<svg
className='fill-current'
width='16'
height='16'
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M12.225 2.20005H10.3V1.77505C10.3 1.02505 9.70005 0.425049 8.95005 0.425049H7.02505C6.27505 0.425049 5.67505 1.02505 5.67505 1.77505V2.20005H3.75005C3.02505 2.20005 2.42505 2.80005 2.42505 3.52505V4.27505C2.42505 4.82505 2.75005 5.27505 3.22505 5.47505L3.62505 13.75C3.67505 14.775 4.52505 15.575 5.55005 15.575H10.4C11.425 15.575 12.275 14.775 12.325 13.75L12.75 5.45005C13.225 5.25005 13.55 4.77505 13.55 4.25005V3.50005C13.55 2.80005 12.95 2.20005 12.225 2.20005ZM6.82505 1.77505C6.82505 1.65005 6.92505 1.55005 7.05005 1.55005H8.97505C9.10005 1.55005 9.20005 1.65005 9.20005 1.77505V2.20005H6.85005V1.77505H6.82505ZM3.57505 3.52505C3.57505 3.42505 3.65005 3.32505 3.77505 3.32505H12.225C12.325 3.32505 12.425 3.40005 12.425 3.52505V4.27505C12.425 4.37505 12.35 4.47505 12.225 4.47505H3.77505C3.67505 4.47505 3.57505 4.40005 3.57505 4.27505V3.52505V3.52505ZM10.425 14.45H5.57505C5.15005 14.45 4.80005 14.125 4.77505 13.675L4.40005 5.57505H11.625L11.25 13.675C11.2 14.1 10.85 14.45 10.425 14.45Z'
fill=''
/>
<path
d='M8.00005 8.1001C7.70005 8.1001 7.42505 8.3501 7.42505 8.6751V11.8501C7.42505 12.1501 7.67505 12.4251 8.00005 12.4251C8.30005 12.4251 8.57505 12.1751 8.57505 11.8501V8.6751C8.57505 8.3501 8.30005 8.1001 8.00005 8.1001Z'
fill=''
/>
<path
d='M9.99994 8.60004C9.67494 8.57504 9.42494 8.80004 9.39994 9.12504L9.24994 11.325C9.22494 11.625 9.44994 11.9 9.77494 11.925C9.79994 11.925 9.79994 11.925 9.82494 11.925C10.1249 11.925 10.3749 11.7 10.3749 11.4L10.5249 9.20004C10.5249 8.87504 10.2999 8.62504 9.99994 8.60004Z'
fill=''
/>
<path
d='M5.97497 8.60004C5.67497 8.62504 5.42497 8.90004 5.44997 9.20004L5.62497 11.4C5.64997 11.7 5.89997 11.925 6.17497 11.925C6.19997 11.925 6.19997 11.925 6.22497 11.925C6.52497 11.9 6.77497 11.625 6.74997 11.325L6.57497 9.12504C6.57497 8.80004 6.29997 8.57504 5.97497 8.60004Z'
fill=''
/>
</svg>
</DropdownMenuItem>
<DropdownMenuItem>
<Trash2 className='size-4 mr-2' />
Delete
</button>
</div>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default DropdownDefault;
export default DropdownEditDelete;

View File

@@ -1,15 +1,29 @@
import { X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useAuth } from 'wasp/client/auth';
import { getPaginatedUsers, updateIsUserAdminById, useQuery } from 'wasp/client/operations';
import { type User } from 'wasp/entities';
import useDebounce from '../../../client/hooks/useDebounce';
import { Button } from '../../../components/ui/button';
import { Checkbox } from '../../../components/ui/checkbox';
import { Input } from '../../../components/ui/input';
import { Label } from '../../../components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../components/ui/select';
import { Switch } from '../../../components/ui/switch';
import { SubscriptionStatus } from '../../../payment/plans';
import { useQuery, getPaginatedUsers } from 'wasp/client/operations';
import { useState, useEffect } from 'react';
import SwitcherOne from '../../elements/forms/SwitcherOne';
import LoadingSpinner from '../../layout/LoadingSpinner';
import DropdownEditDelete from './DropdownEditDelete';
import { updateIsUserAdminById } from 'wasp/client/operations';
import { type User } from 'wasp/entities';
function AdminSwitch({ id, isAdmin }: Pick<User, 'id' | 'isAdmin'>) {
const { data: currentUser } = useAuth();
const isCurrentUser = currentUser?.id === id;
return (
<SwitcherOne isOn={isAdmin} onChange={(value) => updateIsUserAdminById({ id: id, isAdmin: value })} />
<Switch
checked={isAdmin}
onCheckedChange={(value) => updateIsUserAdminById({ id: id, isAdmin: value })}
disabled={isCurrentUser}
/>
);
}
@@ -17,16 +31,18 @@ const UsersTable = () => {
const [currentPage, setCurrentPage] = useState(1);
const [emailFilter, setEmailFilter] = useState<string | undefined>(undefined);
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
const [subscriptionStatusFilter, setSubcriptionStatusFilter] = useState<Array<SubscriptionStatus | null>>(
const [subscriptionStatusFilter, setSubscriptionStatusFilter] = useState<Array<SubscriptionStatus | null>>(
[]
);
const debouncedEmailFilter = useDebounce(emailFilter, 300);
const skipPages = currentPage - 1;
const { data, isLoading } = useQuery(getPaginatedUsers, {
skipPages,
filter: {
...(emailFilter && { emailContains: emailFilter }),
...(debouncedEmailFilter && { emailContains: debouncedEmailFilter }),
...(isAdminFilter !== undefined && { isAdmin: isAdminFilter }),
...(subscriptionStatusFilter.length > 0 && { subscriptionStatusIn: subscriptionStatusFilter }),
},
@@ -36,20 +52,36 @@ const UsersTable = () => {
function backToPageOne() {
setCurrentPage(1);
},
[emailFilter, subscriptionStatusFilter, isAdminFilter]
[debouncedEmailFilter, subscriptionStatusFilter, isAdminFilter]
);
const handleStatusToggle = (status: SubscriptionStatus | null) => {
setSubscriptionStatusFilter((prev) => {
if (prev.includes(status)) {
return prev.filter((s) => s !== status);
} else {
return [...prev, status];
}
});
};
const clearAllStatusFilters = () => {
setSubscriptionStatusFilter([]);
};
const hasActiveFilters = subscriptionStatusFilter && subscriptionStatusFilter.length > 0;
return (
<div className='flex flex-col gap-4'>
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='flex-col flex items-start justify-between p-6 gap-3 w-full bg-gray-100/40 dark:bg-gray-700/50'>
<div className='rounded-sm border border-border bg-card shadow'>
<div className='flex-col flex items-start justify-between p-6 gap-3 w-full bg-muted/40'>
<span className='text-sm font-medium'>Filters:</span>
<div className='flex items-center justify-between gap-3 w-full px-2'>
<div className='relative flex items-center gap-3 '>
<label htmlFor='email-filter' className='block text-sm text-gray-700 dark:text-white'>
<Label htmlFor='email-filter' className='text-sm text-muted-foreground'>
email:
</label>
<input
</Label>
<Input
type='text'
id='email-filter'
placeholder='dude@example.com'
@@ -57,99 +89,103 @@ const UsersTable = () => {
const value = e.currentTarget.value;
setEmailFilter(value === '' ? undefined : value);
}}
className='rounded border border-stroke py-2 px-5 bg-white outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
<label htmlFor='status-filter' className='block text-sm ml-2 text-gray-700 dark:text-white'>
<Label htmlFor='status-filter' className='text-sm ml-2 text-muted-foreground'>
status:
</label>
<div className='flex-grow relative z-20 rounded border border-stroke pr-8 outline-none bg-white transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
<div className='flex items-center'>
{subscriptionStatusFilter.length > 0 ? (
subscriptionStatusFilter.map((opt) => (
<span
key={opt}
className='z-30 flex items-center my-1 mx-2 py-1 px-2 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
>
{opt ? opt : 'has not subscribed'}
<span
onClick={(e) => {
e.stopPropagation();
setSubcriptionStatusFilter((prevValue) => {
return prevValue?.filter((val) => val !== opt);
});
}}
className='z-30 cursor-pointer pl-2 hover:text-danger'
>
<XIcon />
</span>
</span>
))
) : (
<span className='bg-white text-gray-500 py-2 px-5 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'>
Select Status Filters
</span>
)}
</div>
<select
onChange={(e) => {
const selectedValue = e.target.value == 'has_not_subscribed' ? null : e.target.value;
console.log(selectedValue);
if (selectedValue === 'clear-all') {
setSubcriptionStatusFilter([]);
} else {
setSubcriptionStatusFilter((prevValue) => {
if (prevValue.includes(selectedValue as SubscriptionStatus)) {
return prevValue.filter((val) => val !== selectedValue);
} else {
return [...prevValue, selectedValue as SubscriptionStatus];
}
});
}
}}
name='status-filter'
id='status-filter'
className='absolute top-0 left-0 z-20 h-full w-full bg-white opacity-0'
>
<option value='select-filters'>Select filters</option>
{[...Object.values(SubscriptionStatus), null]
.filter((status) => !subscriptionStatusFilter.includes(status))
.map((status) => {
const extendedStatus = status ?? 'has_not_subscribed'
return <option key={extendedStatus} value={extendedStatus}>
{extendedStatus}
</option>
})}
</select>
<span className='absolute top-1/2 right-4 z-10 -translate-y-1/2'>
<ChevronDownIcon />
</span>
</Label>
<div className='relative'>
<Select>
<SelectTrigger className='w-full min-w-[200px]'>
<SelectValue placeholder='Select Status Filter' />
</SelectTrigger>
<SelectContent className='w-[300px]'>
<div className='p-2'>
<div className='flex items-center justify-between mb-2'>
<span className='text-sm font-medium'>Subscription Status</span>
{subscriptionStatusFilter.length > 0 && (
<button
onClick={clearAllStatusFilters}
className='text-xs text-muted-foreground hover:text-foreground'
>
Clear all
</button>
)}
</div>
<div className='space-y-2'>
<div className='flex items-center space-x-2'>
<Checkbox
id='all-statuses'
checked={subscriptionStatusFilter.length === 0}
onCheckedChange={() => clearAllStatusFilters()}
/>
<Label
htmlFor='all-statuses'
className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
All Statuses
</Label>
</div>
<div className='flex items-center space-x-2'>
<Checkbox
id='has-not-subscribed'
checked={subscriptionStatusFilter.includes(null)}
onCheckedChange={() => handleStatusToggle(null)}
/>
<Label
htmlFor='has-not-subscribed'
className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
Has Not Subscribed
</Label>
</div>
{Object.values(SubscriptionStatus).map((status) => (
<div key={status} className='flex items-center space-x-2'>
<Checkbox
id={status}
checked={subscriptionStatusFilter.includes(status)}
onCheckedChange={() => handleStatusToggle(status)}
/>
<Label
htmlFor={status}
className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
{status}
</Label>
</div>
))}
</div>
</div>
</SelectContent>
</Select>
</div>
<div className='flex items-center gap-2'>
<label htmlFor='isAdmin-filter' className='block text-sm ml-2 text-gray-700 dark:text-white'>
<Label htmlFor='admin-filter' className='text-sm ml-2 text-muted-foreground'>
isAdmin:
</label>
<select
name='isAdmin-filter'
onChange={(e) => {
if (e.target.value === 'both') {
</Label>
<Select
onValueChange={(value) => {
if (value === 'both') {
setIsAdminFilter(undefined);
} else {
setIsAdminFilter(e.target.value === 'true');
setIsAdminFilter(value === 'true');
}
}}
className='relative z-20 w-full appearance-none rounded border border-stroke bg-white p-2 pl-4 pr-8 outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'
>
<option value='both'>both</option>
<option value='true'>true</option>
<option value='false'>false</option>
</select>
<SelectTrigger className='w-full'>
<SelectValue placeholder='both' />
</SelectTrigger>
<SelectContent>
<SelectItem value='both'>both</SelectItem>
<SelectItem value='true'>true</SelectItem>
<SelectItem value='false'>false</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{!isLoading && (
<div className='max-w-60'>
<span className='text-md mr-2 text-black dark:text-white'>page</span>
<input
{data?.totalPages && (
<div className='max-w-60 flex flex-row items-center'>
<span className='text-md mr-2 text-foreground'>page</span>
<Input
type='number'
min={1}
defaultValue={currentPage}
@@ -160,15 +196,33 @@ const UsersTable = () => {
setCurrentPage(value);
}
}}
className='rounded-md border-1 border-stroke bg-transparent px-4 font-medium outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
className='w-20'
/>
<span className='text-md text-black dark:text-white'> / {data?.totalPages} </span>
<span className='text-md text-foreground'> /{data?.totalPages} </span>
</div>
)}
</div>
{hasActiveFilters && (
<div className='flex items-center gap-2 px-2 pt-2 border-border'>
<span className='text-sm font-medium text-muted-foreground'>Active Filters:</span>
<div className='flex flex-wrap gap-2'>
{subscriptionStatusFilter.map((status) => (
<Button
key={status ?? 'null'}
variant='outline'
size='sm'
onClick={() => handleStatusToggle(status)}
>
<X className='w-3 h-3 mr-1' />
{status ?? 'Has Not Subscribed'}
</Button>
))}
</div>
</div>
)}
</div>
<div className='grid grid-cols-9 border-t-4 border-stroke py-4.5 px-4 dark:border-strokedark md:px-6 '>
<div className='grid grid-cols-9 border-t-4 border-border py-4.5 px-4 md:px-6 '>
<div className='col-span-3 flex items-center'>
<p className='font-medium'>Email / Username</p>
</div>
@@ -185,32 +239,25 @@ const UsersTable = () => {
<p className='font-medium'></p>
</div>
</div>
{isLoading && (
<div className='-mt-40'>
<LoadingSpinner />
</div>
)}
{isLoading && <LoadingSpinner />}
{!!data?.users &&
data?.users?.length > 0 &&
data.users.map((user) => (
<div
key={user.id}
className='grid grid-cols-9 gap-4 border-t border-stroke py-4.5 px-4 dark:border-strokedark md:px-6 '
>
<div key={user.id} className='grid grid-cols-9 gap-4 py-4.5 px-4 md:px-6 '>
<div className='col-span-3 flex items-center'>
<div className='flex flex-col gap-1 '>
<p className='text-sm text-black dark:text-white'>{user.email}</p>
<p className='text-sm text-black dark:text-white'>{user.username}</p>
<p className='text-sm text-foreground'>{user.email}</p>
<p className='text-sm text-foreground'>{user.username}</p>
</div>
</div>
<div className='col-span-2 flex items-center'>
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>
<p className='text-sm text-foreground'>{user.subscriptionStatus}</p>
</div>
<div className='col-span-2 flex items-center'>
<p className='text-sm text-meta-3'>{user.paymentProcessorUserId}</p>
<p className='text-sm text-muted-foreground'>{user.paymentProcessorUserId}</p>
</div>
<div className='col-span-1 flex items-center'>
<div className='text-sm text-black dark:text-white'>
<div className='text-sm text-foreground'>
<AdminSwitch {...user} />
</div>
</div>
@@ -224,32 +271,4 @@ const UsersTable = () => {
);
};
function ChevronDownIcon() {
return (
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<g opacity='0.8'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z'
fill='#637381'
></path>
</g>
</svg>
);
}
function XIcon() {
return (
<svg width='14' height='14' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M9.35355 3.35355C9.54882 3.15829 9.54882 2.84171 9.35355 2.64645C9.15829 2.45118 8.84171 2.45118 8.64645 2.64645L6 5.29289L3.35355 2.64645C3.15829 2.45118 2.84171 2.45118 2.64645 2.64645C2.45118 2.84171 2.45118 3.15829 2.64645 3.35355L5.29289 6L2.64645 8.64645C2.45118 8.84171 2.45118 9.15829 2.64645 9.35355C2.84171 9.54882 3.15829 9.54882 3.35355 9.35355L6 6.70711L8.64645 9.35355C8.84171 9.54882 9.15829 9.54882 9.35355 9.35355C9.54882 9.15829 9.54882 8.84171 9.35355 8.64645L6.70711 6L9.35355 3.35355Z'
fill='currentColor'
></path>
</svg>
);
}
export default UsersTable;

View File

@@ -11,10 +11,10 @@ const Calendar = ({ user }: { user: AuthUser }) => {
<Breadcrumb pageName='Calendar' />
{/* <!-- ====== Calendar Section Start ====== --> */}
<div className='w-full max-w-full rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='w-full max-w-full rounded-sm border border-border bg-card shadow-default'>
<table className='w-full'>
<thead>
<tr className='grid grid-cols-7 rounded-t-sm bg-primary text-white'>
<tr className='grid grid-cols-7 rounded-t-sm bg-primary text-primary-foreground'>
<th className='flex h-15 items-center justify-center rounded-tl-sm p-1 text-xs font-semibold sm:text-base xl:p-5'>
<span className='hidden lg:block'> Sunday </span>
<span className='block lg:hidden'> Sun </span>
@@ -48,142 +48,140 @@ const Calendar = ({ user }: { user: AuthUser }) => {
<tbody>
{/* <!-- Line 1 --> */}
<tr className='grid grid-cols-7'>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>1</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>1</span>
<div className='group h-16 w-full flex-grow cursor-pointer py-1 md:h-30'>
<span className='group-hover:text-primary md:hidden'>More</span>
<div className='event invisible absolute left-2 z-99 mb-1 flex w-[200%] flex-col rounded-sm border-l-[3px] border-primary bg-gray px-3 py-1 text-left opacity-0 group-hover:visible group-hover:opacity-100 dark:bg-meta-4 md:visible md:w-[190%] md:opacity-100'>
<span className='event-name text-sm font-semibold text-black dark:text-white'>
Redesign Website
</span>
<span className='time text-sm font-medium text-black dark:text-white'>1 Dec - 2 Dec</span>
<div className='event invisible absolute left-2 z-99 mb-1 flex w-[200%] flex-col rounded-sm border-l-[3px] border-primary bg-muted px-3 py-1 text-left opacity-0 group-hover:visible group-hover:opacity-100 md:visible md:w-[190%] md:opacity-100'>
<span className='event-name text-sm font-semibold text-foreground'>Redesign Website</span>
<span className='time text-sm font-medium text-foreground'>1 Dec - 2 Dec</span>
</div>
</div>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>2</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>2</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>3</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>3</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>4</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>4</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>5</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>5</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>6</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>6</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>7</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>7</span>
</td>
</tr>
{/* <!-- Line 1 --> */}
{/* <!-- Line 2 --> */}
<tr className='grid grid-cols-7'>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>8</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>8</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>9</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>9</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>10</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>10</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>11</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>11</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>12</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>12</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>13</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>13</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>14</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>14</span>
</td>
</tr>
{/* <!-- Line 2 --> */}
{/* <!-- Line 3 --> */}
<tr className='grid grid-cols-7'>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>15</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>15</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>16</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>16</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>17</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>17</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>18</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>18</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>19</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>19</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>20</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>20</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>21</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>21</span>
</td>
</tr>
{/* <!-- Line 3 --> */}
{/* <!-- Line 4 --> */}
<tr className='grid grid-cols-7'>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>22</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>22</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>23</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>23</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>24</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>24</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>25</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>25</span>
<div className='group h-16 w-full flex-grow cursor-pointer py-1 md:h-30'>
<span className='group-hover:text-primary md:hidden'>More</span>
<div className='event invisible absolute left-2 z-99 mb-1 flex w-[300%] flex-col rounded-sm border-l-[3px] border-primary bg-gray px-3 py-1 text-left opacity-0 group-hover:visible group-hover:opacity-100 dark:bg-meta-4 md:visible md:w-[290%] md:opacity-100'>
<span className='event-name text-sm font-semibold text-black dark:text-white'>App Design</span>
<span className='time text-sm font-medium text-black dark:text-white'>25 Dec - 27 Dec</span>
<div className='event invisible absolute left-2 z-99 mb-1 flex w-[300%] flex-col rounded-sm border-l-[3px] border-primary bg-muted px-3 py-1 text-left opacity-0 group-hover:visible group-hover:opacity-100 md:visible md:w-[290%] md:opacity-100'>
<span className='event-name text-sm font-semibold text-foreground'>App Design</span>
<span className='time text-sm font-medium text-foreground'>25 Dec - 27 Dec</span>
</div>
</div>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>26</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>26</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>27</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>27</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>28</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>28</span>
</td>
</tr>
{/* <!-- Line 4 --> */}
{/* <!-- Line 5 --> */}
<tr className='grid grid-cols-7'>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>29</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>29</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>30</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>30</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>31</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>31</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>1</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>1</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>2</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>2</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>3</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>3</span>
</td>
<td className='ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray dark:border-strokedark dark:hover:bg-meta-4 md:h-25 md:p-6 xl:h-31'>
<span className='font-medium text-black dark:text-white'>4</span>
<td className='ease relative h-20 cursor-pointer border border-border p-2 transition duration-500 text-accent hover:bg-accent hover:text-accent-foreground md:h-25 md:p-6 xl:h-31'>
<span className='font-medium'>4</span>
</td>
</tr>
{/* <!-- Line 5 --> */}

View File

@@ -1,138 +0,0 @@
import { ApexOptions } from 'apexcharts';
import React, { useState } from 'react';
import ReactApexChart from 'react-apexcharts';
interface BarChartState {
series: { data: number[] }[];
}
const BarChart: React.FC = () => {
const [state, setState] = useState<BarChartState>({
series: [
{
data: [
168, 385, 201, 298, 187, 195, 291, 110, 215, 390, 280, 112, 123, 212,
270, 190, 310, 115, 90, 380, 112, 223, 292, 170, 290, 110, 115, 290,
380, 312,
],
},
],
});
const options: ApexOptions = {
colors: ['#3C50E0'],
chart: {
fontFamily: 'Satoshi, sans-serif',
type: 'bar',
height: 350,
toolbar: {
show: false,
},
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
// endingShape: "rounded",
borderRadius: 2,
},
},
dataLabels: {
enabled: false,
},
stroke: {
show: true,
width: 4,
colors: ['transparent'],
},
xaxis: {
categories: [
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
'12',
'13',
'14',
'15',
'16',
'17',
'18',
'19',
'20',
'21',
'22',
'23',
'24',
'25',
'26',
'27',
'28',
'29',
'30',
],
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
},
legend: {
show: true,
position: 'top',
horizontalAlign: 'left',
fontFamily: 'inter',
},
yaxis: {
title: {
text: 'Visitors',
}
},
grid: {
yaxis: {
lines: {
show: false,
},
},
},
fill: {
opacity: 1,
},
tooltip: {
x: {
show: false,
},
},
};
return (
<div className="col-span-12 rounded-sm border border-stroke bg-white px-5 pt-7.5 pb-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5">
<div>
<h3 className="text-xl font-semibold text-black dark:text-white">
Visitors Analytics
</h3>
</div>
<div className="mb-2">
<div id="chartFour" className="-ml-5">
<ReactApexChart
options={options}
series={state.series}
type="bar"
height={350}
/>
</div>
</div>
</div>
);
};
export default BarChart;

View File

@@ -1,27 +0,0 @@
import { type AuthUser } from 'wasp/auth';
import Breadcrumb from '../../layout/Breadcrumb';
import DefaultLayout from '../../layout/DefaultLayout';
import BarChart from './BarChart';
import PieChart from './PieChart';
import DataStats from './DataStatsChart';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
const Chart = ({ user }: { user: AuthUser }) => {
useRedirectHomeUnlessUserIsAdmin({ user });
return (
<DefaultLayout user={user}>
<Breadcrumb pageName='Chart' />
<div className='grid grid-cols-12 gap-4 md:gap-6 2xl:gap-7.5'>
<DataStats />
<div className='col-span-12'>
<BarChart />
</div>
<PieChart />
</div>
</DefaultLayout>
);
};
export default Chart;

View File

@@ -1,102 +0,0 @@
const DataStatsChart = () => {
return (
<div className="col-span-12 rounded-sm border border-stroke bg-white p-7.5 shadow-default dark:border-strokedark dark:bg-boxdark">
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-4 xl:gap-0">
<div className="flex items-center justify-center gap-2 border-b border-stroke pb-5 dark:border-strokedark xl:border-b-0 xl:border-r xl:pb-0">
<div>
<h4 className="mb-0.5 text-xl font-semibold text-black dark:text-white md:text-title-lg">
4,350
</h4>
<p className="text-sm font-medium">Unique Visitors</p>
</div>
<div className="flex items-center gap-1">
<svg
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.25259 5.87281L4.22834 9.89706L3.16751 8.83623L9.00282 3.00092L14.8381 8.83623L13.7773 9.89705L9.75306 5.87281L9.75306 15.0046L8.25259 15.0046L8.25259 5.87281Z"
fill="#10B981"
/>
</svg>
<span className="text-meta-3">18%</span>
</div>
</div>
<div className="flex items-center justify-center gap-2 border-b border-stroke pb-5 dark:border-strokedark xl:border-b-0 xl:border-r xl:pb-0">
<div>
<h4 className="mb-0.5 text-xl font-semibold text-black dark:text-white md:text-title-lg">
55.9K
</h4>
<p className="text-sm font-medium">Total Pageviews</p>
</div>
<div className="flex items-center gap-1">
<svg
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.25259 5.87281L4.22834 9.89706L3.16751 8.83623L9.00282 3.00092L14.8381 8.83623L13.7773 9.89705L9.75306 5.87281L9.75306 15.0046L8.25259 15.0046L8.25259 5.87281Z"
fill="#10B981"
/>
</svg>
<span className="text-meta-3">25%</span>
</div>
</div>
<div className="flex items-center justify-center gap-2 border-b border-stroke pb-5 dark:border-strokedark sm:border-b-0 sm:pb-0 xl:border-r">
<div>
<h4 className="mb-0.5 text-xl font-semibold text-black dark:text-white md:text-title-lg">
54%
</h4>
<p className="text-sm font-medium">Bounce Rate</p>
</div>
<div className="flex items-center gap-1">
<svg
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.75302 12.1328L13.7773 8.10856L14.8381 9.16939L9.00279 15.0047L3.16748 9.16939L4.22831 8.10856L8.25256 12.1328V3.00098H9.75302V12.1328Z"
fill="#F0950C"
/>
</svg>
<span className="text-meta-8">7%</span>
</div>
</div>
<div className="flex items-center justify-center gap-2">
<div>
<h4 className="mb-0.5 text-xl font-semibold text-black dark:text-white md:text-title-lg">
2m 56s
</h4>
<p className="text-sm font-medium">Visit Duration</p>
</div>
<div className="flex items-center gap-1">
<svg
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.25259 5.87281L4.22834 9.89706L3.16751 8.83623L9.00282 3.00092L14.8381 8.83623L13.7773 9.89705L9.75306 5.87281L9.75306 15.0046L8.25259 15.0046L8.25259 5.87281Z"
fill="#10B981"
/>
</svg>
<span className="text-meta-3">12%</span>
</div>
</div>
</div>
</div>
);
};
export default DataStatsChart;

View File

@@ -1,150 +0,0 @@
import { ApexOptions } from 'apexcharts';
import React, { useState } from 'react';
import ReactApexChart from 'react-apexcharts';
interface PieChartState {
series: number[];
}
const options: ApexOptions = {
chart: {
type: 'donut',
},
colors: ['#10B981', '#375E83', '#259AE6', '#FFA70B'],
labels: ['Remote', 'Hybrid', 'Onsite', 'Leave'],
legend: {
show: true,
position: 'bottom',
},
plotOptions: {
pie: {
donut: {
size: '65%',
background: 'transparent',
},
},
},
dataLabels: {
enabled: false,
},
responsive: [
{
breakpoint: 2600,
options: {
chart: {
width: 380,
},
},
},
{
breakpoint: 640,
options: {
chart: {
width: 200,
},
},
},
],
};
const PieChart: React.FC = () => {
const [state, setState] = useState<PieChartState>({
series: [65, 34, 12, 56],
});
return (
<div className="col-span-12 rounded-sm border border-stroke bg-white px-5 pt-7.5 pb-5 shadow-default dark:border-strokedark dark:bg-boxdark sm:px-7.5 xl:col-span-5">
<div className="mb-3 justify-between gap-4 sm:flex">
<div>
<h5 className="text-xl font-semibold text-black dark:text-white">
Visitors Analytics
</h5>
</div>
<div>
<div className="relative z-20 inline-block">
<select
name=""
id=""
className="relative z-20 inline-flex appearance-none bg-transparent py-1 pl-3 pr-8 text-sm font-medium outline-none"
>
<option value="">Monthly</option>
<option value="">Yearly</option>
</select>
<span className="absolute top-1/2 right-3 z-10 -translate-y-1/2">
<svg
width="10"
height="6"
viewBox="0 0 10 6"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.47072 1.08816C0.47072 1.02932 0.500141 0.955772 0.54427 0.911642C0.647241 0.808672 0.809051 0.808672 0.912022 0.896932L4.85431 4.60386C4.92785 4.67741 5.06025 4.67741 5.14851 4.60386L9.09079 0.896932C9.19376 0.793962 9.35557 0.808672 9.45854 0.911642C9.56151 1.01461 9.5468 1.17642 9.44383 1.27939L5.50155 4.98632C5.22206 5.23639 4.78076 5.23639 4.51598 4.98632L0.558981 1.27939C0.50014 1.22055 0.47072 1.16171 0.47072 1.08816Z"
fill="#637381"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.22659 0.546578L5.00141 4.09604L8.76422 0.557869C9.08459 0.244537 9.54201 0.329403 9.79139 0.578788C10.112 0.899434 10.0277 1.36122 9.77668 1.61224L9.76644 1.62248L5.81552 5.33722C5.36257 5.74249 4.6445 5.7544 4.19352 5.32924C4.19327 5.32901 4.19377 5.32948 4.19352 5.32924L0.225953 1.61241C0.102762 1.48922 -4.20186e-08 1.31674 -3.20269e-08 1.08816C-2.40601e-08 0.905899 0.0780105 0.712197 0.211421 0.578787C0.494701 0.295506 0.935574 0.297138 1.21836 0.539529L1.22659 0.546578ZM4.51598 4.98632C4.78076 5.23639 5.22206 5.23639 5.50155 4.98632L9.44383 1.27939C9.5468 1.17642 9.56151 1.01461 9.45854 0.911642C9.35557 0.808672 9.19376 0.793962 9.09079 0.896932L5.14851 4.60386C5.06025 4.67741 4.92785 4.67741 4.85431 4.60386L0.912022 0.896932C0.809051 0.808672 0.647241 0.808672 0.54427 0.911642C0.500141 0.955772 0.47072 1.02932 0.47072 1.08816C0.47072 1.16171 0.50014 1.22055 0.558981 1.27939L4.51598 4.98632Z"
fill="#637381"
/>
</svg>
</span>
</div>
</div>
</div>
<div className="mb-2">
<div id="chartThree" className="mx-auto flex justify-center">
<ReactApexChart
options={options}
series={state.series}
type="donut"
/>
</div>
</div>
<div className="-mx-8 flex flex-wrap items-center justify-center gap-y-3">
<div className="w-full px-8 sm:w-1/2">
<div className="flex w-full items-center">
<span className="mr-2 block h-3 w-full max-w-3 rounded-full bg-primary"></span>
<p className="flex w-full justify-between text-sm font-medium text-black dark:text-white">
<span> Desktop </span>
<span> 65% </span>
</p>
</div>
</div>
<div className="w-full px-8 sm:w-1/2">
<div className="flex w-full items-center">
<span className="mr-2 block h-3 w-full max-w-3 rounded-full bg-[#6577F3]"></span>
<p className="flex w-full justify-between text-sm font-medium text-black dark:text-white">
<span> Tablet </span>
<span> 34% </span>
</p>
</div>
</div>
<div className="w-full px-8 sm:w-1/2">
<div className="flex w-full items-center">
<span className="mr-2 block h-3 w-full max-w-3 rounded-full bg-[#8FD0EF]"></span>
<p className="flex w-full justify-between text-sm font-medium text-black dark:text-white">
<span> Mobile </span>
<span> 45% </span>
</p>
</div>
</div>
<div className="w-full px-8 sm:w-1/2">
<div className="flex w-full items-center">
<span className="mr-2 block h-3 w-full max-w-3 rounded-full bg-[#0FADCF]"></span>
<p className="flex w-full justify-between text-sm font-medium text-black dark:text-white">
<span> Unknown </span>
<span> 12% </span>
</p>
</div>
</div>
</div>
</div>
);
};
export default PieChart;

View File

@@ -1,42 +0,0 @@
import { useState } from 'react';
import { cn } from '../../../client/cn';
const CheckboxOne = () => {
const [isChecked, setIsChecked] = useState<boolean>(false);
return (
<div>
<label
htmlFor="checkboxLabelOne"
className="flex cursor-pointer select-none items-center"
>
<div className="relative">
<input
type="checkbox"
id="checkboxLabelOne"
className="sr-only"
onChange={() => {
setIsChecked(!isChecked);
}}
/>
<div
className={cn('mr-4 flex h-5 w-5 items-center justify-center rounded-full border', {
'border-primary': isChecked,
})}
>
<span
className={cn('h-2.5 w-2.5 rounded-full bg-transparent', {
'!bg-primary': isChecked,
})}
>
{' '}
</span>
</div>
</div>
Checkbox Text
</label>
</div>
);
};
export default CheckboxOne;

View File

@@ -1,45 +0,0 @@
import { useState } from 'react';
import { cn } from '../../../client/cn';
const CheckboxTwo = () => {
const [enabled, setEnabled] = useState<boolean>(false);
return (
<div>
<label htmlFor='checkboxLabelTwo' className='flex cursor-pointer text-sm text-gray-700 select-none items-center'>
enabled:
<div className='relative'>
<input
type='checkbox'
id='checkboxLabelTwo'
className='sr-only'
onChange={() => {
setEnabled(!enabled);
}}
/>
<div
className={cn('ml-2 flex h-5 w-5 items-center justify-center rounded border', {
'border-primary bg-gray dark:bg-transparent': enabled,
})}
>
<span
className={cn('opacity-0', {
'!opacity-100': enabled,
})}
>
<svg width='11' height='8' viewBox='0 0 11 8' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M10.0915 0.951972L10.0867 0.946075L10.0813 0.940568C9.90076 0.753564 9.61034 0.753146 9.42927 0.939309L4.16201 6.22962L1.58507 3.63469C1.40401 3.44841 1.11351 3.44879 0.932892 3.63584C0.755703 3.81933 0.755703 4.10875 0.932892 4.29224L0.932878 4.29225L0.934851 4.29424L3.58046 6.95832C3.73676 7.11955 3.94983 7.2 4.1473 7.2C4.36196 7.2 4.55963 7.11773 4.71406 6.9584L10.0468 1.60234C10.2436 1.4199 10.2421 1.1339 10.0915 0.951972ZM4.2327 6.30081L4.2317 6.2998C4.23206 6.30015 4.23237 6.30049 4.23269 6.30082L4.2327 6.30081Z'
fill='#3056D3'
stroke='#3056D3'
strokeWidth='0.4'
></path>
</svg>
</span>
</div>
</div>
</label>
</div>
);
};
export default CheckboxTwo;

View File

@@ -1,290 +0,0 @@
import { type AuthUser } from 'wasp/auth';
import Breadcrumb from '../../layout/Breadcrumb';
import DefaultLayout from '../../layout/DefaultLayout';
import CheckboxOne from './CheckboxOne';
import SwitcherTwo from './SwitcherTwo';
import SwitcherOne from './SwitcherOne';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
import { useState } from 'react';
const FormElements = ({ user }: { user: AuthUser }) => {
useRedirectHomeUnlessUserIsAdmin({ user });
return (
<DefaultLayout user={user}>
<Breadcrumb pageName='FormElements' />
<div className='grid grid-cols-1 gap-9 sm:grid-cols-2'>
<div className='flex flex-col gap-9'>
{/* <!-- Input Fields --> */}
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Input Fields</h3>
</div>
<div className='flex flex-col gap-5.5 p-6.5'>
<div>
<label className='mb-3 block text-black dark:text-white'>Default Input</label>
<input
type='text'
placeholder='Default Input'
className='w-full rounded-lg border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
<div>
<label className='mb-3 block text-black dark:text-white'>Active Input</label>
<input
type='text'
placeholder='Active Input'
className='w-full rounded-lg border-[1.5px] border-primary bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:bg-form-input'
/>
</div>
<div>
<label className='mb-3 block font-medium text-black dark:text-white'>Disabled label</label>
<input
type='text'
placeholder='Disabled label'
disabled
className='w-full rounded-lg border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary dark:disabled:bg-black'
/>
</div>
</div>
</div>
{/* <!-- Toggle switch input --> */}
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Toggle switch input</h3>
</div>
<SwitchExamples />
</div>
{/* <!-- Time and date --> */}
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Time and date</h3>
</div>
<div className='flex flex-col gap-5.5 p-6.5'>
<div>
<label className='mb-3 block text-black dark:text-white'>Date picker</label>
<div className='relative'>
<input
type='date'
className='custom-input-date custom-input-date-1 w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
</div>
<div>
<label className='mb-3 block text-black dark:text-white'>Select date</label>
<div className='relative'>
<input
type='date'
className='custom-input-date custom-input-date-2 w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
</div>
</div>
</div>
{/* <!-- File upload --> */}
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>File upload</h3>
</div>
<div className='flex flex-col gap-5.5 p-6.5'>
<div>
<label className='mb-3 block text-black dark:text-white'>Attach file</label>
<input
type='file'
className='w-full cursor-pointer rounded-lg border-[1.5px] border-stroke bg-transparent font-medium outline-none transition file:mr-5 file:border-collapse file:cursor-pointer file:border-0 file:border-r file:border-solid file:border-stroke file:bg-whiter file:py-3 file:px-5 file:hover:bg-primary file:hover:bg-opacity-10 focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:file:border-form-strokedark dark:file:bg-white/30 dark:file:text-white dark:focus:border-primary'
/>
</div>
<div>
<label className='mb-3 block text-black dark:text-white'>Attach file</label>
<input
type='file'
className='w-full rounded-md border border-stroke p-3 outline-none transition file:mr-4 file:rounded file:border-[0.5px] file:border-stroke file:bg-[#EEEEEE] file:py-1 file:px-2.5 file:text-sm file:font-medium focus:border-primary file:focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:file:border-strokedark dark:file:bg-white/30 dark:file:text-white'
/>
</div>
</div>
</div>
</div>
<div className='flex flex-col gap-9'>
{/* <!-- Textarea Fields --> */}
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Textarea Fields</h3>
</div>
<div className='flex flex-col gap-5.5 p-6.5'>
<div>
<label className='mb-3 block text-black dark:text-white'>Default textarea</label>
<textarea
rows={6}
placeholder='Default textarea'
className='w-full rounded-lg border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
></textarea>
</div>
<div>
<label className='mb-3 block text-black dark:text-white'>Active textarea</label>
<textarea
rows={6}
placeholder='Active textarea'
className='w-full rounded-lg border-[1.5px] border-primary bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:bg-form-input'
></textarea>
</div>
<div>
<label className='mb-3 block text-black dark:text-white'>Disabled textarea</label>
<textarea
rows={6}
disabled
placeholder='Disabled textarea'
className='w-full rounded-lg border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary dark:disabled:bg-black'
></textarea>
</div>
</div>
</div>
{/* <!-- Checkbox and radio --> */}
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Checkbox and radio</h3>
</div>
<div className='flex flex-col gap-5.5 p-6.5'>
<CheckboxOne />
</div>
</div>
{/* <!-- Select input --> */}
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Select input</h3>
</div>
<div className='flex flex-col gap-5.5 p-6.5'>
<div>
<label className='mb-3 block text-black dark:text-white'>Select Country</label>
<div className='relative z-20 bg-white dark:bg-form-input'>
<span className='absolute top-1/2 left-4 z-30 -translate-y-1/2'>
<GlobeIcon />
</span>
<select className='relative z-20 w-full appearance-none rounded border border-stroke bg-transparent py-3 px-12 outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
<option value=''>USA</option>
<option value=''>UK</option>
<option value=''>Canada</option>
</select>
<span className='absolute top-1/2 right-4 z-10 -translate-y-1/2'>
<ChevronDownIcon />
</span>
</div>
</div>
<div>
<label className='mb-3 block text-black dark:text-white'>Multiselect Dropdown</label>
<div className='relative z-20 w-full rounded border border-stroke p-1.5 pr-8 font-medium outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
<div className='flex flex-wrap items-center'>
<span className='m-1.5 flex items-center justify-center rounded border-[.5px] border-stroke bg-gray py-1.5 px-2.5 text-sm font-medium dark:border-strokedark dark:bg-white/30'>
Design
<span className='cursor-pointer pl-2 hover:text-danger'>
<XIcon />
</span>
</span>
<span className='m-1.5 flex items-center justify-center rounded border-[.5px] border-stroke bg-gray py-1.5 px-2.5 text-sm font-medium dark:border-strokedark dark:bg-white/30'>
Development
<span className='cursor-pointer pl-2 hover:text-danger'>
<XIcon />
</span>
</span>
</div>
<select
name=''
id=''
className='absolute top-0 left-0 z-20 h-full w-full bg-transparent opacity-0'
>
<option value=''>Option</option>
<option value=''>Option</option>
</select>
<span className='absolute top-1/2 right-4 z-10 -translate-y-1/2'>
<ChevronDownIcon />
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</DefaultLayout>
);
};
function SwitchExamples() {
const [isFirstOn, setIsFirstOn] = useState<boolean>(false);
const [isSecondOn, setIsSecondOn] = useState<boolean>(false);
return (
<div className='flex flex-col gap-5.5 p-6.5'>
<SwitcherOne isOn={isFirstOn} onChange={setIsFirstOn} />
<SwitcherTwo isOn={isSecondOn} onChange={setIsSecondOn} />
</div>
);
}
function GlobeIcon() {
return (
<svg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'>
<g opacity='0.8'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M10.0007 2.50065C5.85852 2.50065 2.50065 5.85852 2.50065 10.0007C2.50065 14.1428 5.85852 17.5007 10.0007 17.5007C14.1428 17.5007 17.5007 14.1428 17.5007 10.0007C17.5007 5.85852 14.1428 2.50065 10.0007 2.50065ZM0.833984 10.0007C0.833984 4.93804 4.93804 0.833984 10.0007 0.833984C15.0633 0.833984 19.1673 4.93804 19.1673 10.0007C19.1673 15.0633 15.0633 19.1673 10.0007 19.1673C4.93804 19.1673 0.833984 15.0633 0.833984 10.0007Z'
fill='#637381'
></path>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M0.833984 9.99935C0.833984 9.53911 1.20708 9.16602 1.66732 9.16602H18.334C18.7942 9.16602 19.1673 9.53911 19.1673 9.99935C19.1673 10.4596 18.7942 10.8327 18.334 10.8327H1.66732C1.20708 10.8327 0.833984 10.4596 0.833984 9.99935Z'
fill='#637381'
></path>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M7.50084 10.0008C7.55796 12.5632 8.4392 15.0301 10.0006 17.0418C11.5621 15.0301 12.4433 12.5632 12.5005 10.0008C12.4433 7.43845 11.5621 4.97153 10.0007 2.95982C8.4392 4.97153 7.55796 7.43845 7.50084 10.0008ZM10.0007 1.66749L9.38536 1.10547C7.16473 3.53658 5.90275 6.69153 5.83417 9.98346C5.83392 9.99503 5.83392 10.0066 5.83417 10.0182C5.90275 13.3101 7.16473 16.4651 9.38536 18.8962C9.54325 19.069 9.76655 19.1675 10.0007 19.1675C10.2348 19.1675 10.4581 19.069 10.6159 18.8962C12.8366 16.4651 14.0986 13.3101 14.1671 10.0182C14.1674 10.0066 14.1674 9.99503 14.1671 9.98346C14.0986 6.69153 12.8366 3.53658 10.6159 1.10547L10.0007 1.66749Z'
fill='#637381'
></path>
</g>
</svg>
);
}
function ChevronDownIcon() {
return (
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<g opacity='0.8'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z'
fill='#637381'
></path>
</g>
</svg>
);
}
function XIcon() {
return (
<svg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M9.35355 3.35355C9.54882 3.15829 9.54882 2.84171 9.35355 2.64645C9.15829 2.45118 8.84171 2.45118 8.64645 2.64645L6 5.29289L3.35355 2.64645C3.15829 2.45118 2.84171 2.45118 2.64645 2.64645C2.45118 2.84171 2.45118 3.15829 2.64645 3.35355L5.29289 6L2.64645 8.64645C2.45118 8.84171 2.45118 9.15829 2.64645 9.35355C2.84171 9.54882 3.15829 9.54882 3.35355 9.35355L6 6.70711L8.64645 9.35355C8.84171 9.54882 9.15829 9.54882 9.35355 9.35355C9.54882 9.15829 9.54882 8.84171 9.35355 8.64645L6.70711 6L9.35355 3.35355Z'
fill='currentColor'
></path>
</svg>
);
}
export default FormElements;

View File

@@ -1,230 +0,0 @@
import { type AuthUser } from 'wasp/auth';
import Breadcrumb from '../../layout/Breadcrumb';
import DefaultLayout from '../../layout/DefaultLayout';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
const FormLayout = ({ user }: { user: AuthUser }) => {
useRedirectHomeUnlessUserIsAdmin({ user });
return (
<DefaultLayout user={user}>
<Breadcrumb pageName='FormLayout' />
<div className='grid grid-cols-1 gap-9 sm:grid-cols-2'>
<div className='flex flex-col gap-9'>
{/* <!-- Contact Form --> */}
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Contact Form</h3>
</div>
<form action='#'>
<div className='p-6.5'>
<div className='mb-4.5 flex flex-col gap-6 xl:flex-row'>
<div className='w-full xl:w-1/2'>
<label className='mb-2.5 block text-black dark:text-white'>First name</label>
<input
type='text'
placeholder='Enter your first name'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
<div className='w-full xl:w-1/2'>
<label className='mb-2.5 block text-black dark:text-white'>Last name</label>
<input
type='text'
placeholder='Enter your last name'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
</div>
<div className='mb-4.5'>
<label className='mb-2.5 block text-black dark:text-white'>
Email <span className='text-meta-1'>*</span>
</label>
<input
type='email'
placeholder='Enter your email address'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
<div className='mb-4.5'>
<label className='mb-2.5 block text-black dark:text-white'>Subject</label>
<input
type='text'
placeholder='Select subject'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
<div className='mb-4.5'>
<label className='mb-2.5 block text-black dark:text-white'>Subject</label>
<div className='relative z-20 bg-transparent dark:bg-form-input'>
<select className='relative z-20 w-full appearance-none rounded border border-stroke bg-transparent py-3 px-5 outline-none transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'>
<option value=''>Type your subject</option>
<option value=''>USA</option>
<option value=''>UK</option>
<option value=''>Canada</option>
</select>
<span className='absolute top-1/2 right-4 z-30 -translate-y-1/2'>
<svg
className='fill-current'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g opacity='0.8'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z'
fill=''
></path>
</g>
</svg>
</span>
</div>
</div>
<div className='mb-6'>
<label className='mb-2.5 block text-black dark:text-white'>Message</label>
<textarea
rows={6}
placeholder='Type your message'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
></textarea>
</div>
<button className='flex w-full justify-center rounded bg-primary p-3 font-medium text-gray'>
Send Message
</button>
</div>
</form>
</div>
</div>
<div className='flex flex-col gap-9'>
{/* <!-- Sign In Form --> */}
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Sign In Form</h3>
</div>
<form action='#'>
<div className='p-6.5'>
<div className='mb-4.5'>
<label className='mb-2.5 block text-black dark:text-white'>Email</label>
<input
type='email'
placeholder='Enter your email address'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
<div>
<label className='mb-2.5 block text-black dark:text-white'>Password</label>
<input
type='password'
placeholder='Enter password'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
<div className='mt-5 mb-5.5 flex items-center justify-between'>
<label htmlFor='formCheckbox' className='flex cursor-pointer'>
<div className='relative pt-0.5'>
<input type='checkbox' id='formCheckbox' className='taskCheckbox sr-only' />
<div className='box mr-3 flex h-5 w-5 items-center justify-center rounded border border-stroke dark:border-strokedark'>
<span className='text-white opacity-0'>
<svg
className='fill-current'
width='10'
height='7'
viewBox='0 0 10 7'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M9.70685 0.292804C9.89455 0.480344 10 0.734667 10 0.999847C10 1.26503 9.89455 1.51935 9.70685 1.70689L4.70059 6.7072C4.51283 6.89468 4.2582 7 3.9927 7C3.72721 7 3.47258 6.89468 3.28482 6.7072L0.281063 3.70701C0.0986771 3.5184 -0.00224342 3.26578 3.785e-05 3.00357C0.00231912 2.74136 0.10762 2.49053 0.29326 2.30511C0.4789 2.11969 0.730026 2.01451 0.992551 2.01224C1.25508 2.00996 1.50799 2.11076 1.69683 2.29293L3.9927 4.58607L8.29108 0.292804C8.47884 0.105322 8.73347 0 8.99896 0C9.26446 0 9.51908 0.105322 9.70685 0.292804Z'
fill=''
/>
</svg>
</span>
</div>
</div>
<p>Remember me</p>
</label>
<a href='#' className='text-sm text-primary'>
Forget password?
</a>
</div>
<button className='flex w-full justify-center rounded bg-primary p-3 font-medium text-gray'>
Sign In
</button>
</div>
</form>
</div>
{/* <!-- Sign Up Form --> */}
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Sign Up Form</h3>
</div>
<form action='#'>
<div className='p-6.5'>
<div className='mb-4.5'>
<label className='mb-2.5 block text-black dark:text-white'>Name</label>
<input
type='text'
placeholder='Enter your full name'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
<div className='mb-4.5'>
<label className='mb-2.5 block text-black dark:text-white'>Email</label>
<input
type='email'
placeholder='Enter your email address'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
<div className='mb-4.5'>
<label className='mb-2.5 block text-black dark:text-white'>Password</label>
<input
type='password'
placeholder='Enter password'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
<div className='mb-5.5'>
<label className='mb-2.5 block text-black dark:text-white'>Re-type Password</label>
<input
type='password'
placeholder='Re-enter password'
className='w-full rounded border-[1.5px] border-stroke bg-transparent py-3 px-5 font-medium outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
/>
</div>
<button className='flex w-full justify-center rounded bg-primary p-3 font-medium text-gray'>
Sign Up
</button>
</div>
</form>
</div>
</div>
</div>
</DefaultLayout>
);
};
export default FormLayout;

View File

@@ -1,30 +0,0 @@
import { useId } from 'react';
import { cn } from '../../../client/cn';
function SwitcherOne({ isOn, onChange }: { isOn: boolean; onChange: (value: boolean) => void }) {
const id = useId();
return (
<div className='relative'>
<label htmlFor={id} className='flex cursor-pointer select-none items-center'>
<div className='relative'>
<input
id={id}
type='checkbox'
className='sr-only'
checked={isOn}
onChange={(e) => onChange(e.target.checked)}
/>
<div className='reblock h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]'></div>
<div
className={cn('absolute left-1 top-1 h-6 w-6 rounded-full bg-white dark:bg-gray-400 transition', {
'!right-1 !translate-x-full !bg-primary dark:!bg-white': isOn,
})}
></div>
</div>
</label>
</div>
);
}
export default SwitcherOne;

View File

@@ -1,73 +0,0 @@
import { useId } from 'react';
import { cn } from '../../../client/cn';
function SwitcherTwo({ isOn, onChange }: { isOn: boolean; onChange: (value: boolean) => void }) {
const id = useId();
return (
<div>
<label htmlFor={id} className='flex cursor-pointer select-none items-center'>
<div className='relative'>
<input
type='checkbox'
id={id}
className='sr-only'
checked={isOn}
onChange={(e) => onChange(e.target.checked)}
/>
<div className='block h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]'></div>
<div
className={cn(
'dot absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-white transition',
{
'!right-1 !translate-x-full !bg-primary dark:!bg-white': isOn,
}
)}
>
<span className={cn('hidden', { '!block': isOn })}>
<CheckIcon />
</span>
<span className={cn({ hidden: isOn })}>
<XIcon />
</span>
</div>
</div>
</label>
</div>
);
}
const XIcon = () => {
return (
<svg
className='h-4 w-4 stroke-current'
fill='none'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M6 18L18 6M6 6l12 12'></path>
</svg>
);
};
const CheckIcon = () => {
return (
<svg
className='fill-white dark:fill-black'
width='11'
height='8'
viewBox='0 0 11 8'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M10.0915 0.951972L10.0867 0.946075L10.0813 0.940568C9.90076 0.753564 9.61034 0.753146 9.42927 0.939309L4.16201 6.22962L1.58507 3.63469C1.40401 3.44841 1.11351 3.44879 0.932892 3.63584C0.755703 3.81933 0.755703 4.10875 0.932892 4.29224L0.932878 4.29225L0.934851 4.29424L3.58046 6.95832C3.73676 7.11955 3.94983 7.2 4.1473 7.2C4.36196 7.2 4.55963 7.11773 4.71406 6.9584L10.0468 1.60234C10.2436 1.4199 10.2421 1.1339 10.0915 0.951972ZM4.2327 6.30081L4.2317 6.2998C4.23206 6.30015 4.23237 6.30049 4.23269 6.30082L4.2327 6.30081Z'
fill=''
stroke=''
strokeWidth='0.4'
></path>
</svg>
);
};
export default SwitcherTwo;

View File

@@ -1,6 +1,12 @@
import { type AuthUser } from 'wasp/auth';
import { FileText, Mail, Upload, User } from 'lucide-react';
import { FormEvent } from 'react';
import toast from 'react-hot-toast';
import { type AuthUser } from 'wasp/auth';
import { Button } from '../../../components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../../../components/ui/card';
import { Input } from '../../../components/ui/input';
import { Label } from '../../../components/ui/label';
import { Textarea } from '../../../components/ui/textarea';
import Breadcrumb from '../../layout/Breadcrumb';
import DefaultLayout from '../../layout/DefaultLayout';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
@@ -26,48 +32,24 @@ const SettingsPage = ({ user }: { user: AuthUser }) => {
<div className='grid grid-cols-5 gap-8'>
<div className='col-span-5 xl:col-span-3'>
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-7 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Personal Information</h3>
</div>
<div className='p-7'>
<Card>
<CardHeader>
<CardTitle>Personal Information</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className='mb-5.5 flex flex-col gap-5.5 sm:flex-row'>
<div className='w-full sm:w-1/2'>
<label className='mb-3 block text-sm font-medium text-black dark:text-white' htmlFor='fullName'>
<Label htmlFor='full-name' className='mb-3 block text-sm font-medium text-foreground'>
Full Name
</label>
</Label>
<div className='relative'>
<span className='absolute left-4.5 top-4'>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g opacity='0.8'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M3.72039 12.887C4.50179 12.1056 5.5616 11.6666 6.66667 11.6666H13.3333C14.4384 11.6666 15.4982 12.1056 16.2796 12.887C17.061 13.6684 17.5 14.7282 17.5 15.8333V17.5C17.5 17.9602 17.1269 18.3333 16.6667 18.3333C16.2064 18.3333 15.8333 17.9602 15.8333 17.5V15.8333C15.8333 15.1703 15.5699 14.5344 15.1011 14.0655C14.6323 13.5967 13.9964 13.3333 13.3333 13.3333H6.66667C6.00363 13.3333 5.36774 13.5967 4.8989 14.0655C4.43006 14.5344 4.16667 15.1703 4.16667 15.8333V17.5C4.16667 17.9602 3.79357 18.3333 3.33333 18.3333C2.8731 18.3333 2.5 17.9602 2.5 17.5V15.8333C2.5 14.7282 2.93899 13.6684 3.72039 12.887Z'
fill=''
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M9.99967 3.33329C8.61896 3.33329 7.49967 4.45258 7.49967 5.83329C7.49967 7.214 8.61896 8.33329 9.99967 8.33329C11.3804 8.33329 12.4997 7.214 12.4997 5.83329C12.4997 4.45258 11.3804 3.33329 9.99967 3.33329ZM5.83301 5.83329C5.83301 3.53211 7.69849 1.66663 9.99967 1.66663C12.3009 1.66663 14.1663 3.53211 14.1663 5.83329C14.1663 8.13448 12.3009 9.99996 9.99967 9.99996C7.69849 9.99996 5.83301 8.13448 5.83301 5.83329Z'
fill=''
/>
</g>
</svg>
</span>
<input
className='w-full rounded border border-stroke bg-gray py-3 pl-11.5 pr-4.5 text-black focus:border-primary focus-visible:outline-none dark:border-strokedark dark:bg-meta-4 dark:text-white dark:focus:border-primary'
<User className='absolute left-4.5 top-2 h-5 w-5 text-muted-foreground' />
<Input
className='pl-11.5'
type='text'
name='fullName'
id='fullName'
id='full-name'
placeholder='Devid Jhon'
defaultValue='Devid Jhon'
/>
@@ -75,17 +57,16 @@ const SettingsPage = ({ user }: { user: AuthUser }) => {
</div>
<div className='w-full sm:w-1/2'>
<label
className='mb-3 block text-sm font-medium text-black dark:text-white'
htmlFor='phoneNumber'
<Label
htmlFor='phone-number'
className='mb-3 block text-sm font-medium text-foreground'
>
Phone Number
</label>
<input
className='w-full rounded border border-stroke bg-gray py-3 px-4.5 text-black focus:border-primary focus-visible:outline-none dark:border-strokedark dark:bg-meta-4 dark:text-white dark:focus:border-primary'
type='text'
</Label>
<Input
type=''
name='phoneNumber'
id='phoneNumber'
id='phone-number'
placeholder='+990 3343 7865'
defaultValue='+990 3343 7865'
/>
@@ -93,40 +74,16 @@ const SettingsPage = ({ user }: { user: AuthUser }) => {
</div>
<div className='mb-5.5'>
<label className='mb-3 block text-sm font-medium text-black dark:text-white' htmlFor='emailAddress'>
<Label htmlFor='email-address' className='mb-3 block text-sm font-medium text-foreground'>
Email Address
</label>
</Label>
<div className='relative'>
<span className='absolute left-4.5 top-4'>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g opacity='0.8'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M3.33301 4.16667C2.87658 4.16667 2.49967 4.54357 2.49967 5V15C2.49967 15.4564 2.87658 15.8333 3.33301 15.8333H16.6663C17.1228 15.8333 17.4997 15.4564 17.4997 15V5C17.4997 4.54357 17.1228 4.16667 16.6663 4.16667H3.33301ZM0.833008 5C0.833008 3.6231 1.9561 2.5 3.33301 2.5H16.6663C18.0432 2.5 19.1663 3.6231 19.1663 5V15C19.1663 16.3769 18.0432 17.5 16.6663 17.5H3.33301C1.9561 17.5 0.833008 16.3769 0.833008 15V5Z'
fill=''
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M0.983719 4.52215C1.24765 4.1451 1.76726 4.05341 2.1443 4.31734L9.99975 9.81615L17.8552 4.31734C18.2322 4.05341 18.7518 4.1451 19.0158 4.52215C19.2797 4.89919 19.188 5.4188 18.811 5.68272L10.4776 11.5161C10.1907 11.7169 9.80879 11.7169 9.52186 11.5161L1.18853 5.68272C0.811486 5.4188 0.719791 4.89919 0.983719 4.52215Z'
fill=''
/>
</g>
</svg>
</span>
<input
className='w-full rounded border border-stroke bg-gray py-3 pl-11.5 pr-4.5 text-black focus:border-primary focus-visible:outline-none dark:border-strokedark dark:bg-meta-4 dark:text-white dark:focus:border-primary'
<Mail className='absolute left-4.5 top-2 h-5 w-5 text-muted-foreground' />
<Input
className='pl-11.5'
type='email'
name='emailAddress'
id='emailAddress'
id='email-address'
placeholder='devidjond45@gmail.com'
defaultValue='devidjond45@gmail.com'
/>
@@ -134,95 +91,56 @@ const SettingsPage = ({ user }: { user: AuthUser }) => {
</div>
<div className='mb-5.5'>
<label className='mb-3 block text-sm font-medium text-black dark:text-white' htmlFor='Username'>
<Label htmlFor='username' className='mb-3 block text-sm font-medium text-foreground'>
Username
</label>
<input
className='w-full rounded border border-stroke bg-gray py-3 px-4.5 text-black focus:border-primary focus-visible:outline-none dark:border-strokedark dark:bg-meta-4 dark:text-white dark:focus:border-primary'
</Label>
<Input
type='text'
name='Username'
id='Username'
id='username'
placeholder='devidjhon24'
defaultValue='devidjhon24'
/>
</div>
<div className='mb-5.5'>
<label className='mb-3 block text-sm font-medium text-black dark:text-white' htmlFor='Username'>
<Label htmlFor='bio' className='mb-3 block text-sm font-medium text-foreground'>
BIO
</label>
</Label>
<div className='relative'>
<span className='absolute left-4.5 top-4'>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g opacity='0.8' clipPath='url(#clip0_88_10224)'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1.56524 3.23223C2.03408 2.76339 2.66997 2.5 3.33301 2.5H9.16634C9.62658 2.5 9.99967 2.8731 9.99967 3.33333C9.99967 3.79357 9.62658 4.16667 9.16634 4.16667H3.33301C3.11199 4.16667 2.90003 4.25446 2.74375 4.41074C2.58747 4.56702 2.49967 4.77899 2.49967 5V16.6667C2.49967 16.8877 2.58747 17.0996 2.74375 17.2559C2.90003 17.4122 3.11199 17.5 3.33301 17.5H14.9997C15.2207 17.5 15.4326 17.4122 15.5889 17.2559C15.7452 17.0996 15.833 16.8877 15.833 16.6667V10.8333C15.833 10.3731 16.2061 10 16.6663 10C17.1266 10 17.4997 10.3731 17.4997 10.8333V16.6667C17.4997 17.3297 17.2363 17.9656 16.7674 18.4344C16.2986 18.9033 15.6627 19.1667 14.9997 19.1667H3.33301C2.66997 19.1667 2.03408 18.9033 1.56524 18.4344C1.0964 17.9656 0.833008 17.3297 0.833008 16.6667V5C0.833008 4.33696 1.0964 3.70107 1.56524 3.23223Z'
fill=''
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M16.6664 2.39884C16.4185 2.39884 16.1809 2.49729 16.0056 2.67253L8.25216 10.426L7.81167 12.188L9.57365 11.7475L17.3271 3.99402C17.5023 3.81878 17.6008 3.5811 17.6008 3.33328C17.6008 3.08545 17.5023 2.84777 17.3271 2.67253C17.1519 2.49729 16.9142 2.39884 16.6664 2.39884ZM14.8271 1.49402C15.3149 1.00622 15.9765 0.732178 16.6664 0.732178C17.3562 0.732178 18.0178 1.00622 18.5056 1.49402C18.9934 1.98182 19.2675 2.64342 19.2675 3.33328C19.2675 4.02313 18.9934 4.68473 18.5056 5.17253L10.5889 13.0892C10.4821 13.196 10.3483 13.2718 10.2018 13.3084L6.86847 14.1417C6.58449 14.2127 6.28409 14.1295 6.0771 13.9225C5.87012 13.7156 5.78691 13.4151 5.85791 13.1312L6.69124 9.79783C6.72787 9.65131 6.80364 9.51749 6.91044 9.41069L14.8271 1.49402Z'
fill=''
/>
</g>
<defs>
<clipPath id='clip0_88_10224'>
<rect width='20' height='20' fill='white' />
</clipPath>
</defs>
</svg>
</span>
<textarea
className='w-full rounded border border-stroke bg-gray py-3 pl-11.5 pr-4.5 text-black focus:border-primary focus-visible:outline-none dark:border-strokedark dark:bg-meta-4 dark:text-white dark:focus:border-primary'
<FileText className='absolute left-4.5 top-4 h-5 w-5 text-muted-foreground' />
<Textarea
className='w-full rounded border border-border bg-background py-3 pl-11.5 pr-4.5 text-foreground focus:border-primary focus-visible:outline-none'
name='bio'
id='bio'
rows={6}
placeholder='Write your bio here'
defaultValue='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque posuere fermentum urna, eu condimentum mauris tempus ut. Donec fermentum blandit aliquet.'
></textarea>
></Textarea>
</div>
</div>
<div className='flex justify-end gap-4.5'>
<button
className='flex justify-center rounded border border-stroke py-2 px-6 font-medium text-black hover:shadow-1 dark:border-strokedark dark:text-white'
type='submit'
>
<Button variant='outline' type='submit'>
Cancel
</button>
<button
className='flex justify-center rounded bg-primary py-2 px-6 font-medium text-gray hover:shadow-1'
type='submit'
>
Save
</button>
</Button>
<Button type='submit'>Save</Button>
</div>
</form>
</div>
</div>
</CardContent>
</Card>
</div>
<div className='col-span-5 xl:col-span-2'>
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke py-4 px-7 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Your Photo</h3>
</div>
<div className='p-7'>
<Card>
<CardHeader>
<CardTitle>Your Photo</CardTitle>
</CardHeader>
<CardContent>
<form action='#'>
<div className='mb-4 flex items-center gap-3'>
<div className='h-14 w-14 rounded-full'>{/* <img src={userThree} alt="User" /> */}</div>
<div>
<span className='mb-1.5 text-black dark:text-white'>Edit your photo</span>
<span className='mb-1.5 text-foreground'>Edit your photo</span>
<span className='flex gap-2.5'>
<button className='text-sm hover:text-primary'>Delete</button>
<button className='text-sm hover:text-primary'>Update</button>
@@ -232,7 +150,7 @@ const SettingsPage = ({ user }: { user: AuthUser }) => {
<div
id='FileUpload'
className='relative mb-5.5 block w-full cursor-pointer appearance-none rounded border-2 border-dashed border-primary bg-gray py-4 px-4 dark:bg-meta-4 sm:py-7.5'
className='relative mb-5.5 block w-full cursor-pointer appearance-none rounded border-2 border-dashed border-primary bg-background py-4 px-4 sm:py-7.5'
>
<input
type='file'
@@ -240,27 +158,8 @@ const SettingsPage = ({ user }: { user: AuthUser }) => {
className='absolute inset-0 z-50 m-0 h-full w-full cursor-pointer p-0 opacity-0 outline-none'
/>
<div className='flex flex-col items-center justify-center space-y-3'>
<span className='flex h-10 w-10 items-center justify-center rounded-full border border-stroke bg-white dark:border-strokedark dark:bg-boxdark'>
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1.99967 9.33337C2.36786 9.33337 2.66634 9.63185 2.66634 10V12.6667C2.66634 12.8435 2.73658 13.0131 2.8616 13.1381C2.98663 13.2631 3.1562 13.3334 3.33301 13.3334H12.6663C12.8431 13.3334 13.0127 13.2631 13.1377 13.1381C13.2628 13.0131 13.333 12.8435 13.333 12.6667V10C13.333 9.63185 13.6315 9.33337 13.9997 9.33337C14.3679 9.33337 14.6663 9.63185 14.6663 10V12.6667C14.6663 13.1971 14.4556 13.7058 14.0806 14.0809C13.7055 14.456 13.1968 14.6667 12.6663 14.6667H3.33301C2.80257 14.6667 2.29387 14.456 1.91879 14.0809C1.54372 13.7058 1.33301 13.1971 1.33301 12.6667V10C1.33301 9.63185 1.63148 9.33337 1.99967 9.33337Z'
fill='#3C50E0'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M7.5286 1.52864C7.78894 1.26829 8.21106 1.26829 8.4714 1.52864L11.8047 4.86197C12.0651 5.12232 12.0651 5.54443 11.8047 5.80478C11.5444 6.06513 11.1223 6.06513 10.8619 5.80478L8 2.94285L5.13807 5.80478C4.87772 6.06513 4.45561 6.06513 4.19526 5.80478C3.93491 5.54443 3.93491 5.12232 4.19526 4.86197L7.5286 1.52864Z'
fill='#3C50E0'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M7.99967 1.33337C8.36786 1.33337 8.66634 1.63185 8.66634 2.00004V10C8.66634 10.3682 8.36786 10.6667 7.99967 10.6667C7.63148 10.6667 7.33301 10.3682 7.33301 10V2.00004C7.33301 1.63185 7.63148 1.33337 7.99967 1.33337Z'
fill='#3C50E0'
/>
</svg>
<span className='flex h-10 w-10 items-center justify-center rounded-full border border-border bg-background'>
<Upload className='h-4 w-4 text-primary' />
</span>
<p>
<span className='text-primary'>Click to upload</span> or drag and drop
@@ -271,22 +170,14 @@ const SettingsPage = ({ user }: { user: AuthUser }) => {
</div>
<div className='flex justify-end gap-4.5'>
<button
className='flex justify-center rounded border border-stroke py-2 px-6 font-medium text-black hover:shadow-1 dark:border-strokedark dark:text-white'
type='submit'
>
<Button variant='outline' type='submit'>
Cancel
</button>
<button
className='flex justify-center rounded bg-primary py-2 px-6 font-medium text-gray hover:bg-opacity-70'
type='submit'
>
Save
</button>
</Button>
<Button type='submit'>Save</Button>
</div>
</form>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>

View File

@@ -1,75 +0,0 @@
import { type AuthUser } from 'wasp/auth';
import Breadcrumb from '../../layout/Breadcrumb';
import DefaultLayout from '../../layout/DefaultLayout';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
const Alerts = ({ user }: { user: AuthUser }) => {
useRedirectHomeUnlessUserIsAdmin({ user });
return (
<DefaultLayout user={user}>
<Breadcrumb pageName='Alerts' />
<div className='rounded-sm border border-stroke bg-white p-4 shadow-default dark:border-strokedark dark:bg-boxdark md:p-6 xl:p-9'>
<div className='flex flex-col gap-7.5'>
{/* <!-- Alerts Item --> */}
<div className='flex w-full border-l-6 border-warning bg-warning bg-opacity-[15%] px-7 py-8 shadow-md dark:bg-[#1B1B24] dark:bg-opacity-30 md:p-9'>
<div className='mr-5 flex h-9 w-9 items-center justify-center rounded-lg bg-warning bg-opacity-30'>
<svg width='19' height='16' viewBox='0 0 19 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M1.50493 16H17.5023C18.6204 16 19.3413 14.9018 18.8354 13.9735L10.8367 0.770573C10.2852 -0.256858 8.70677 -0.256858 8.15528 0.770573L0.156617 13.9735C-0.334072 14.8998 0.386764 16 1.50493 16ZM10.7585 12.9298C10.7585 13.6155 10.2223 14.1433 9.45583 14.1433C8.6894 14.1433 8.15311 13.6155 8.15311 12.9298V12.9015C8.15311 12.2159 8.6894 11.688 9.45583 11.688C10.2223 11.688 10.7585 12.2159 10.7585 12.9015V12.9298ZM8.75236 4.01062H10.2548C10.6674 4.01062 10.9127 4.33826 10.8671 4.75288L10.2071 10.1186C10.1615 10.5049 9.88572 10.7455 9.50142 10.7455C9.11929 10.7455 8.84138 10.5028 8.79579 10.1186L8.13574 4.75288C8.09449 4.33826 8.33984 4.01062 8.75236 4.01062Z'
fill='#FBBF24'
></path>
</svg>
</div>
<div className='w-full'>
<h5 className='mb-3 text-lg font-semibold text-[#9D5425]'>Attention needed</h5>
<p className='leading-relaxed text-[#D0915C]'>
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the
industry's standard dummy text ever since the 1500s, when
</p>
</div>
</div>
{/* <!-- Alerts Item --> */}
<div className='flex w-full border-l-6 border-[#34D399] bg-[#34D399] bg-opacity-[15%] px-7 py-8 shadow-md dark:bg-[#1B1B24] dark:bg-opacity-30 md:p-9'>
<div className='mr-5 flex h-9 w-full max-w-[36px] items-center justify-center rounded-lg bg-[#34D399]'>
<svg width='16' height='12' viewBox='0 0 16 12' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M15.2984 0.826822L15.2868 0.811827L15.2741 0.797751C14.9173 0.401867 14.3238 0.400754 13.9657 0.794406L5.91888 9.45376L2.05667 5.2868C1.69856 4.89287 1.10487 4.89389 0.747996 5.28987C0.417335 5.65675 0.417335 6.22337 0.747996 6.59026L0.747959 6.59029L0.752701 6.59541L4.86742 11.0348C5.14445 11.3405 5.52858 11.5 5.89581 11.5C6.29242 11.5 6.65178 11.3355 6.92401 11.035L15.2162 2.11161C15.5833 1.74452 15.576 1.18615 15.2984 0.826822Z'
fill='white'
stroke='white'
></path>
</svg>
</div>
<div className='w-full'>
<h5 className='mb-3 text-lg font-semibold text-black dark:text-[#34D399] '>Message Sent Successfully</h5>
<p className='text-base leading-relaxed text-body'>
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
</p>
</div>
</div>
{/* <!-- Alerts Item --> */}
<div className='flex w-full border-l-6 border-[#F87171] bg-[#F87171] bg-opacity-[15%] px-7 py-8 shadow-md dark:bg-[#1B1B24] dark:bg-opacity-30 md:p-9'>
<div className='mr-5 flex h-9 w-full max-w-[36px] items-center justify-center rounded-lg bg-[#F87171]'>
<svg width='13' height='13' viewBox='0 0 13 13' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M6.4917 7.65579L11.106 12.2645C11.2545 12.4128 11.4715 12.5 11.6738 12.5C11.8762 12.5 12.0931 12.4128 12.2416 12.2645C12.5621 11.9445 12.5623 11.4317 12.2423 11.1114C12.2422 11.1113 12.2422 11.1113 12.2422 11.1113C12.242 11.1111 12.2418 11.1109 12.2416 11.1107L7.64539 6.50351L12.2589 1.91221L12.2595 1.91158C12.5802 1.59132 12.5802 1.07805 12.2595 0.757793C11.9393 0.437994 11.4268 0.437869 11.1064 0.757418C11.1063 0.757543 11.1062 0.757668 11.106 0.757793L6.49234 5.34931L1.89459 0.740581L1.89396 0.739942C1.57364 0.420019 1.0608 0.420019 0.740487 0.739944C0.42005 1.05999 0.419837 1.57279 0.73985 1.89309L6.4917 7.65579ZM6.4917 7.65579L1.89459 12.2639L1.89395 12.2645C1.74546 12.4128 1.52854 12.5 1.32616 12.5C1.12377 12.5 0.906853 12.4128 0.758361 12.2645L1.1117 11.9108L0.758358 12.2645C0.437984 11.9445 0.437708 11.4319 0.757539 11.1116C0.757812 11.1113 0.758086 11.111 0.75836 11.1107L5.33864 6.50287L0.740487 1.89373L6.4917 7.65579Z'
fill='#ffffff'
stroke='#ffffff'
></path>
</svg>
</div>
<div className='w-full'>
<h5 className='mb-3 font-semibold text-[#B45454]'>There were 1 errors with your submission</h5>
<ul>
<li className='leading-relaxed text-[#CD5D5D]'>Lorem Ipsum is simply dummy text of the printing</li>
</ul>
</div>
</div>
</div>
</div>
</DefaultLayout>
);
};
export default Alerts;

View File

@@ -1,5 +1,6 @@
import { Heart, Plus, Trash2 } from 'lucide-react';
import { type AuthUser } from 'wasp/auth';
import { Link } from 'react-router-dom';
import { Button } from '../../../components/ui/button';
import Breadcrumb from '../../layout/Breadcrumb';
import DefaultLayout from '../../layout/DefaultLayout';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
@@ -11,456 +12,62 @@ const Buttons = ({ user }: { user: AuthUser }) => {
<DefaultLayout user={user}>
<Breadcrumb pageName='Buttons' />
{/* <!-- Normal Button Items --> */}
<div className='mb-10 rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke px-7 py-4 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Normal Button</h3>
{/* Button Variants */}
<div className='mb-10 rounded-sm border border-border bg-card shadow-default'>
<div className='border-b border-border px-7 py-4'>
<h3 className='font-medium text-foreground'>Button Variants</h3>
</div>
<div className='p-4 md:p-6 xl:p-9'>
<div className='mb-7.5 flex flex-wrap gap-5 xl:gap-20'>
<Link
to='#'
className='inline-flex items-center justify-center bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
<Link
to='#'
className='inline-flex items-center justify-center rounded-md bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
<Link
to='#'
className='inline-flex items-center justify-center rounded-full bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
<Link
to='#'
className='inline-flex items-center justify-center rounded-md border border-primary py-4 px-10 text-center font-medium text-primary hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
</div>
<div className='mb-7.5 flex flex-wrap gap-5 xl:gap-20'>
<Link
to='#'
className='inline-flex items-center justify-center bg-meta-3 py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
<Link
to='#'
className='inline-flex items-center justify-center rounded-md bg-meta-3 py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
<Link
to='#'
className='inline-flex items-center justify-center rounded-full bg-meta-3 py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
<Link
to='#'
className='inline-flex items-center justify-center rounded-md border border-meta-3 py-4 px-10 text-center font-medium text-meta-3 hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
</div>
<div className='flex flex-wrap gap-5 xl:gap-20'>
<Link
to='#'
className='inline-flex items-center justify-center bg-black py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
<Link
to='#'
className='inline-flex items-center justify-center rounded-md bg-black py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
<Link
to='#'
className='inline-flex items-center justify-center rounded-full bg-black py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
<Link
to='#'
className='inline-flex items-center justify-center rounded-md border border-black py-4 px-10 text-center font-medium text-black hover:bg-opacity-90 lg:px-8 xl:px-10'
>
Button
</Link>
<div className='flex flex-wrap gap-4'>
<Button variant='default'>Default</Button>
<Button variant='outline'>Outline</Button>
<Button variant='secondary'>Secondary</Button>
<Button variant='ghost'>Ghost</Button>
<Button variant='link'>Link</Button>
<Button variant='destructive'>Destructive</Button>
</div>
</div>
</div>
{/* <!-- Button With Icon Items --> */}
<div className='mb-10 rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
<div className='border-b border-stroke px-7 py-4 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Button With Icon</h3>
{/* Button Sizes */}
<div className='mb-10 rounded-sm border border-border bg-card shadow-default'>
<div className='border-b border-border px-7 py-4'>
<h3 className='font-medium text-foreground'>Button Sizes</h3>
</div>
<div className='p-4 md:p-6 xl:p-9'>
<div className='mb-7.5 flex flex-wrap gap-5 xl:gap-7.5'>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M17.8125 16.6656H2.1875C1.69022 16.6656 1.21331 16.4681 0.861675 16.1164C0.510044 15.7648 0.3125 15.2879 0.3125 14.7906V5.20935C0.3125 4.71207 0.510044 4.23516 0.861675 3.88353C1.21331 3.53189 1.69022 3.33435 2.1875 3.33435H17.8125C18.3098 3.33435 18.7867 3.53189 19.1383 3.88353C19.49 4.23516 19.6875 4.71207 19.6875 5.20935V14.7906C19.6875 15.2879 19.49 15.7648 19.1383 16.1164C18.7867 16.4681 18.3098 16.6656 17.8125 16.6656ZM2.1875 4.58435C2.02174 4.58435 1.86277 4.6502 1.74556 4.76741C1.62835 4.88462 1.5625 5.04359 1.5625 5.20935V14.7906C1.5625 14.9564 1.62835 15.1153 1.74556 15.2325C1.86277 15.3498 2.02174 15.4156 2.1875 15.4156H17.8125C17.9783 15.4156 18.1372 15.3498 18.2544 15.2325C18.3717 15.1153 18.4375 14.9564 18.4375 14.7906V5.20935C18.4375 5.04359 18.3717 4.88462 18.2544 4.76741C18.1372 4.6502 17.9783 4.58435 17.8125 4.58435H2.1875Z'
fill=''
/>
<path
d='M9.9996 10.6438C9.63227 10.6437 9.2721 10.5421 8.95898 10.35L0.887102 5.45001C0.744548 5.36381 0.642073 5.22452 0.602222 5.06277C0.58249 4.98268 0.578725 4.89948 0.591144 4.81794C0.603563 4.73639 0.631922 4.65809 0.674602 4.58751C0.717281 4.51692 0.773446 4.45543 0.839888 4.40655C0.906331 4.35767 0.981751 4.32236 1.06184 4.30263C1.22359 4.26277 1.39455 4.28881 1.5371 4.37501L9.60898 9.28126C9.7271 9.35331 9.8628 9.39143 10.0012 9.39143C10.1395 9.39143 10.2752 9.35331 10.3934 9.28126L18.4621 4.37501C18.5323 4.33233 18.6102 4.30389 18.6913 4.29131C18.7725 4.27873 18.8554 4.28227 18.9352 4.30171C19.015 4.32115 19.0901 4.35612 19.1564 4.40462C19.2227 4.45312 19.2788 4.51421 19.3215 4.58438C19.3642 4.65456 19.3926 4.73245 19.4052 4.81362C19.4177 4.89478 19.4142 4.97763 19.3948 5.05743C19.3753 5.13723 19.3404 5.21242 19.2919 5.27871C19.2434 5.34499 19.1823 5.40108 19.1121 5.44376L11.0402 10.35C10.7271 10.5421 10.3669 10.6437 9.9996 10.6438Z'
fill=''
/>
</svg>
</span>
Button With Icon
</Link>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 bg-meta-3 py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M17.8125 16.6656H2.1875C1.69022 16.6656 1.21331 16.4681 0.861675 16.1164C0.510044 15.7648 0.3125 15.2879 0.3125 14.7906V5.20935C0.3125 4.71207 0.510044 4.23516 0.861675 3.88353C1.21331 3.53189 1.69022 3.33435 2.1875 3.33435H17.8125C18.3098 3.33435 18.7867 3.53189 19.1383 3.88353C19.49 4.23516 19.6875 4.71207 19.6875 5.20935V14.7906C19.6875 15.2879 19.49 15.7648 19.1383 16.1164C18.7867 16.4681 18.3098 16.6656 17.8125 16.6656ZM2.1875 4.58435C2.02174 4.58435 1.86277 4.6502 1.74556 4.76741C1.62835 4.88462 1.5625 5.04359 1.5625 5.20935V14.7906C1.5625 14.9564 1.62835 15.1153 1.74556 15.2325C1.86277 15.3498 2.02174 15.4156 2.1875 15.4156H17.8125C17.9783 15.4156 18.1372 15.3498 18.2544 15.2325C18.3717 15.1153 18.4375 14.9564 18.4375 14.7906V5.20935C18.4375 5.04359 18.3717 4.88462 18.2544 4.76741C18.1372 4.6502 17.9783 4.58435 17.8125 4.58435H2.1875Z'
fill=''
/>
<path
d='M9.9996 10.6438C9.63227 10.6437 9.2721 10.5421 8.95898 10.35L0.887102 5.45001C0.744548 5.36381 0.642073 5.22452 0.602222 5.06277C0.58249 4.98268 0.578725 4.89948 0.591144 4.81794C0.603563 4.73639 0.631922 4.65809 0.674602 4.58751C0.717281 4.51692 0.773446 4.45543 0.839888 4.40655C0.906331 4.35767 0.981751 4.32236 1.06184 4.30263C1.22359 4.26277 1.39455 4.28881 1.5371 4.37501L9.60898 9.28126C9.7271 9.35331 9.8628 9.39143 10.0012 9.39143C10.1395 9.39143 10.2752 9.35331 10.3934 9.28126L18.4621 4.37501C18.5323 4.33233 18.6102 4.30389 18.6913 4.29131C18.7725 4.27873 18.8554 4.28227 18.9352 4.30171C19.015 4.32115 19.0901 4.35612 19.1564 4.40462C19.2227 4.45312 19.2788 4.51421 19.3215 4.58438C19.3642 4.65456 19.3926 4.73245 19.4052 4.81362C19.4177 4.89478 19.4142 4.97763 19.3948 5.05743C19.3753 5.13723 19.3404 5.21242 19.2919 5.27871C19.2434 5.34499 19.1823 5.40108 19.1121 5.44376L11.0402 10.35C10.7271 10.5421 10.3669 10.6437 9.9996 10.6438Z'
fill=''
/>
</svg>
</span>
Button With Icon
</Link>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 bg-black py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M17.8125 16.6656H2.1875C1.69022 16.6656 1.21331 16.4681 0.861675 16.1164C0.510044 15.7648 0.3125 15.2879 0.3125 14.7906V5.20935C0.3125 4.71207 0.510044 4.23516 0.861675 3.88353C1.21331 3.53189 1.69022 3.33435 2.1875 3.33435H17.8125C18.3098 3.33435 18.7867 3.53189 19.1383 3.88353C19.49 4.23516 19.6875 4.71207 19.6875 5.20935V14.7906C19.6875 15.2879 19.49 15.7648 19.1383 16.1164C18.7867 16.4681 18.3098 16.6656 17.8125 16.6656ZM2.1875 4.58435C2.02174 4.58435 1.86277 4.6502 1.74556 4.76741C1.62835 4.88462 1.5625 5.04359 1.5625 5.20935V14.7906C1.5625 14.9564 1.62835 15.1153 1.74556 15.2325C1.86277 15.3498 2.02174 15.4156 2.1875 15.4156H17.8125C17.9783 15.4156 18.1372 15.3498 18.2544 15.2325C18.3717 15.1153 18.4375 14.9564 18.4375 14.7906V5.20935C18.4375 5.04359 18.3717 4.88462 18.2544 4.76741C18.1372 4.6502 17.9783 4.58435 17.8125 4.58435H2.1875Z'
fill=''
/>
<path
d='M9.9996 10.6438C9.63227 10.6437 9.2721 10.5421 8.95898 10.35L0.887102 5.45001C0.744548 5.36381 0.642073 5.22452 0.602222 5.06277C0.58249 4.98268 0.578725 4.89948 0.591144 4.81794C0.603563 4.73639 0.631922 4.65809 0.674602 4.58751C0.717281 4.51692 0.773446 4.45543 0.839888 4.40655C0.906331 4.35767 0.981751 4.32236 1.06184 4.30263C1.22359 4.26277 1.39455 4.28881 1.5371 4.37501L9.60898 9.28126C9.7271 9.35331 9.8628 9.39143 10.0012 9.39143C10.1395 9.39143 10.2752 9.35331 10.3934 9.28126L18.4621 4.37501C18.5323 4.33233 18.6102 4.30389 18.6913 4.29131C18.7725 4.27873 18.8554 4.28227 18.9352 4.30171C19.015 4.32115 19.0901 4.35612 19.1564 4.40462C19.2227 4.45312 19.2788 4.51421 19.3215 4.58438C19.3642 4.65456 19.3926 4.73245 19.4052 4.81362C19.4177 4.89478 19.4142 4.97763 19.3948 5.05743C19.3753 5.13723 19.3404 5.21242 19.2919 5.27871C19.2434 5.34499 19.1823 5.40108 19.1121 5.44376L11.0402 10.35C10.7271 10.5421 10.3669 10.6437 9.9996 10.6438Z'
fill=''
/>
</svg>
</span>
Button With Icon
</Link>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 border border-primary py-4 px-10 text-center font-medium text-primary hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M17.8125 16.6656H2.1875C1.69022 16.6656 1.21331 16.4681 0.861675 16.1164C0.510044 15.7648 0.3125 15.2879 0.3125 14.7906V5.20935C0.3125 4.71207 0.510044 4.23516 0.861675 3.88353C1.21331 3.53189 1.69022 3.33435 2.1875 3.33435H17.8125C18.3098 3.33435 18.7867 3.53189 19.1383 3.88353C19.49 4.23516 19.6875 4.71207 19.6875 5.20935V14.7906C19.6875 15.2879 19.49 15.7648 19.1383 16.1164C18.7867 16.4681 18.3098 16.6656 17.8125 16.6656ZM2.1875 4.58435C2.02174 4.58435 1.86277 4.6502 1.74556 4.76741C1.62835 4.88462 1.5625 5.04359 1.5625 5.20935V14.7906C1.5625 14.9564 1.62835 15.1153 1.74556 15.2325C1.86277 15.3498 2.02174 15.4156 2.1875 15.4156H17.8125C17.9783 15.4156 18.1372 15.3498 18.2544 15.2325C18.3717 15.1153 18.4375 14.9564 18.4375 14.7906V5.20935C18.4375 5.04359 18.3717 4.88462 18.2544 4.76741C18.1372 4.6502 17.9783 4.58435 17.8125 4.58435H2.1875Z'
fill=''
/>
<path
d='M9.9996 10.6438C9.63227 10.6437 9.2721 10.5421 8.95898 10.35L0.887102 5.45001C0.744548 5.36381 0.642073 5.22452 0.602222 5.06277C0.58249 4.98268 0.578725 4.89948 0.591144 4.81794C0.603563 4.73639 0.631922 4.65809 0.674602 4.58751C0.717281 4.51692 0.773446 4.45543 0.839888 4.40655C0.906331 4.35767 0.981751 4.32236 1.06184 4.30263C1.22359 4.26277 1.39455 4.28881 1.5371 4.37501L9.60898 9.28126C9.7271 9.35331 9.8628 9.39143 10.0012 9.39143C10.1395 9.39143 10.2752 9.35331 10.3934 9.28126L18.4621 4.37501C18.5323 4.33233 18.6102 4.30389 18.6913 4.29131C18.7725 4.27873 18.8554 4.28227 18.9352 4.30171C19.015 4.32115 19.0901 4.35612 19.1564 4.40462C19.2227 4.45312 19.2788 4.51421 19.3215 4.58438C19.3642 4.65456 19.3926 4.73245 19.4052 4.81362C19.4177 4.89478 19.4142 4.97763 19.3948 5.05743C19.3753 5.13723 19.3404 5.21242 19.2919 5.27871C19.2434 5.34499 19.1823 5.40108 19.1121 5.44376L11.0402 10.35C10.7271 10.5421 10.3669 10.6437 9.9996 10.6438Z'
fill=''
/>
</svg>
</span>
Button With Icon
</Link>
<div className='flex flex-wrap items-center gap-4'>
<Button size='sm'>Small</Button>
<Button size='default'>Default</Button>
<Button size='lg'>Large</Button>
<Button size='icon'>
<Plus />
</Button>
</div>
</div>
</div>
<div className='mb-7.5 flex flex-wrap gap-5 xl:gap-7.5'>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 rounded-md bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M18.0758 0.849976H16.0695C15.819 0.851233 15.5774 0.942521 15.3886 1.10717C15.1999 1.27183 15.0766 1.49887 15.0414 1.74685L14.4789 5.80935H13.3976V3.4031C13.3952 3.1654 13.3002 2.93802 13.1327 2.76935C12.9652 2.60068 12.7384 2.50403 12.5008 2.49998H10.082C10.0553 2.27763 9.94981 2.07221 9.78472 1.92089C9.61964 1.76956 9.40584 1.68233 9.18202 1.67498H6.45389C6.32885 1.67815 6.20571 1.70632 6.09172 1.75782C5.97773 1.80932 5.8752 1.8831 5.79017 1.97484C5.70513 2.06657 5.63932 2.17439 5.59659 2.29195C5.55387 2.40951 5.5351 2.53443 5.54139 2.65935V3.32498H3.15077C2.91396 3.32162 2.68544 3.41207 2.51507 3.57659C2.3447 3.7411 2.24632 3.96632 2.24139 4.2031V5.81248C2.0999 5.81539 1.96078 5.84937 1.83387 5.91201C1.70697 5.97466 1.59538 6.06443 1.50702 6.17498C1.41616 6.29094 1.35267 6.42593 1.32128 6.56986C1.2899 6.7138 1.29143 6.86297 1.32577 7.00623C1.32443 7.02182 1.32443 7.0375 1.32577 7.0531L3.23827 12.9375C3.29323 13.1432 3.4153 13.3247 3.58513 13.4532C3.75496 13.5818 3.96282 13.6499 4.17577 13.6468H13.3883C13.7379 13.6464 14.0756 13.5197 14.3391 13.29C14.6027 13.0603 14.7744 12.7431 14.8226 12.3968L16.2508 2.09998H18.0726C18.2384 2.09998 18.3974 2.03413 18.5146 1.91692C18.6318 1.79971 18.6976 1.64074 18.6976 1.47498C18.6976 1.30922 18.6318 1.15024 18.5146 1.03303C18.3974 0.915824 18.2384 0.849976 18.0726 0.849976H18.0758ZM12.1383 5.79373H10.0945V3.74998H12.1476L12.1383 5.79373ZM6.79139 2.9156H8.84452V3.39998V5.7906H6.79139V2.9156ZM3.49139 4.5656H5.54139V5.79373H3.49139V4.5656ZM13.5851 12.225C13.579 12.2727 13.5556 12.3166 13.5193 12.3483C13.4831 12.38 13.4364 12.3972 13.3883 12.3968H4.37577L2.65389 7.04998H14.3039L13.5851 12.225Z'
fill=''
/>
<path
d='M5.31172 15.1125C4.9118 15.1094 4.51997 15.2252 4.18594 15.4451C3.85191 15.665 3.59073 15.9792 3.43553 16.3478C3.28034 16.7164 3.23813 17.1228 3.31425 17.5154C3.39037 17.908 3.58139 18.2692 3.86309 18.5531C4.14478 18.837 4.50445 19.0308 4.89647 19.11C5.28849 19.1891 5.6952 19.1501 6.06499 18.9978C6.43477 18.8454 6.75099 18.5867 6.97351 18.2544C7.19603 17.9221 7.31483 17.5312 7.31485 17.1312C7.31608 16.8671 7.26522 16.6053 7.16518 16.3608C7.06515 16.1164 6.91789 15.894 6.73184 15.7065C6.5458 15.519 6.3246 15.3701 6.08092 15.2681C5.83725 15.1662 5.57586 15.1133 5.31172 15.1125ZM5.31172 17.9C5.15905 17.9031 5.00891 17.8607 4.88045 17.7781C4.75199 17.6955 4.65103 17.5766 4.59045 17.4364C4.52986 17.2962 4.51239 17.1412 4.54026 16.9911C4.56814 16.8409 4.64009 16.7025 4.74695 16.5934C4.85382 16.4843 4.99075 16.4096 5.14028 16.3786C5.28981 16.3477 5.44518 16.3619 5.58656 16.4196C5.72794 16.4773 5.84894 16.5758 5.93412 16.7026C6.0193 16.8293 6.06481 16.9785 6.06484 17.1312C6.06651 17.3329 5.9882 17.5271 5.84705 17.6712C5.70589 17.8152 5.51341 17.8975 5.31172 17.9Z'
fill=''
/>
<path
d='M12.9504 15.1125C12.5505 15.1094 12.1586 15.2252 11.8246 15.4451C11.4906 15.665 11.2294 15.9792 11.0742 16.3478C10.919 16.7164 10.8768 17.1228 10.9529 17.5154C11.029 17.908 11.2201 18.2692 11.5018 18.5531C11.7835 18.837 12.1431 19.0308 12.5351 19.11C12.9272 19.1891 13.3339 19.1501 13.7037 18.9978C14.0734 18.8454 14.3897 18.5867 14.6122 18.2544C14.8347 17.9221 14.9535 17.5312 14.9535 17.1312C14.9552 16.598 14.7452 16.086 14.3696 15.7075C13.994 15.329 13.4836 15.115 12.9504 15.1125ZM12.9504 17.9C12.7977 17.9031 12.6476 17.8607 12.5191 17.7781C12.3907 17.6955 12.2897 17.5766 12.2291 17.4364C12.1685 17.2962 12.1511 17.1412 12.1789 16.9911C12.2068 16.8409 12.2788 16.7025 12.3856 16.5934C12.4925 16.4843 12.6294 16.4096 12.779 16.3786C12.9285 16.3477 13.0838 16.3619 13.2252 16.4196C13.3666 16.4773 13.4876 16.5758 13.5728 16.7026C13.658 16.8293 13.7035 16.9785 13.7035 17.1312C13.7052 17.3329 13.6269 17.5271 13.4857 17.6712C13.3446 17.8152 13.1521 17.8975 12.9504 17.9Z'
fill=''
/>
</svg>
</span>
Button With Icon
</Link>
{/* Button with Icon */}
<div className='mb-10 rounded-sm border border-border bg-card shadow-default'>
<div className='border-b border-border px-7 py-4'>
<h3 className='font-medium text-foreground'>Button with Icon</h3>
</div>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 rounded-md bg-meta-3 py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M18.0758 0.849976H16.0695C15.819 0.851233 15.5774 0.942521 15.3886 1.10717C15.1999 1.27183 15.0766 1.49887 15.0414 1.74685L14.4789 5.80935H13.3976V3.4031C13.3952 3.1654 13.3002 2.93802 13.1327 2.76935C12.9652 2.60068 12.7384 2.50403 12.5008 2.49998H10.082C10.0553 2.27763 9.94981 2.07221 9.78472 1.92089C9.61964 1.76956 9.40584 1.68233 9.18202 1.67498H6.45389C6.32885 1.67815 6.20571 1.70632 6.09172 1.75782C5.97773 1.80932 5.8752 1.8831 5.79017 1.97484C5.70513 2.06657 5.63932 2.17439 5.59659 2.29195C5.55387 2.40951 5.5351 2.53443 5.54139 2.65935V3.32498H3.15077C2.91396 3.32162 2.68544 3.41207 2.51507 3.57659C2.3447 3.7411 2.24632 3.96632 2.24139 4.2031V5.81248C2.0999 5.81539 1.96078 5.84937 1.83387 5.91201C1.70697 5.97466 1.59538 6.06443 1.50702 6.17498C1.41616 6.29094 1.35267 6.42593 1.32128 6.56986C1.2899 6.7138 1.29143 6.86297 1.32577 7.00623C1.32443 7.02182 1.32443 7.0375 1.32577 7.0531L3.23827 12.9375C3.29323 13.1432 3.4153 13.3247 3.58513 13.4532C3.75496 13.5818 3.96282 13.6499 4.17577 13.6468H13.3883C13.7379 13.6464 14.0756 13.5197 14.3391 13.29C14.6027 13.0603 14.7744 12.7431 14.8226 12.3968L16.2508 2.09998H18.0726C18.2384 2.09998 18.3974 2.03413 18.5146 1.91692C18.6318 1.79971 18.6976 1.64074 18.6976 1.47498C18.6976 1.30922 18.6318 1.15024 18.5146 1.03303C18.3974 0.915824 18.2384 0.849976 18.0726 0.849976H18.0758ZM12.1383 5.79373H10.0945V3.74998H12.1476L12.1383 5.79373ZM6.79139 2.9156H8.84452V3.39998V5.7906H6.79139V2.9156ZM3.49139 4.5656H5.54139V5.79373H3.49139V4.5656ZM13.5851 12.225C13.579 12.2727 13.5556 12.3166 13.5193 12.3483C13.4831 12.38 13.4364 12.3972 13.3883 12.3968H4.37577L2.65389 7.04998H14.3039L13.5851 12.225Z'
fill=''
/>
<path
d='M5.31172 15.1125C4.9118 15.1094 4.51997 15.2252 4.18594 15.4451C3.85191 15.665 3.59073 15.9792 3.43553 16.3478C3.28034 16.7164 3.23813 17.1228 3.31425 17.5154C3.39037 17.908 3.58139 18.2692 3.86309 18.5531C4.14478 18.837 4.50445 19.0308 4.89647 19.11C5.28849 19.1891 5.6952 19.1501 6.06499 18.9978C6.43477 18.8454 6.75099 18.5867 6.97351 18.2544C7.19603 17.9221 7.31483 17.5312 7.31485 17.1312C7.31608 16.8671 7.26522 16.6053 7.16518 16.3608C7.06515 16.1164 6.91789 15.894 6.73184 15.7065C6.5458 15.519 6.3246 15.3701 6.08092 15.2681C5.83725 15.1662 5.57586 15.1133 5.31172 15.1125ZM5.31172 17.9C5.15905 17.9031 5.00891 17.8607 4.88045 17.7781C4.75199 17.6955 4.65103 17.5766 4.59045 17.4364C4.52986 17.2962 4.51239 17.1412 4.54026 16.9911C4.56814 16.8409 4.64009 16.7025 4.74695 16.5934C4.85382 16.4843 4.99075 16.4096 5.14028 16.3786C5.28981 16.3477 5.44518 16.3619 5.58656 16.4196C5.72794 16.4773 5.84894 16.5758 5.93412 16.7026C6.0193 16.8293 6.06481 16.9785 6.06484 17.1312C6.06651 17.3329 5.9882 17.5271 5.84705 17.6712C5.70589 17.8152 5.51341 17.8975 5.31172 17.9Z'
fill=''
/>
<path
d='M12.9504 15.1125C12.5505 15.1094 12.1586 15.2252 11.8246 15.4451C11.4906 15.665 11.2294 15.9792 11.0742 16.3478C10.919 16.7164 10.8768 17.1228 10.9529 17.5154C11.029 17.908 11.2201 18.2692 11.5018 18.5531C11.7835 18.837 12.1431 19.0308 12.5351 19.11C12.9272 19.1891 13.3339 19.1501 13.7037 18.9978C14.0734 18.8454 14.3897 18.5867 14.6122 18.2544C14.8347 17.9221 14.9535 17.5312 14.9535 17.1312C14.9552 16.598 14.7452 16.086 14.3696 15.7075C13.994 15.329 13.4836 15.115 12.9504 15.1125ZM12.9504 17.9C12.7977 17.9031 12.6476 17.8607 12.5191 17.7781C12.3907 17.6955 12.2897 17.5766 12.2291 17.4364C12.1685 17.2962 12.1511 17.1412 12.1789 16.9911C12.2068 16.8409 12.2788 16.7025 12.3856 16.5934C12.4925 16.4843 12.6294 16.4096 12.779 16.3786C12.9285 16.3477 13.0838 16.3619 13.2252 16.4196C13.3666 16.4773 13.4876 16.5758 13.5728 16.7026C13.658 16.8293 13.7035 16.9785 13.7035 17.1312C13.7052 17.3329 13.6269 17.5271 13.4857 17.6712C13.3446 17.8152 13.1521 17.8975 12.9504 17.9Z'
fill=''
/>
</svg>
</span>
Button With Icon
</Link>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 rounded-md bg-black py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M18.0758 0.849976H16.0695C15.819 0.851233 15.5774 0.942521 15.3886 1.10717C15.1999 1.27183 15.0766 1.49887 15.0414 1.74685L14.4789 5.80935H13.3976V3.4031C13.3952 3.1654 13.3002 2.93802 13.1327 2.76935C12.9652 2.60068 12.7384 2.50403 12.5008 2.49998H10.082C10.0553 2.27763 9.94981 2.07221 9.78472 1.92089C9.61964 1.76956 9.40584 1.68233 9.18202 1.67498H6.45389C6.32885 1.67815 6.20571 1.70632 6.09172 1.75782C5.97773 1.80932 5.8752 1.8831 5.79017 1.97484C5.70513 2.06657 5.63932 2.17439 5.59659 2.29195C5.55387 2.40951 5.5351 2.53443 5.54139 2.65935V3.32498H3.15077C2.91396 3.32162 2.68544 3.41207 2.51507 3.57659C2.3447 3.7411 2.24632 3.96632 2.24139 4.2031V5.81248C2.0999 5.81539 1.96078 5.84937 1.83387 5.91201C1.70697 5.97466 1.59538 6.06443 1.50702 6.17498C1.41616 6.29094 1.35267 6.42593 1.32128 6.56986C1.2899 6.7138 1.29143 6.86297 1.32577 7.00623C1.32443 7.02182 1.32443 7.0375 1.32577 7.0531L3.23827 12.9375C3.29323 13.1432 3.4153 13.3247 3.58513 13.4532C3.75496 13.5818 3.96282 13.6499 4.17577 13.6468H13.3883C13.7379 13.6464 14.0756 13.5197 14.3391 13.29C14.6027 13.0603 14.7744 12.7431 14.8226 12.3968L16.2508 2.09998H18.0726C18.2384 2.09998 18.3974 2.03413 18.5146 1.91692C18.6318 1.79971 18.6976 1.64074 18.6976 1.47498C18.6976 1.30922 18.6318 1.15024 18.5146 1.03303C18.3974 0.915824 18.2384 0.849976 18.0726 0.849976H18.0758ZM12.1383 5.79373H10.0945V3.74998H12.1476L12.1383 5.79373ZM6.79139 2.9156H8.84452V3.39998V5.7906H6.79139V2.9156ZM3.49139 4.5656H5.54139V5.79373H3.49139V4.5656ZM13.5851 12.225C13.579 12.2727 13.5556 12.3166 13.5193 12.3483C13.4831 12.38 13.4364 12.3972 13.3883 12.3968H4.37577L2.65389 7.04998H14.3039L13.5851 12.225Z'
fill=''
/>
<path
d='M5.31172 15.1125C4.9118 15.1094 4.51997 15.2252 4.18594 15.4451C3.85191 15.665 3.59073 15.9792 3.43553 16.3478C3.28034 16.7164 3.23813 17.1228 3.31425 17.5154C3.39037 17.908 3.58139 18.2692 3.86309 18.5531C4.14478 18.837 4.50445 19.0308 4.89647 19.11C5.28849 19.1891 5.6952 19.1501 6.06499 18.9978C6.43477 18.8454 6.75099 18.5867 6.97351 18.2544C7.19603 17.9221 7.31483 17.5312 7.31485 17.1312C7.31608 16.8671 7.26522 16.6053 7.16518 16.3608C7.06515 16.1164 6.91789 15.894 6.73184 15.7065C6.5458 15.519 6.3246 15.3701 6.08092 15.2681C5.83725 15.1662 5.57586 15.1133 5.31172 15.1125ZM5.31172 17.9C5.15905 17.9031 5.00891 17.8607 4.88045 17.7781C4.75199 17.6955 4.65103 17.5766 4.59045 17.4364C4.52986 17.2962 4.51239 17.1412 4.54026 16.9911C4.56814 16.8409 4.64009 16.7025 4.74695 16.5934C4.85382 16.4843 4.99075 16.4096 5.14028 16.3786C5.28981 16.3477 5.44518 16.3619 5.58656 16.4196C5.72794 16.4773 5.84894 16.5758 5.93412 16.7026C6.0193 16.8293 6.06481 16.9785 6.06484 17.1312C6.06651 17.3329 5.9882 17.5271 5.84705 17.6712C5.70589 17.8152 5.51341 17.8975 5.31172 17.9Z'
fill=''
/>
<path
d='M12.9504 15.1125C12.5505 15.1094 12.1586 15.2252 11.8246 15.4451C11.4906 15.665 11.2294 15.9792 11.0742 16.3478C10.919 16.7164 10.8768 17.1228 10.9529 17.5154C11.029 17.908 11.2201 18.2692 11.5018 18.5531C11.7835 18.837 12.1431 19.0308 12.5351 19.11C12.9272 19.1891 13.3339 19.1501 13.7037 18.9978C14.0734 18.8454 14.3897 18.5867 14.6122 18.2544C14.8347 17.9221 14.9535 17.5312 14.9535 17.1312C14.9552 16.598 14.7452 16.086 14.3696 15.7075C13.994 15.329 13.4836 15.115 12.9504 15.1125ZM12.9504 17.9C12.7977 17.9031 12.6476 17.8607 12.5191 17.7781C12.3907 17.6955 12.2897 17.5766 12.2291 17.4364C12.1685 17.2962 12.1511 17.1412 12.1789 16.9911C12.2068 16.8409 12.2788 16.7025 12.3856 16.5934C12.4925 16.4843 12.6294 16.4096 12.779 16.3786C12.9285 16.3477 13.0838 16.3619 13.2252 16.4196C13.3666 16.4773 13.4876 16.5758 13.5728 16.7026C13.658 16.8293 13.7035 16.9785 13.7035 17.1312C13.7052 17.3329 13.6269 17.5271 13.4857 17.6712C13.3446 17.8152 13.1521 17.8975 12.9504 17.9Z'
fill=''
/>
</svg>
</span>
Button With Icon
</Link>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 rounded-md border border-primary py-4 px-10 text-center font-medium text-primary hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M18.0758 0.849976H16.0695C15.819 0.851233 15.5774 0.942521 15.3886 1.10717C15.1999 1.27183 15.0766 1.49887 15.0414 1.74685L14.4789 5.80935H13.3976V3.4031C13.3952 3.1654 13.3002 2.93802 13.1327 2.76935C12.9652 2.60068 12.7384 2.50403 12.5008 2.49998H10.082C10.0553 2.27763 9.94981 2.07221 9.78472 1.92089C9.61964 1.76956 9.40584 1.68233 9.18202 1.67498H6.45389C6.32885 1.67815 6.20571 1.70632 6.09172 1.75782C5.97773 1.80932 5.8752 1.8831 5.79017 1.97484C5.70513 2.06657 5.63932 2.17439 5.59659 2.29195C5.55387 2.40951 5.5351 2.53443 5.54139 2.65935V3.32498H3.15077C2.91396 3.32162 2.68544 3.41207 2.51507 3.57659C2.3447 3.7411 2.24632 3.96632 2.24139 4.2031V5.81248C2.0999 5.81539 1.96078 5.84937 1.83387 5.91201C1.70697 5.97466 1.59538 6.06443 1.50702 6.17498C1.41616 6.29094 1.35267 6.42593 1.32128 6.56986C1.2899 6.7138 1.29143 6.86297 1.32577 7.00623C1.32443 7.02182 1.32443 7.0375 1.32577 7.0531L3.23827 12.9375C3.29323 13.1432 3.4153 13.3247 3.58513 13.4532C3.75496 13.5818 3.96282 13.6499 4.17577 13.6468H13.3883C13.7379 13.6464 14.0756 13.5197 14.3391 13.29C14.6027 13.0603 14.7744 12.7431 14.8226 12.3968L16.2508 2.09998H18.0726C18.2384 2.09998 18.3974 2.03413 18.5146 1.91692C18.6318 1.79971 18.6976 1.64074 18.6976 1.47498C18.6976 1.30922 18.6318 1.15024 18.5146 1.03303C18.3974 0.915824 18.2384 0.849976 18.0726 0.849976H18.0758ZM12.1383 5.79373H10.0945V3.74998H12.1476L12.1383 5.79373ZM6.79139 2.9156H8.84452V3.39998V5.7906H6.79139V2.9156ZM3.49139 4.5656H5.54139V5.79373H3.49139V4.5656ZM13.5851 12.225C13.579 12.2727 13.5556 12.3166 13.5193 12.3483C13.4831 12.38 13.4364 12.3972 13.3883 12.3968H4.37577L2.65389 7.04998H14.3039L13.5851 12.225Z'
fill=''
/>
<path
d='M5.31172 15.1125C4.9118 15.1094 4.51997 15.2252 4.18594 15.4451C3.85191 15.665 3.59073 15.9792 3.43553 16.3478C3.28034 16.7164 3.23813 17.1228 3.31425 17.5154C3.39037 17.908 3.58139 18.2692 3.86309 18.5531C4.14478 18.837 4.50445 19.0308 4.89647 19.11C5.28849 19.1891 5.6952 19.1501 6.06499 18.9978C6.43477 18.8454 6.75099 18.5867 6.97351 18.2544C7.19603 17.9221 7.31483 17.5312 7.31485 17.1312C7.31608 16.8671 7.26522 16.6053 7.16518 16.3608C7.06515 16.1164 6.91789 15.894 6.73184 15.7065C6.5458 15.519 6.3246 15.3701 6.08092 15.2681C5.83725 15.1662 5.57586 15.1133 5.31172 15.1125ZM5.31172 17.9C5.15905 17.9031 5.00891 17.8607 4.88045 17.7781C4.75199 17.6955 4.65103 17.5766 4.59045 17.4364C4.52986 17.2962 4.51239 17.1412 4.54026 16.9911C4.56814 16.8409 4.64009 16.7025 4.74695 16.5934C4.85382 16.4843 4.99075 16.4096 5.14028 16.3786C5.28981 16.3477 5.44518 16.3619 5.58656 16.4196C5.72794 16.4773 5.84894 16.5758 5.93412 16.7026C6.0193 16.8293 6.06481 16.9785 6.06484 17.1312C6.06651 17.3329 5.9882 17.5271 5.84705 17.6712C5.70589 17.8152 5.51341 17.8975 5.31172 17.9Z'
fill=''
/>
<path
d='M12.9504 15.1125C12.5505 15.1094 12.1586 15.2252 11.8246 15.4451C11.4906 15.665 11.2294 15.9792 11.0742 16.3478C10.919 16.7164 10.8768 17.1228 10.9529 17.5154C11.029 17.908 11.2201 18.2692 11.5018 18.5531C11.7835 18.837 12.1431 19.0308 12.5351 19.11C12.9272 19.1891 13.3339 19.1501 13.7037 18.9978C14.0734 18.8454 14.3897 18.5867 14.6122 18.2544C14.8347 17.9221 14.9535 17.5312 14.9535 17.1312C14.9552 16.598 14.7452 16.086 14.3696 15.7075C13.994 15.329 13.4836 15.115 12.9504 15.1125ZM12.9504 17.9C12.7977 17.9031 12.6476 17.8607 12.5191 17.7781C12.3907 17.6955 12.2897 17.5766 12.2291 17.4364C12.1685 17.2962 12.1511 17.1412 12.1789 16.9911C12.2068 16.8409 12.2788 16.7025 12.3856 16.5934C12.4925 16.4843 12.6294 16.4096 12.779 16.3786C12.9285 16.3477 13.0838 16.3619 13.2252 16.4196C13.3666 16.4773 13.4876 16.5758 13.5728 16.7026C13.658 16.8293 13.7035 16.9785 13.7035 17.1312C13.7052 17.3329 13.6269 17.5271 13.4857 17.6712C13.3446 17.8152 13.1521 17.8975 12.9504 17.9Z'
fill=''
/>
</svg>
</span>
Button With Icon
</Link>
</div>
<div className='flex flex-wrap gap-5 xl:gap-7.5'>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 rounded-full bg-primary py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g clipPath='url(#clip0_182_46495)'>
<path
d='M18.875 11.4375C18.3125 10.8438 17.5625 10.5312 16.75 10.5312C16.125 10.5312 15.5625 10.7188 15.0625 11.0938C15 11.125 14.9688 11.1562 14.9062 11.2188C14.8438 11.1875 14.8125 11.125 14.75 11.0938C14.25 10.7188 13.6875 10.5312 13.0625 10.5312C12.9062 10.5312 12.7812 10.5312 12.6562 10.5625C11.7188 9.5 10.5625 8.75 9.3125 8.40625C10.625 7.75 11.5312 6.40625 11.5312 4.875C11.5312 2.6875 9.75 0.9375 7.59375 0.9375C5.40625 0.9375 3.65625 2.71875 3.65625 4.875C3.65625 6.4375 4.5625 7.78125 5.875 8.40625C4.5625 8.78125 3.40625 9.53125 2.4375 10.6562C1.125 12.2188 0.375 14.4062 0.3125 16.7812C0.3125 17.0312 0.4375 17.25 0.65625 17.3438C1.5 17.75 4.4375 19.0938 7.59375 19.0938C9.28125 19.0938 10.8438 18.8125 10.9062 18.8125C11.25 18.75 11.4688 18.4375 11.4062 18.0938C11.3438 17.75 11.0312 17.5312 10.6875 17.5938C10.6875 17.5938 9.15625 17.875 7.59375 17.875C5.0625 17.8438 2.65625 16.875 1.5625 16.375C1.65625 14.4375 2.3125 12.7187 3.375 11.4375C4.46875 10.125 5.96875 9.40625 7.59375 9.40625C9.03125 9.40625 10.375 10 11.4375 11.0312C11.2812 11.1562 11.125 11.2812 11 11.4062C10.4688 11.9688 10.1875 12.75 10.1875 13.5938C10.1875 14.4375 10.5 15.2188 11.1562 16C11.6875 16.6562 12.4375 17.2812 13.2812 18L13.3125 18.0312C13.5937 18.25 13.9062 18.5312 14.2188 18.8125C14.4062 19 14.6875 19.0938 14.9375 19.0938C15.1875 19.0938 15.4687 19 15.6562 18.8125C16 18.5312 16.3125 18.25 16.5938 18C17.4375 17.2812 18.1875 16.6562 18.7188 16C19.375 15.2188 19.6875 14.4375 19.6875 13.5938C19.6875 12.7812 19.4062 12.0312 18.875 11.4375ZM4.875 4.875C4.875 3.375 6.09375 2.1875 7.5625 2.1875C9.0625 2.1875 10.25 3.40625 10.25 4.875C10.25 6.375 9.03125 7.5625 7.5625 7.5625C6.09375 7.5625 4.875 6.34375 4.875 4.875ZM17.75 15.2188C17.2812 15.7812 16.5938 16.375 15.7812 17.0625C15.5312 17.2812 15.2188 17.5312 14.9062 17.7812C14.625 17.5312 14.3438 17.2812 14.0938 17.0938L14.0625 17.0625C13.25 16.375 12.5625 15.7812 12.0938 15.2188C11.625 14.6562 11.4062 14.1562 11.4062 13.625C11.4062 13.0937 11.5938 12.625 11.9062 12.2812C12.2188 11.9375 12.6563 11.75 13.0938 11.75C13.4375 11.75 13.75 11.8438 14 12.0625C14.125 12.1562 14.2188 12.25 14.3125 12.375C14.5938 12.7188 15.1875 12.7188 15.5 12.375C15.5938 12.25 15.7187 12.1562 15.8125 12.0625C16.0937 11.8438 16.4062 11.75 16.7188 11.75C17.1875 11.75 17.5938 11.9375 17.9062 12.2812C18.2188 12.625 18.4062 13.0937 18.4062 13.625C18.4375 14.1875 18.2188 14.6562 17.75 15.2188Z'
fill=''
/>
</g>
<defs>
<clipPath id='clip0_182_46495'>
<rect width='20' height='20' fill='white' />
</clipPath>
</defs>
</svg>
</span>
Button With Icon
</Link>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 rounded-full bg-meta-3 py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g clipPath='url(#clip0_182_46495)'>
<path
d='M18.875 11.4375C18.3125 10.8438 17.5625 10.5312 16.75 10.5312C16.125 10.5312 15.5625 10.7188 15.0625 11.0938C15 11.125 14.9688 11.1562 14.9062 11.2188C14.8438 11.1875 14.8125 11.125 14.75 11.0938C14.25 10.7188 13.6875 10.5312 13.0625 10.5312C12.9062 10.5312 12.7812 10.5312 12.6562 10.5625C11.7188 9.5 10.5625 8.75 9.3125 8.40625C10.625 7.75 11.5312 6.40625 11.5312 4.875C11.5312 2.6875 9.75 0.9375 7.59375 0.9375C5.40625 0.9375 3.65625 2.71875 3.65625 4.875C3.65625 6.4375 4.5625 7.78125 5.875 8.40625C4.5625 8.78125 3.40625 9.53125 2.4375 10.6562C1.125 12.2188 0.375 14.4062 0.3125 16.7812C0.3125 17.0312 0.4375 17.25 0.65625 17.3438C1.5 17.75 4.4375 19.0938 7.59375 19.0938C9.28125 19.0938 10.8438 18.8125 10.9062 18.8125C11.25 18.75 11.4688 18.4375 11.4062 18.0938C11.3438 17.75 11.0312 17.5312 10.6875 17.5938C10.6875 17.5938 9.15625 17.875 7.59375 17.875C5.0625 17.8438 2.65625 16.875 1.5625 16.375C1.65625 14.4375 2.3125 12.7187 3.375 11.4375C4.46875 10.125 5.96875 9.40625 7.59375 9.40625C9.03125 9.40625 10.375 10 11.4375 11.0312C11.2812 11.1562 11.125 11.2812 11 11.4062C10.4688 11.9688 10.1875 12.75 10.1875 13.5938C10.1875 14.4375 10.5 15.2188 11.1562 16C11.6875 16.6562 12.4375 17.2812 13.2812 18L13.3125 18.0312C13.5937 18.25 13.9062 18.5312 14.2188 18.8125C14.4062 19 14.6875 19.0938 14.9375 19.0938C15.1875 19.0938 15.4687 19 15.6562 18.8125C16 18.5312 16.3125 18.25 16.5938 18C17.4375 17.2812 18.1875 16.6562 18.7188 16C19.375 15.2188 19.6875 14.4375 19.6875 13.5938C19.6875 12.7812 19.4062 12.0312 18.875 11.4375ZM4.875 4.875C4.875 3.375 6.09375 2.1875 7.5625 2.1875C9.0625 2.1875 10.25 3.40625 10.25 4.875C10.25 6.375 9.03125 7.5625 7.5625 7.5625C6.09375 7.5625 4.875 6.34375 4.875 4.875ZM17.75 15.2188C17.2812 15.7812 16.5938 16.375 15.7812 17.0625C15.5312 17.2812 15.2188 17.5312 14.9062 17.7812C14.625 17.5312 14.3438 17.2812 14.0938 17.0938L14.0625 17.0625C13.25 16.375 12.5625 15.7812 12.0938 15.2188C11.625 14.6562 11.4062 14.1562 11.4062 13.625C11.4062 13.0937 11.5938 12.625 11.9062 12.2812C12.2188 11.9375 12.6563 11.75 13.0938 11.75C13.4375 11.75 13.75 11.8438 14 12.0625C14.125 12.1562 14.2188 12.25 14.3125 12.375C14.5938 12.7188 15.1875 12.7188 15.5 12.375C15.5938 12.25 15.7187 12.1562 15.8125 12.0625C16.0937 11.8438 16.4062 11.75 16.7188 11.75C17.1875 11.75 17.5938 11.9375 17.9062 12.2812C18.2188 12.625 18.4062 13.0937 18.4062 13.625C18.4375 14.1875 18.2188 14.6562 17.75 15.2188Z'
fill=''
/>
</g>
<defs>
<clipPath id='clip0_182_46495'>
<rect width='20' height='20' fill='white' />
</clipPath>
</defs>
</svg>
</span>
Button With Icon
</Link>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 rounded-full bg-black py-4 px-10 text-center font-medium text-white hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g clipPath='url(#clip0_182_46495)'>
<path
d='M18.875 11.4375C18.3125 10.8438 17.5625 10.5312 16.75 10.5312C16.125 10.5312 15.5625 10.7188 15.0625 11.0938C15 11.125 14.9688 11.1562 14.9062 11.2188C14.8438 11.1875 14.8125 11.125 14.75 11.0938C14.25 10.7188 13.6875 10.5312 13.0625 10.5312C12.9062 10.5312 12.7812 10.5312 12.6562 10.5625C11.7188 9.5 10.5625 8.75 9.3125 8.40625C10.625 7.75 11.5312 6.40625 11.5312 4.875C11.5312 2.6875 9.75 0.9375 7.59375 0.9375C5.40625 0.9375 3.65625 2.71875 3.65625 4.875C3.65625 6.4375 4.5625 7.78125 5.875 8.40625C4.5625 8.78125 3.40625 9.53125 2.4375 10.6562C1.125 12.2188 0.375 14.4062 0.3125 16.7812C0.3125 17.0312 0.4375 17.25 0.65625 17.3438C1.5 17.75 4.4375 19.0938 7.59375 19.0938C9.28125 19.0938 10.8438 18.8125 10.9062 18.8125C11.25 18.75 11.4688 18.4375 11.4062 18.0938C11.3438 17.75 11.0312 17.5312 10.6875 17.5938C10.6875 17.5938 9.15625 17.875 7.59375 17.875C5.0625 17.8438 2.65625 16.875 1.5625 16.375C1.65625 14.4375 2.3125 12.7187 3.375 11.4375C4.46875 10.125 5.96875 9.40625 7.59375 9.40625C9.03125 9.40625 10.375 10 11.4375 11.0312C11.2812 11.1562 11.125 11.2812 11 11.4062C10.4688 11.9688 10.1875 12.75 10.1875 13.5938C10.1875 14.4375 10.5 15.2188 11.1562 16C11.6875 16.6562 12.4375 17.2812 13.2812 18L13.3125 18.0312C13.5937 18.25 13.9062 18.5312 14.2188 18.8125C14.4062 19 14.6875 19.0938 14.9375 19.0938C15.1875 19.0938 15.4687 19 15.6562 18.8125C16 18.5312 16.3125 18.25 16.5938 18C17.4375 17.2812 18.1875 16.6562 18.7188 16C19.375 15.2188 19.6875 14.4375 19.6875 13.5938C19.6875 12.7812 19.4062 12.0312 18.875 11.4375ZM4.875 4.875C4.875 3.375 6.09375 2.1875 7.5625 2.1875C9.0625 2.1875 10.25 3.40625 10.25 4.875C10.25 6.375 9.03125 7.5625 7.5625 7.5625C6.09375 7.5625 4.875 6.34375 4.875 4.875ZM17.75 15.2188C17.2812 15.7812 16.5938 16.375 15.7812 17.0625C15.5312 17.2812 15.2188 17.5312 14.9062 17.7812C14.625 17.5312 14.3438 17.2812 14.0938 17.0938L14.0625 17.0625C13.25 16.375 12.5625 15.7812 12.0938 15.2188C11.625 14.6562 11.4062 14.1562 11.4062 13.625C11.4062 13.0937 11.5938 12.625 11.9062 12.2812C12.2188 11.9375 12.6563 11.75 13.0938 11.75C13.4375 11.75 13.75 11.8438 14 12.0625C14.125 12.1562 14.2188 12.25 14.3125 12.375C14.5938 12.7188 15.1875 12.7188 15.5 12.375C15.5938 12.25 15.7187 12.1562 15.8125 12.0625C16.0937 11.8438 16.4062 11.75 16.7188 11.75C17.1875 11.75 17.5938 11.9375 17.9062 12.2812C18.2188 12.625 18.4062 13.0937 18.4062 13.625C18.4375 14.1875 18.2188 14.6562 17.75 15.2188Z'
fill=''
/>
</g>
<defs>
<clipPath id='clip0_182_46495'>
<rect width='20' height='20' fill='white' />
</clipPath>
</defs>
</svg>
</span>
Button With Icon
</Link>
<Link
to='#'
className='inline-flex items-center justify-center gap-2.5 rounded-full border border-primary py-4 px-10 text-center font-medium text-primary hover:bg-opacity-90 lg:px-8 xl:px-10'
>
<span>
<svg
className='fill-current'
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g clipPath='url(#clip0_182_46495)'>
<path
d='M18.875 11.4375C18.3125 10.8438 17.5625 10.5312 16.75 10.5312C16.125 10.5312 15.5625 10.7188 15.0625 11.0938C15 11.125 14.9688 11.1562 14.9062 11.2188C14.8438 11.1875 14.8125 11.125 14.75 11.0938C14.25 10.7188 13.6875 10.5312 13.0625 10.5312C12.9062 10.5312 12.7812 10.5312 12.6562 10.5625C11.7188 9.5 10.5625 8.75 9.3125 8.40625C10.625 7.75 11.5312 6.40625 11.5312 4.875C11.5312 2.6875 9.75 0.9375 7.59375 0.9375C5.40625 0.9375 3.65625 2.71875 3.65625 4.875C3.65625 6.4375 4.5625 7.78125 5.875 8.40625C4.5625 8.78125 3.40625 9.53125 2.4375 10.6562C1.125 12.2188 0.375 14.4062 0.3125 16.7812C0.3125 17.0312 0.4375 17.25 0.65625 17.3438C1.5 17.75 4.4375 19.0938 7.59375 19.0938C9.28125 19.0938 10.8438 18.8125 10.9062 18.8125C11.25 18.75 11.4688 18.4375 11.4062 18.0938C11.3438 17.75 11.0312 17.5312 10.6875 17.5938C10.6875 17.5938 9.15625 17.875 7.59375 17.875C5.0625 17.8438 2.65625 16.875 1.5625 16.375C1.65625 14.4375 2.3125 12.7187 3.375 11.4375C4.46875 10.125 5.96875 9.40625 7.59375 9.40625C9.03125 9.40625 10.375 10 11.4375 11.0312C11.2812 11.1562 11.125 11.2812 11 11.4062C10.4688 11.9688 10.1875 12.75 10.1875 13.5938C10.1875 14.4375 10.5 15.2188 11.1562 16C11.6875 16.6562 12.4375 17.2812 13.2812 18L13.3125 18.0312C13.5937 18.25 13.9062 18.5312 14.2188 18.8125C14.4062 19 14.6875 19.0938 14.9375 19.0938C15.1875 19.0938 15.4687 19 15.6562 18.8125C16 18.5312 16.3125 18.25 16.5938 18C17.4375 17.2812 18.1875 16.6562 18.7188 16C19.375 15.2188 19.6875 14.4375 19.6875 13.5938C19.6875 12.7812 19.4062 12.0312 18.875 11.4375ZM4.875 4.875C4.875 3.375 6.09375 2.1875 7.5625 2.1875C9.0625 2.1875 10.25 3.40625 10.25 4.875C10.25 6.375 9.03125 7.5625 7.5625 7.5625C6.09375 7.5625 4.875 6.34375 4.875 4.875ZM17.75 15.2188C17.2812 15.7812 16.5938 16.375 15.7812 17.0625C15.5312 17.2812 15.2188 17.5312 14.9062 17.7812C14.625 17.5312 14.3438 17.2812 14.0938 17.0938L14.0625 17.0625C13.25 16.375 12.5625 15.7812 12.0938 15.2188C11.625 14.6562 11.4062 14.1562 11.4062 13.625C11.4062 13.0937 11.5938 12.625 11.9062 12.2812C12.2188 11.9375 12.6563 11.75 13.0938 11.75C13.4375 11.75 13.75 11.8438 14 12.0625C14.125 12.1562 14.2188 12.25 14.3125 12.375C14.5938 12.7188 15.1875 12.7188 15.5 12.375C15.5938 12.25 15.7187 12.1562 15.8125 12.0625C16.0937 11.8438 16.4062 11.75 16.7188 11.75C17.1875 11.75 17.5938 11.9375 17.9062 12.2812C18.2188 12.625 18.4062 13.0937 18.4062 13.625C18.4375 14.1875 18.2188 14.6562 17.75 15.2188Z'
fill=''
/>
</g>
<defs>
<clipPath id='clip0_182_46495'>
<rect width='20' height='20' fill='white' />
</clipPath>
</defs>
</svg>
</span>
Button With Icon
</Link>
<div className='p-4 md:p-6 xl:p-9'>
<div className='flex flex-wrap gap-4'>
<Button>
<Plus />
Add Item
</Button>
<Button variant='outline'>
<Heart />
Like
</Button>
<Button variant='destructive'>
<Trash2 />
Delete
</Button>
</div>
</div>
</div>

View File

@@ -5,15 +5,16 @@ interface BreadcrumbProps {
const Breadcrumb = ({ pageName }: BreadcrumbProps) => {
return (
<div className='mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
<h2 className='text-title-md2 font-semibold text-black dark:text-white'>{pageName}</h2>
<h2 className='text-title-md2 font-semibold text-foreground'>{pageName}</h2>
<nav>
<ol className='flex items-center gap-2'>
<ul className='flex items-center gap-1'>
<li>
<WaspRouterLink to={routes.AdminRoute.to}>Dashboard /</WaspRouterLink>
<WaspRouterLink to={routes.AdminRoute.to}>Dashboard</WaspRouterLink>
</li>
<li className='text-primary'>{pageName}</li>
</ol>
<li>/</li>
<li className='font-medium'>{pageName}</li>
</ul>
</nav>
</div>
);

View File

@@ -1,5 +1,5 @@
import { FC, ReactNode, useState } from 'react';
import { type AuthUser } from 'wasp/auth';
import { useState, ReactNode, FC } from 'react';
import Header from './Header';
import Sidebar from './Sidebar';
@@ -12,28 +12,16 @@ const DefaultLayout: FC<Props> = ({ children, user }) => {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className='dark:bg-boxdark-2 dark:text-bodydark'>
{/* <!-- ===== Page Wrapper Start ===== --> */}
<div className='bg-background text-foreground'>
<div className='flex h-screen overflow-hidden'>
{/* <!-- ===== Sidebar Start ===== --> */}
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
{/* <!-- ===== Sidebar End ===== --> */}
{/* <!-- ===== Content Area Start ===== --> */}
<div className='relative flex flex-1 flex-col overflow-y-auto overflow-x-hidden'>
{/* <!-- ===== Header Start ===== --> */}
<Header sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} user={user} />
{/* <!-- ===== Header End ===== --> */}
{/* <!-- ===== Main Content Start ===== --> */}
<main>
<div className='mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10'>{children}</div>
</main>
{/* <!-- ===== Main Content End ===== --> */}
</div>
{/* <!-- ===== Content Area End ===== --> */}
</div>
{/* <!-- ===== Page Wrapper End ===== --> */}
</div>
);
};

View File

@@ -1,8 +1,8 @@
import { type AuthUser } from 'wasp/auth';
import MessageButton from '../../messages/MessageButton';
import DropdownUser from '../../user/DropdownUser';
import { cn } from '../../client/cn';
import DarkModeSwitcher from '../../client/components/DarkModeSwitcher';
import MessageButton from '../../messages/MessageButton';
import DropdownUser from '../../user/DropdownUser';
const Header = (props: {
sidebarOpen: string | boolean | undefined;
@@ -10,8 +10,8 @@ const Header = (props: {
user: AuthUser;
}) => {
return (
<header className='sticky top-0 z-999 flex w-full bg-white dark:bg-boxdark dark:drop-shadow-none'>
<div className='flex flex-grow items-center justify-between sm:justify-end sm:gap-5 px-8 py-5 shadow '>
<header className='sticky top-0 z-10 flex w-full bg-background border-b border-border shadow-sm'>
<div className='flex flex-grow items-center justify-between sm:justify-end sm:gap-5 px-8 py-5'>
<div className='flex items-center gap-2 sm:gap-4 lg:hidden'>
{/* <!-- Hamburger Toggle BTN --> */}
@@ -21,13 +21,13 @@ const Header = (props: {
e.stopPropagation();
props.setSidebarOpen(!props.sidebarOpen);
}}
className='z-99999 block rounded-sm border border-stroke bg-white p-1.5 shadow-sm dark:border-strokedark dark:bg-boxdark lg:hidden'
className='z-99999 block rounded-sm border border-border bg-background p-1.5 shadow-sm lg:hidden'
>
<span className='relative block h-5.5 w-5.5 cursor-pointer'>
<span className='du-block absolute right-0 h-full w-full'>
<span
className={cn(
'relative top-0 left-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-[0] duration-200 ease-in-out dark:bg-white',
'relative top-0 left-0 my-1 block h-0.5 w-0 rounded-sm bg-foreground delay-[0] duration-200 ease-in-out',
{
'!w-full delay-300': !props.sidebarOpen,
}
@@ -35,7 +35,7 @@ const Header = (props: {
></span>
<span
className={cn(
'relative top-0 left-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-150 duration-200 ease-in-out dark:bg-white',
'relative top-0 left-0 my-1 block h-0.5 w-0 rounded-sm bg-foreground delay-150 duration-200 ease-in-out',
{
'delay-400 !w-full': !props.sidebarOpen,
}
@@ -43,7 +43,7 @@ const Header = (props: {
></span>
<span
className={cn(
'relative top-0 left-0 my-1 block h-0.5 w-0 rounded-sm bg-black delay-200 duration-200 ease-in-out dark:bg-white',
'relative top-0 left-0 my-1 block h-0.5 w-0 rounded-sm bg-foreground delay-200 duration-200 ease-in-out',
{
'!w-full delay-500': !props.sidebarOpen,
}
@@ -53,7 +53,7 @@ const Header = (props: {
<span className='absolute right-0 h-full w-full rotate-45'>
<span
className={cn(
'absolute left-2.5 top-0 block h-full w-0.5 rounded-sm bg-black delay-300 duration-200 ease-in-out dark:bg-white',
'absolute left-2.5 top-0 block h-full w-0.5 rounded-sm bg-foreground delay-300 duration-200 ease-in-out',
{
'!h-0 !delay-[0]': !props.sidebarOpen,
}
@@ -61,7 +61,7 @@ const Header = (props: {
></span>
<span
className={cn(
'delay-400 absolute left-0 top-2.5 block h-0.5 w-full rounded-sm bg-black duration-200 ease-in-out dark:bg-white',
'delay-400 absolute left-0 top-2.5 block h-0.5 w-full rounded-sm bg-foreground duration-200 ease-in-out',
{
'!h-0 !delay-200': !props.sidebarOpen,
}

View File

@@ -1,7 +1,7 @@
const LoadingSpinner = () => {
return (
<div className="flex h-screen items-center justify-center bg-white">
<div className="h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-t-transparent"></div>
<div className='flex py-10 items-center justify-center'>
<div className='h-16 w-16 animate-spin rounded-full border-4 border-solid border-primary border-t-transparent'></div>
</div>
);
};

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,12 @@
import './Main.css';
import NavBar from './components/NavBar/NavBar';
import CookieConsentBanner from './components/cookie-consent/Banner';
import { appNavigationItems } from './components/NavBar/contentSections';
import { landingPageNavigationItems } from '../landing-page/contentSections';
import { useMemo, useEffect } from 'react';
import { routes } from 'wasp/client/router';
import { useEffect, useMemo } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { useAuth } from 'wasp/client/auth';
import { routes } from 'wasp/client/router';
import { landingPageNavigationItems } from '../landing-page/contentSections';
import './Main.css';
import NavBar from './components/NavBar/NavBar';
import { appNavigationItems } from './components/NavBar/contentSections';
import CookieConsentBanner from './components/cookie-consent/Banner';
import { useIsLandingPage } from './hooks/useIsLandingPage';
/**
@@ -20,7 +20,9 @@ export default function App() {
const navigationItems = isLandingPage ? landingPageNavigationItems : appNavigationItems;
const shouldDisplayAppNavBar = useMemo(() => {
return location.pathname !== routes.LoginRoute.build() && location.pathname !== routes.SignupRoute.build();
return (
location.pathname !== routes.LoginRoute.build() && location.pathname !== routes.SignupRoute.build()
);
}, [location]);
const isAdminDashboard = useMemo(() => {
@@ -39,13 +41,13 @@ export default function App() {
return (
<>
<div className='min-h-screen dark:text-white dark:bg-boxdark-2'>
<div className='min-h-screen bg-background text-foreground'>
{isAdminDashboard ? (
<Outlet />
) : (
<>
{shouldDisplayAppNavBar && <NavBar navigationItems={navigationItems} />}
<div className='mx-auto max-w-7xl sm:px-6 lg:px-8'>
<div className='mx-auto max-w-screen-2xl'>
<Outlet />
</div>
</>

View File

@@ -19,6 +19,31 @@
.inbox-height {
@apply h-[calc(100vh_-_8.125rem)] lg:h-[calc(100vh_-_5.625rem)];
}
/* Text gradient utilities */
.text-gradient-primary {
background: linear-gradient(to right, hsl(var(--secondary-muted)), hsl(var(--secondary)));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.text-gradient-primary-diagonal {
background: linear-gradient(135deg, hsl(var(--secondary-muted)), hsl(var(--secondary)));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Border gradient utilities */
.border-gradient-primary {
background: linear-gradient(to right, hsl(var(--secondary-muted)), hsl(var(--secondary)));
padding: 1px;
}
.border-gradient-primary > * {
background: hsl(var(--background));
}
}
/* Here is an example of how to add a custom font.
@@ -44,16 +69,16 @@
}
.apexcharts-legend-text {
@apply !text-body dark:!text-bodydark;
@apply !text-muted-foreground;
}
.apexcharts-text {
@apply !fill-body dark:!fill-bodydark;
@apply !fill-muted-foreground;
}
.apexcharts-xcrosshairs {
@apply !fill-stroke dark:!fill-strokedark;
@apply !fill-border;
}
.apexcharts-gridline {
@apply !stroke-stroke dark:!stroke-strokedark;
@apply !stroke-border;
}
.apexcharts-series.apexcharts-pie-series path {
@apply dark:!stroke-transparent;
@@ -62,106 +87,94 @@
@apply !inline-flex gap-1.5;
}
.apexcharts-tooltip.apexcharts-theme-light {
@apply dark:!bg-boxdark dark:!border-strokedark;
@apply dark:!bg-card dark:!border-border;
}
.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title {
@apply dark:!bg-meta-4 dark:!border-strokedark;
@apply dark:!bg-muted dark:!border-border;
}
.apexcharts-xaxistooltip, .apexcharts-yaxistooltip {
@apply dark:!bg-meta-4 dark:!border-meta-4 dark:!text-bodydark1;
@apply dark:!bg-muted dark:!border-muted dark:!text-muted-foreground;
}
.apexcharts-xaxistooltip-bottom:after {
@apply dark:!border-b-meta-4;
@apply dark:!border-b-muted;
}
.apexcharts-xaxistooltip-bottom:before {
@apply dark:!border-b-meta-4;
@apply dark:!border-b-muted;
}
.flatpickr-day.selected {
@apply bg-primary border-primary hover:bg-primary hover:border-primary;
}
.flatpickr-months .flatpickr-prev-month:hover svg,
.flatpickr-months .flatpickr-next-month:hover svg {
@apply fill-primary;
}
.flatpickr-calendar.arrowTop:before {
@apply dark:!border-b-boxdark;
}
.flatpickr-calendar.arrowTop:after {
@apply dark:!border-b-boxdark;
}
.flatpickr-calendar {
@apply dark:!bg-boxdark dark:!text-bodydark dark:!shadow-8 !p-6 2xsm:!w-auto;
}
.flatpickr-day {
@apply dark:!text-bodydark;
}
.flatpickr-months .flatpickr-prev-month, .flatpickr-months .flatpickr-next-month {
@apply !top-7 dark:!text-white dark:!fill-white;
}
.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month, .flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
@apply !left-7
}
.flatpickr-months .flatpickr-prev-month.flatpickr-next-month, .flatpickr-months .flatpickr-next-month.flatpickr-next-month {
@apply !right-7
}
span.flatpickr-weekday,
.flatpickr-months .flatpickr-month {
@apply dark:!text-white dark:!fill-white;
}
.flatpickr-day.inRange {
@apply dark:!bg-meta-4 dark:!border-meta-4 dark:!shadow-7;
}
.flatpickr-day.selected, .flatpickr-day.startRange,
.flatpickr-day.selected, .flatpickr-day.endRange {
@apply dark:!text-white;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--card-accent: 0 0% 100%;
--card-accent-foreground: 0 0% 3.9%;
--card-subtle: 31 57% 96%;
--card-subtle-foreground: 0 0% 9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 210 100% 13%;
--primary-foreground: 0 0% 98%;
--secondary: 32 100% 37%;
--secondary-foreground: 0 0% 9%;
--secondary-muted: 32 75% 78%;
--secondary-muted-foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 33 74% 62%;
--accent-foreground: 0 0% 98%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--success: 141 71% 48%;
--success-foreground: 0 0% 98%;
--warning: 36 100% 50%;
--warning-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 210 50% 5%;
--foreground: 0 0% 98%;
--card: 210 50% 5%;
--card-foreground: 0 0% 98%;
--card-accent: 31 57% 93%;
--card-accent-foreground: 0 0% 3.9%;
--card-subtle: 233 24% 15%;
--card-subtle-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 31 57% 93%;
--primary-foreground: 0 0% 9%;
--secondary: 209 91% 61%;
--secondary-foreground: 0 0% 98%;
--secondary-muted: 210 67% 94%;
--secondary-muted-foreground: 0 0% 3.9%;
--muted: 210 100% 13%;
--muted-foreground: 0 0% 63.9%;
--accent: 31 57% 93%;
--accent-foreground: 210 100% 13%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--success: 141 71% 48%;
--success-foreground: 0 0% 98%;
--warning: 36 100% 50%;
--warning-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
}
.map-btn .jvm-zoom-btn {
@apply flex items-center justify-center w-7.5 h-7.5 rounded border border-stroke dark:border-strokedark hover:border-primary dark:hover:border-primary bg-white hover:bg-primary text-body hover:text-white dark:text-bodydark dark:hover:text-white text-2xl leading-none px-0 pt-0 pb-0.5;
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
.mapOne .jvm-zoom-btn {
@apply left-auto top-auto bottom-0;
}
.mapOne .jvm-zoom-btn.jvm-zoomin {
@apply right-10;
}
.mapOne .jvm-zoom-btn.jvm-zoomout {
@apply right-0;
}
.mapTwo .jvm-zoom-btn {
@apply top-auto bottom-0;
}
.mapTwo .jvm-zoom-btn.jvm-zoomin {
@apply left-0;
}
.mapTwo .jvm-zoom-btn.jvm-zoomout {
@apply left-10;
}
.taskCheckbox:checked ~ .box span {
@apply opacity-100;
}
.taskCheckbox:checked ~ p {
@apply line-through;
}
.taskCheckbox:checked ~ .box {
@apply bg-primary border-primary dark:border-primary;
}
.custom-input-date::-webkit-calendar-picker-indicator {
background-position: center;
background-repeat: no-repeat;
background-size: 20px;
}
.custom-input-date-1::-webkit-calendar-picker-indicator {
background-image: url(./images/icon/icon-calendar.svg);
}
.custom-input-date-2::-webkit-calendar-picker-indicator {
background-image: url(./images/icon/icon-arrow-down.svg);
}
[x-cloak] {
display: none !important;
}

View File

@@ -1,3 +1,5 @@
import { Moon, Sun } from 'lucide-react';
import { Label } from '../../components/ui/label';
import { cn } from '../cn';
import useColorMode from '../hooks/useColorMode';
@@ -7,7 +9,11 @@ const DarkModeSwitcher = () => {
return (
<div>
<label className={cn('relative m-0 block h-7.5 w-14 rounded-full', isInLightMode ? 'bg-stroke' : 'bg-primary')}>
<Label
className={cn(
'relative m-0 block h-7.5 w-14 rounded-full transition-colors duration-300 ease-in-out cursor-pointer bg-muted'
)}
>
<input
type='checkbox'
onChange={() => {
@@ -19,7 +25,7 @@ const DarkModeSwitcher = () => {
/>
<span
className={cn(
'absolute top-1/2 left-[3px] flex h-6 w-6 -translate-y-1/2 translate-x-0 items-center justify-center rounded-full bg-white shadow-switcher duration-200 ease-linear',
'absolute top-1/2 left-[3px] flex h-6 w-6 -translate-y-1/2 translate-x-0 items-center justify-center rounded-full bg-white shadow-md border border-border transition-all duration-300 ease-in-out',
{
'!right-[3px] !translate-x-full': !isInLightMode,
}
@@ -27,45 +33,24 @@ const DarkModeSwitcher = () => {
>
<ModeIcon isInLightMode={isInLightMode} />
</span>
</label>
</Label>
</div>
);
};
};
function ModeIcon({ isInLightMode }: { isInLightMode: boolean }) {
const iconStyle = 'absolute inset-0 flex items-center justify-center transition-opacity ease-in-out duration-400';
const iconStyle =
'absolute inset-0 flex items-center justify-center transition-opacity ease-in-out duration-300';
return (
<>
<span className={cn(iconStyle, isInLightMode ? 'opacity-100' : 'opacity-0')}><SunIcon /></span>
<span className={cn(iconStyle, !isInLightMode ? 'opacity-100' : 'opacity-0')}><MoonIcon /></span>
<span className={cn(iconStyle, isInLightMode ? 'opacity-100' : 'opacity-0')}>
<Sun className='size-4 stroke-amber-500 fill-amber-500' />
</span>
<span className={cn(iconStyle, !isInLightMode ? 'opacity-100' : 'opacity-0')}>
<Moon className='size-4 stroke-slate-600 fill-slate-600' />
</span>
</>
);
}
function SunIcon() {
return (
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M7.99992 12.6666C10.5772 12.6666 12.6666 10.5772 12.6666 7.99992C12.6666 5.42259 10.5772 3.33325 7.99992 3.33325C5.42259 3.33325 3.33325 5.42259 3.33325 7.99992C3.33325 10.5772 5.42259 12.6666 7.99992 12.6666Z'
fill='#969AA1'
/>
<path
d='M8.00008 15.3067C7.63341 15.3067 7.33342 15.0334 7.33342 14.6667V14.6134C7.33342 14.2467 7.63341 13.9467 8.00008 13.9467C8.36675 13.9467 8.66675 14.2467 8.66675 14.6134C8.66675 14.9801 8.36675 15.3067 8.00008 15.3067ZM12.7601 13.4267C12.5867 13.4267 12.4201 13.3601 12.2867 13.2334L12.2001 13.1467C11.9401 12.8867 11.9401 12.4667 12.2001 12.2067C12.4601 11.9467 12.8801 11.9467 13.1401 12.2067L13.2267 12.2934C13.4867 12.5534 13.4867 12.9734 13.2267 13.2334C13.1001 13.3601 12.9334 13.4267 12.7601 13.4267ZM3.24008 13.4267C3.06675 13.4267 2.90008 13.3601 2.76675 13.2334C2.50675 12.9734 2.50675 12.5534 2.76675 12.2934L2.85342 12.2067C3.11342 11.9467 3.53341 11.9467 3.79341 12.2067C4.05341 12.4667 4.05341 12.8867 3.79341 13.1467L3.70675 13.2334C3.58008 13.3601 3.40675 13.4267 3.24008 13.4267ZM14.6667 8.66675H14.6134C14.2467 8.66675 13.9467 8.36675 13.9467 8.00008C13.9467 7.63341 14.2467 7.33342 14.6134 7.33342C14.9801 7.33342 15.3067 7.63341 15.3067 8.00008C15.3067 8.36675 15.0334 8.66675 14.6667 8.66675ZM1.38675 8.66675H1.33341C0.966748 8.66675 0.666748 8.36675 0.666748 8.00008C0.666748 7.63341 0.966748 7.33342 1.33341 7.33342C1.70008 7.33342 2.02675 7.63341 2.02675 8.00008C2.02675 8.36675 1.75341 8.66675 1.38675 8.66675ZM12.6734 3.99341C12.5001 3.99341 12.3334 3.92675 12.2001 3.80008C11.9401 3.54008 11.9401 3.12008 12.2001 2.86008L12.2867 2.77341C12.5467 2.51341 12.9667 2.51341 13.2267 2.77341C13.4867 3.03341 13.4867 3.45341 13.2267 3.71341L13.1401 3.80008C13.0134 3.92675 12.8467 3.99341 12.6734 3.99341ZM3.32675 3.99341C3.15341 3.99341 2.98675 3.92675 2.85342 3.80008L2.76675 3.70675C2.50675 3.44675 2.50675 3.02675 2.76675 2.76675C3.02675 2.50675 3.44675 2.50675 3.70675 2.76675L3.79341 2.85342C4.05341 3.11342 4.05341 3.53341 3.79341 3.79341C3.66675 3.92675 3.49341 3.99341 3.32675 3.99341ZM8.00008 2.02675C7.63341 2.02675 7.33342 1.75341 7.33342 1.38675V1.33341C7.33342 0.966748 7.63341 0.666748 8.00008 0.666748C8.36675 0.666748 8.66675 0.966748 8.66675 1.33341C8.66675 1.70008 8.36675 2.02675 8.00008 2.02675Z'
fill='#969AA1'
/>
</svg>
);
}
function MoonIcon() {
return (
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M14.3533 10.62C14.2466 10.44 13.9466 10.16 13.1999 10.2933C12.7866 10.3667 12.3666 10.4 11.9466 10.38C10.3933 10.3133 8.98659 9.6 8.00659 8.5C7.13993 7.53333 6.60659 6.27333 6.59993 4.91333C6.59993 4.15333 6.74659 3.42 7.04659 2.72666C7.33993 2.05333 7.13326 1.7 6.98659 1.55333C6.83326 1.4 6.47326 1.18666 5.76659 1.48C3.03993 2.62666 1.35326 5.36 1.55326 8.28666C1.75326 11.04 3.68659 13.3933 6.24659 14.28C6.85993 14.4933 7.50659 14.62 8.17326 14.6467C8.27993 14.6533 8.38659 14.66 8.49326 14.66C10.7266 14.66 12.8199 13.6067 14.1399 11.8133C14.5866 11.1933 14.4666 10.8 14.3533 10.62Z'
fill='#969AA1'
/>
</svg>
);
}
export default DarkModeSwitcher;

View File

@@ -1,169 +1,285 @@
import { LogIn, Menu } from 'lucide-react';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { Link as ReactRouterLink } from 'react-router-dom';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
import { useAuth } from 'wasp/client/auth';
import { useState, Dispatch, SetStateAction } from 'react';
import { Dialog } from '@headlessui/react';
import { BiLogIn } from 'react-icons/bi';
import { AiFillCloseCircle } from 'react-icons/ai';
import { HiBars3 } from 'react-icons/hi2';
import logo from '../../static/logo.webp';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../../components/ui/sheet';
import DropdownUser from '../../../user/DropdownUser';
import { UserMenuItems } from '../../../user/UserMenuItems';
import DarkModeSwitcher from '../DarkModeSwitcher';
import { useIsLandingPage } from '../../hooks/useIsLandingPage';
import { cn } from '../../cn';
import { useIsLandingPage } from '../../hooks/useIsLandingPage';
import logo from '../../static/logo.webp';
import DarkModeSwitcher from '../DarkModeSwitcher';
export interface NavigationItem {
name: string;
to: string;
}
const NavLogo = () => <img className='h-8 w-8' src={logo} alt='Your SaaS App' />;
export default function AppNavBar({ navigationItems }: { navigationItems: NavigationItem[] }) {
export default function NavBar({ navigationItems }: { navigationItems: NavigationItem[] }) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const isLandingPage = useIsLandingPage();
const { data: user, isLoading: isUserLoading } = useAuth();
useEffect(() => {
const throttledHandler = throttleWithTrailingInvocation(() => {
setIsScrolled(window.scrollY > 0);
}, 50);
window.addEventListener('scroll', throttledHandler);
return () => {
window.removeEventListener('scroll', throttledHandler);
throttledHandler.cancel();
};
}, []);
return (
<header
className={cn('absolute inset-x-0 top-0 z-50 dark:bg-boxdark-2', {
'shadow sticky bg-white bg-opacity-50 backdrop-blur-lg backdrop-filter dark:border dark:border-gray-100/10':
!isLandingPage,
})}
>
<>
{isLandingPage && <Announcement />}
<nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'>
<div className='flex items-center lg:flex-1'>
<WaspRouterLink
to={routes.LandingPageRoute.to}
className='flex items-center -m-1.5 p-1.5 text-gray-900 duration-300 ease-in-out hover:text-yellow-500'
<header className={cn('sticky top-0 z-50 transition-all duration-300', isScrolled && 'top-4')}>
<div
className={cn('transition-all duration-300', {
'mx-4 md:mx-20 pr-2 lg:pr-0 rounded-full shadow-lg bg-background/90 backdrop-blur-lg border border-border':
isScrolled,
'mx-0 bg-background/80 backdrop-blur-lg border-b border-border': !isScrolled,
})}
>
<nav
className={cn('flex items-center justify-between transition-all duration-300', {
'p-3 lg:px-6': isScrolled,
'p-6 lg:px-8': !isScrolled,
})}
aria-label='Global'
>
<NavLogo />
{isLandingPage && (
<span className='ml-2 text-sm font-semibold leading-6 dark:text-white'>Your SaaS</span>
)}
</WaspRouterLink>
</div>
<div className='flex lg:hidden'>
<button
type='button'
className='-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700 dark:text-white'
onClick={() => setMobileMenuOpen(true)}
>
<span className='sr-only'>Open main menu</span>
<HiBars3 className='h-6 w-6' aria-hidden='true' />
</button>
</div>
<div className='hidden lg:flex lg:gap-x-12'>{renderNavigationItems(navigationItems)}</div>
<div className='hidden lg:flex lg:flex-1 gap-3 justify-end items-center'>
<ul className='flex justify-center items-center gap-2 sm:gap-4'>
<DarkModeSwitcher />
</ul>
{isUserLoading ? null : !user ? (
<WaspRouterLink to={routes.LoginRoute.to} className='text-sm font-semibold leading-6 ml-3'>
<div className='flex items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white'>
Log in <BiLogIn size='1.1rem' className='ml-1 mt-[0.1rem]' />
</div>
</WaspRouterLink>
) : (
<div className='ml-3'>
<DropdownUser user={user} />
</div>
)}
</div>
</nav>
<Dialog as='div' className='lg:hidden' open={mobileMenuOpen} onClose={setMobileMenuOpen}>
<div className='fixed inset-0 z-50' />
<Dialog.Panel className='fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white dark:text-white dark:bg-boxdark px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10'>
<div className='flex items-center justify-between'>
<WaspRouterLink to={routes.LandingPageRoute.to} className='-m-1.5 p-1.5'>
<span className='sr-only'>Your SaaS</span>
<NavLogo />
</WaspRouterLink>
<button
type='button'
className='-m-2.5 rounded-md p-2.5 text-gray-700 dark:text-gray-50'
onClick={() => setMobileMenuOpen(false)}
>
<span className='sr-only'>Close menu</span>
<AiFillCloseCircle className='h-6 w-6' aria-hidden='true' />
</button>
</div>
<div className='mt-6 flow-root'>
<div className='-my-6 divide-y divide-gray-500/10'>
<div className='space-y-2 py-6'>{renderNavigationItems(navigationItems, setMobileMenuOpen)}</div>
<div className='py-6'>
{isUserLoading ? null : !user ? (
<WaspRouterLink to={routes.LoginRoute.to}>
<div className='flex justify-end items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white'>
Log in <BiLogIn size='1.1rem' className='ml-1' />
</div>
</WaspRouterLink>
) : (
<UserMenuItems user={user} setMobileMenuOpen={setMobileMenuOpen} />
<div className='flex items-center gap-6'>
<WaspRouterLink
to={routes.LandingPageRoute.to}
className='flex items-center text-foreground duration-300 ease-in-out hover:text-primary transition-colors'
>
<NavLogo isScrolled={isScrolled} />
{isLandingPage && (
<span
className={cn('font-semibold leading-6 text-foreground transition-all duration-300', {
'ml-2 text-sm': !isScrolled,
'ml-2 text-xs': isScrolled,
})}
>
Your SaaS
</span>
)}
</div>
<div className='py-6'>
<DarkModeSwitcher />
</div>
</WaspRouterLink>
<ul className='hidden lg:flex items-center gap-6 ml-4'>
{renderNavigationItems(navigationItems)}
</ul>
</div>
</div>
</Dialog.Panel>
</Dialog>
</header>
<div className='flex lg:hidden'>
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
<SheetTrigger asChild>
<button
type='button'
className={cn(
'inline-flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors'
)}
>
<span className='sr-only'>Open main menu</span>
<Menu
className={cn('transition-all duration-300', {
'h-6 w-6': !isScrolled,
'h-5 w-5': isScrolled,
})}
aria-hidden='true'
/>
</button>
</SheetTrigger>
<SheetContent side='right' className='w-[300px] sm:w-[400px]'>
<SheetHeader>
<SheetTitle className='flex items-center'>
<WaspRouterLink to={routes.LandingPageRoute.to}>
<span className='sr-only'>Your SaaS</span>
<NavLogo isScrolled={false} />
</WaspRouterLink>
</SheetTitle>
</SheetHeader>
<div className='mt-6 flow-root'>
<div className='-my-6 divide-y divide-border'>
<ul className='space-y-2 py-6'>
{renderNavigationItems(navigationItems, setMobileMenuOpen)}
</ul>
<div className='py-6'>
{isUserLoading ? null : !user ? (
<WaspRouterLink to={routes.LoginRoute.to}>
<div className='flex justify-end items-center duration-300 ease-in-out text-foreground hover:text-primary transition-colors'>
Log in <LogIn size='1.1rem' className='ml-1' />
</div>
</WaspRouterLink>
) : (
<div className='space-y-2'>
<UserMenuItems user={user} onItemClick={() => setMobileMenuOpen(false)} />
</div>
)}
</div>
<div className='py-6'>
<DarkModeSwitcher />
</div>
</div>
</div>
</SheetContent>
</Sheet>
</div>
<div className='hidden lg:flex lg:flex-1 gap-3 justify-end items-center'>
<ul className='flex justify-center items-center gap-2 sm:gap-4'>
<DarkModeSwitcher />
</ul>
{isUserLoading ? null : !user ? (
<WaspRouterLink
to={routes.LoginRoute.to}
className={cn('font-semibold leading-6 ml-3 transition-all duration-300', {
'text-sm': !isScrolled,
'text-xs': isScrolled,
})}
>
<div className='flex items-center duration-300 ease-in-out text-foreground hover:text-primary transition-colors'>
Log in{' '}
<LogIn
size={isScrolled ? '1rem' : '1.1rem'}
className={cn('transition-all duration-300', {
'ml-1 mt-[0.1rem]': !isScrolled,
'ml-1': isScrolled,
})}
/>
</div>
</WaspRouterLink>
) : (
<div className='ml-3'>
<DropdownUser user={user} />
</div>
)}
</div>
</nav>
</div>
</header>
</>
);
}
function throttleWithTrailingInvocation(
fn: () => void,
delayInMilliseconds: number
): ((...args: any[]) => void) & { cancel: () => void } {
let fnLastCallTime: number | null = null;
let trailingInvocationTimeoutId: ReturnType<typeof setTimeout> | null = null;
let isTrailingInvocationPending = false;
const callFn = () => {
fnLastCallTime = Date.now();
fn();
};
const throttledFn = () => {
const currentTime = Date.now();
const timeSinceLastExecution = fnLastCallTime ? currentTime - fnLastCallTime : 0;
const shouldCallImmediately = fnLastCallTime === null || timeSinceLastExecution >= delayInMilliseconds;
if (shouldCallImmediately) {
callFn();
return;
}
if (!isTrailingInvocationPending) {
isTrailingInvocationPending = true;
const remainingDelayTime = Math.max(delayInMilliseconds - timeSinceLastExecution, 0);
trailingInvocationTimeoutId = setTimeout(() => {
callFn();
isTrailingInvocationPending = false;
}, remainingDelayTime);
}
};
throttledFn.cancel = () => {
if (trailingInvocationTimeoutId) {
clearTimeout(trailingInvocationTimeoutId);
trailingInvocationTimeoutId = null;
}
isTrailingInvocationPending = false;
};
return throttledFn as typeof throttledFn & { cancel: () => void };
}
function renderNavigationItems(
navigationItems: NavigationItem[],
setMobileMenuOpen?: Dispatch<SetStateAction<boolean>>
) {
const menuStyles = cn({
'-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50 dark:text-white dark:hover:bg-boxdark-2':
'block rounded-lg px-3 py-2 text-sm font-medium leading-7 text-foreground hover:bg-accent hover:text-accent-foreground transition-colors':
!!setMobileMenuOpen,
'text-sm font-semibold leading-6 text-gray-900 duration-300 ease-in-out hover:text-yellow-500 dark:text-white':
'text-sm font-normal leading-6 text-foreground duration-300 ease-in-out hover:text-primary transition-colors':
!setMobileMenuOpen,
});
return navigationItems.map((item) => {
return (
<ReactRouterLink
to={item.to}
key={item.name}
className={menuStyles}
onClick={setMobileMenuOpen && (() => setMobileMenuOpen(false))}
>
{item.name}
</ReactRouterLink>
<li key={item.name}>
<ReactRouterLink
to={item.to}
className={menuStyles}
onClick={setMobileMenuOpen && (() => setMobileMenuOpen(false))}
>
{item.name}
</ReactRouterLink>
</li>
);
});
}
const ContestURL = 'https://github.com/wasp-lang/wasp';
const NavLogo = ({ isScrolled }: { isScrolled: boolean }) => (
<img
className={cn('transition-all duration-500', {
'size-8': !isScrolled,
'size-6': isScrolled,
})}
src={logo}
alt='Your SaaS App'
/>
);
const announcementUrl = 'https://github.com/wasp-lang/wasp';
function Announcement() {
return (
<div className='flex justify-center items-center gap-3 p-3 w-full bg-gradient-to-r from-[#d946ef] to-[#fc0] font-semibold text-white text-center z-49'>
<p
onClick={() => window.open(ContestURL, '_blank')}
className='hidden lg:block cursor-pointer hover:opacity-90 hover:drop-shadow'
<div className='relative flex justify-center items-center gap-3 p-3 w-full bg-gradient-to-r from-accent to-secondary font-semibold text-primary-foreground text-center z-[51]'>
<a
href={announcementUrl}
target='_blank'
rel='noopener noreferrer'
className='hidden lg:block cursor-pointer hover:opacity-90 hover:drop-shadow transition-opacity'
>
Support Open-Source Software!
</p>
<div className='hidden lg:block self-stretch w-0.5 bg-white'></div>
<div
onClick={() => window.open(ContestURL, '_blank')}
className='hidden lg:block cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
</a>
<div className='hidden lg:block self-stretch w-0.5 bg-primary-foreground/20'></div>
<a
href={announcementUrl}
target='_blank'
rel='noopener noreferrer'
className='hidden lg:block cursor-pointer rounded-full bg-background/20 px-2.5 py-1 text-xs hover:bg-background/30 transition-colors tracking-wider'
>
Star Our Repo on Github
</div>
<div
onClick={() => window.open(ContestURL, '_blank')}
className='lg:hidden cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
</a>
<a
href={announcementUrl}
target='_blank'
rel='noopener noreferrer'
className='lg:hidden cursor-pointer rounded-full bg-background/20 px-2.5 py-1 text-xs hover:bg-background/30 transition-colors'
>
Star the Our Repo on Github and Support Open-Source!
</div>
Star the Our Repo and Support Open-Source!
</a>
</div>
);
}

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { useAuth } from 'wasp/client/auth';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
@@ -12,7 +11,7 @@ export function NotFoundPage() {
<p className='text-lg text-bodydark mb-8'>Oops! The page you're looking for doesn't exist.</p>
<WaspRouterLink
to={user ? routes.DemoAppRoute.to : routes.LandingPageRoute.to}
className='inline-block px-8 py-3 text-white font-semibold bg-yellow-500 rounded-lg hover:bg-yellow-400 transition duration-300'
className='inline-block px-8 py-3 text-accent-foreground font-semibold bg-accent rounded-lg hover:bg-accent/90 transition duration-300'
>
Go Back Home
</WaspRouterLink>

View File

@@ -0,0 +1,15 @@
import { useEffect, useState } from 'react';
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;

View File

@@ -1,6 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M9.0002 12.825C8.83145 12.825 8.69082 12.7688 8.5502 12.6563L2.08145 6.30002C1.82832 6.0469 1.82832 5.65315 2.08145 5.40002C2.33457 5.1469 2.72832 5.1469 2.98145 5.40002L9.0002 11.2781L15.0189 5.34377C15.2721 5.09065 15.6658 5.09065 15.9189 5.34377C16.1721 5.5969 16.1721 5.99065 15.9189 6.24377L9.45019 12.6C9.30957 12.7406 9.16895 12.825 9.0002 12.825Z"
fill="#64748B"
/>
</svg>

Before

Width:  |  Height:  |  Size: 493 B

View File

@@ -1,6 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M15.7504 2.9812H14.2879V2.36245C14.2879 2.02495 14.0066 1.71558 13.641 1.71558C13.2754 1.71558 12.9941 1.99683 12.9941 2.36245V2.9812H4.97852V2.36245C4.97852 2.02495 4.69727 1.71558 4.33164 1.71558C3.96602 1.71558 3.68477 1.99683 3.68477 2.36245V2.9812H2.25039C1.29414 2.9812 0.478516 3.7687 0.478516 4.75308V14.5406C0.478516 15.4968 1.26602 16.3125 2.25039 16.3125H15.7504C16.7066 16.3125 17.5223 15.525 17.5223 14.5406V4.72495C17.5223 3.7687 16.7066 2.9812 15.7504 2.9812ZM1.77227 8.21245H4.16289V10.9968H1.77227V8.21245ZM5.42852 8.21245H8.38164V10.9968H5.42852V8.21245ZM8.38164 12.2625V15.0187H5.42852V12.2625H8.38164V12.2625ZM9.64727 12.2625H12.6004V15.0187H9.64727V12.2625ZM9.64727 10.9968V8.21245H12.6004V10.9968H9.64727ZM13.8379 8.21245H16.2285V10.9968H13.8379V8.21245ZM2.25039 4.24683H3.71289V4.83745C3.71289 5.17495 3.99414 5.48433 4.35977 5.48433C4.72539 5.48433 5.00664 5.20308 5.00664 4.83745V4.24683H13.0504V4.83745C13.0504 5.17495 13.3316 5.48433 13.6973 5.48433C14.0629 5.48433 14.3441 5.20308 14.3441 4.83745V4.24683H15.7504C16.0316 4.24683 16.2566 4.47183 16.2566 4.75308V6.94683H1.77227V4.75308C1.77227 4.47183 1.96914 4.24683 2.25039 4.24683ZM1.77227 14.5125V12.2343H4.16289V14.9906H2.25039C1.96914 15.0187 1.77227 14.7937 1.77227 14.5125ZM15.7504 15.0187H13.8379V12.2625H16.2285V14.5406C16.2566 14.7937 16.0316 15.0187 15.7504 15.0187Z"
fill="#64748B"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,10 +0,0 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_512_11103)">
<path d="M8.83697 5.88205C8.8368 7.05058 9.18468 8.19267 9.83625 9.16268C10.4878 10.1327 11.4135 10.8866 12.4953 11.3284C13.5772 11.7701 14.766 11.8796 15.9103 11.6429C17.0546 11.4062 18.1025 10.8341 18.9203 9.99941V10.0834C18.9203 14.7243 15.1584 18.4862 10.5175 18.4862C5.87667 18.4862 2.11475 14.7243 2.11475 10.0834C2.11475 5.44259 5.87667 1.68066 10.5175 1.68066H10.6016C10.042 2.22779 9.59754 2.88139 9.29448 3.60295C8.99143 4.32451 8.83587 5.09943 8.83697 5.88205ZM3.7953 10.0834C3.79469 11.5833 4.29571 13.0403 5.21864 14.2226C6.14157 15.4049 7.4334 16.2446 8.88857 16.608C10.3437 16.9715 11.8787 16.8379 13.2491 16.2284C14.6196 15.6189 15.7469 14.5686 16.4516 13.2446C15.1974 13.54 13.8885 13.5102 12.6492 13.1578C11.4098 12.8054 10.281 12.1422 9.36988 11.2311C8.45877 10.32 7.79557 9.19119 7.44318 7.95181C7.0908 6.71243 7.06093 5.40357 7.3564 4.1494C6.28049 4.72259 5.38073 5.57759 4.75343 6.62288C4.12614 7.66817 3.79495 8.86438 3.7953 10.0834Z" fill="#757693"/>
</g>
<defs>
<clipPath id="clip0_512_11103">
<rect width="20.1667" height="20.1667" fill="#757693" transform="translate(0.434204)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,10 +0,0 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_1131)">
<path d="M11 16.4998C9.54133 16.4998 8.14238 15.9204 7.11093 14.8889C6.07948 13.8575 5.50002 12.4585 5.50002 10.9998C5.50002 9.54115 6.07948 8.1422 7.11093 7.11075C8.14238 6.0793 9.54133 5.49984 11 5.49984C12.4587 5.49984 13.8577 6.0793 14.8891 7.11075C15.9206 8.1422 16.5 9.54115 16.5 10.9998C16.5 12.4585 15.9206 13.8575 14.8891 14.8889C13.8577 15.9204 12.4587 16.4998 11 16.4998ZM11 14.6665C11.9725 14.6665 12.9051 14.2802 13.5927 13.5926C14.2804 12.9049 14.6667 11.9723 14.6667 10.9998C14.6667 10.0274 14.2804 9.09475 13.5927 8.40711C12.9051 7.71948 11.9725 7.33317 11 7.33317C10.0276 7.33317 9.09493 7.71948 8.4073 8.40711C7.71966 9.09475 7.33335 10.0274 7.33335 10.9998C7.33335 11.9723 7.71966 12.9049 8.4073 13.5926C9.09493 14.2802 10.0276 14.6665 11 14.6665ZM10.0834 0.916504H11.9167V3.6665H10.0834V0.916504ZM10.0834 18.3332H11.9167V21.0832H10.0834V18.3332ZM3.2221 4.51809L4.51827 3.22192L6.46252 5.16617L5.16635 6.46234L3.2221 4.519V4.51809ZM15.5375 16.8335L16.8337 15.5373L18.7779 17.4816L17.4818 18.7778L15.5375 16.8335ZM17.4818 3.221L18.7779 4.51809L16.8337 6.46234L15.5375 5.16617L17.4818 3.22192V3.221ZM5.16635 15.5373L6.46252 16.8335L4.51827 18.7778L3.2221 17.4816L5.16635 15.5373ZM21.0834 10.0832V11.9165H18.3334V10.0832H21.0834ZM3.66669 10.0832V11.9165H0.916687V10.0832H3.66669Z" fill="#757693"/>
</g>
<defs>
<clipPath id="clip0_1_1131">
<rect width="22" height="22" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,35 +0,0 @@
export function UpArrow() {
return (
<svg
className='fill-meta-3'
width='10'
height='11'
viewBox='0 0 10 11'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M4.35716 2.47737L0.908974 5.82987L5.0443e-07 4.94612L5 0.0848689L10 4.94612L9.09103 5.82987L5.64284 2.47737L5.64284 10.0849L4.35716 10.0849L4.35716 2.47737Z'
fill=''
/>
</svg>
);
}
export function DownArrow() {
return (
<svg
className='fill-meta-5'
width='10'
height='11'
viewBox='0 0 10 11'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M5.64284 7.69237L9.09102 4.33987L10 5.22362L5 10.0849L-8.98488e-07 5.22362L0.908973 4.33987L4.35716 7.69237L4.35716 0.0848701L5.64284 0.0848704L5.64284 7.69237Z'
fill=''
/>
</svg>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Some files were not shown because too many files have changed in this diff Show More