Improve Nav Bar & use Wasp Link components (#311)

* fix Links and Nav Bar

* update app_diff

* Update NavBar.tsx

* use react-router-dom instead of wasp router

* fix app_diff

* use Wasp Router Link where necessary

* Update NavBar.tsx
This commit is contained in:
vincanger
2024-11-15 16:13:25 +01:00
committed by GitHub
parent 304c49daff
commit a8654e3d64
21 changed files with 257 additions and 349 deletions

View File

@@ -0,0 +1,11 @@
--- template/app/migrations/20241030143842_checkout_session_id/migration.sql
+++ opensaas-sh/app/migrations/20241030143842_checkout_session_id/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `checkoutSessionId` on the `User` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "User" DROP COLUMN "checkoutSessionId";

View File

@@ -1,9 +1,9 @@
--- template/app/src/auth/LoginPage.tsx --- template/app/src/auth/LoginPage.tsx
+++ opensaas-sh/app/src/auth/LoginPage.tsx +++ opensaas-sh/app/src/auth/LoginPage.tsx
@@ -1,8 +1,14 @@ @@ -1,8 +1,15 @@
-import { Link } from 'react-router-dom'; +import { Navigate } from 'react-router-dom';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
-import { LoginForm } from 'wasp/client/auth'; -import { LoginForm } from 'wasp/client/auth';
+import { Navigate, Link } from 'react-router-dom';
+import { LoginForm, useAuth } from 'wasp/client/auth'; +import { LoginForm, useAuth } from 'wasp/client/auth';
import { AuthPageLayout } from './AuthPageLayout'; import { AuthPageLayout } from './AuthPageLayout';

View File

@@ -0,0 +1,79 @@
--- template/app/src/client/components/NavBar/NavBar.tsx
+++ opensaas-sh/app/src/client/components/NavBar/NavBar.tsx
@@ -32,6 +32,7 @@
!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
@@ -39,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'>
@@ -61,8 +60,8 @@
</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>
) : (
@@ -77,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
@@ -96,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>
) : (
@@ -138,3 +137,27 @@
);
});
}
+
+const ContestURL =
+ 'https://docs.opensaas.sh/blog/';
+
+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'>🍪 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'
+ >
+ 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'
+ >
+ 🍪 The Most Annoying Cookie Banner Contest! 🤬 →
+ </div>
+ </div>
+ );
+}
\ No newline at end of file

View File

@@ -1,96 +0,0 @@
--- template/app/src/landing-page/components/Header.tsx
+++ opensaas-sh/app/src/landing-page/components/Header.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { HiBars3 } from 'react-icons/hi2';
import { BiLogIn } from 'react-icons/bi';
import { AiFillCloseCircle } from 'react-icons/ai';
@@ -20,18 +20,19 @@
const { data: user, isLoading: isUserLoading } = useAuth();
- const NavLogo = () => <img className='h-8 w-8' src={logo} alt='Your SaaS App' />;
+ const NavLogo = () => <img className='h-8 w-8' src={logo} alt='Open SaaS App' />;
return (
<header className='absolute inset-x-0 top-0 z-50 dark:bg-boxdark-2'>
+ <Announcement />
<nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'>
<div className='flex items-center lg:flex-1'>
<a
href='/'
- className='flex items-center -m-1.5 p-1.5 text-gray-900 duration-300 ease-in-out hover:text-yellow-500'
+ className='flex items-center -m-1.5 p-1.5 text-gray-900 duration-300 ease-in-out hover:text-yellow-500 dark:text-white'
>
<NavLogo />
- <span className='ml-2 text-sm font-semibold leading-6 dark:text-white'>Your Saas</span>
+ <span className='ml-2 text-sm font-semibold leading-6 dark:text-white'>Open SaaS</span>
</a>
</div>
<div className='flex lg:hidden'>
@@ -57,14 +58,14 @@
</div>
<div className='hidden lg:flex lg:flex-1 lg:justify-end lg:align-end'>
{/* <!-- Dark Mode Toggler --> */}
- <div className='flex items-center gap-3 2xsm:gap-7'>
+ <div className='flex items-center gap-3 2xsm:gap-7 text-sm font-semibold leading-6'>
<ul className='flex justify-center items-center gap-2 2xsm:gap-4'>
<DarkModeSwitcher />
</ul>
{isUserLoading ? null : !user ? (
<Link to='/login'>
- <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 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>
</Link>
) : (
@@ -78,7 +79,7 @@
<Dialog.Panel className='fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-boxdark dark:text-white'>
<div className='flex items-center justify-between'>
<a href='/' className='-m-1.5 p-1.5'>
- <span className='sr-only'>Your SaaS</span>
+ <span className='sr-only'>Open SaaS</span>
<NavLogo />
</a>
<button
@@ -107,8 +108,8 @@
<div className='py-6'>
{isUserLoading ? null : !user ? (
<Link to='/login'>
- <div className='flex justify-start 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 className='flex justify-end items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white'>
+ Try the Demo App{' '} <BiLogIn size='1.1rem' className='ml-1' />
</div>
</Link>
) : (
@@ -125,3 +126,26 @@
</header>
)
}
+
+const ContestURL = 'https://x.com/WaspLang';
+
+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'>🍪 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'
+ >
+ Vote for the winner here! →
+ </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'
+ >
+ 🍪 The Most Annoying Cookie Banner Contest! 🤬 →
+ </div>
+ </div>
+ );
+}

View File

@@ -1,17 +1,18 @@
--- template/app/src/landing-page/contentSections.ts --- template/app/src/landing-page/contentSections.ts
+++ opensaas-sh/app/src/landing-page/contentSections.ts +++ opensaas-sh/app/src/landing-page/contentSections.ts
@@ -1,74 +1,131 @@ @@ -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 { DocsUrl, BlogUrl } from '../shared/common';
-import daBoiAvatar from '../client/static/da-boi.webp'; -import daBoiAvatar from '../client/static/da-boi.webp';
-import avatarPlaceholder from '../client/static/avatar-placeholder.webp'; -import avatarPlaceholder from '../client/static/avatar-placeholder.webp';
-import { routes } from 'wasp/client/router';
+import { DocsUrl, BlogUrl, GithubUrl } from '../shared/common'; +import { DocsUrl, BlogUrl, GithubUrl } from '../shared/common';
export const navigation = [ export const landingPageNavigationItems: NavigationItem[] = [
{ name: 'Features', href: '#features' }, { name: 'Features', to: '#features' },
- { name: 'Pricing', href: routes.PricingPageRoute.build() }, - { name: 'Pricing', to: routes.PricingPageRoute.to },
{ name: 'Documentation', href: DocsUrl }, { name: 'Documentation', to: DocsUrl },
{ name: 'Blog', href: BlogUrl }, { name: 'Blog', to: BlogUrl },
]; ];
export const features = [ export const features = [
{ {

View File

@@ -1,20 +1,18 @@
import { Link } from 'react-router-dom'; import { Link as WaspRouterLink, routes } from 'wasp/client/router';
interface BreadcrumbProps { interface BreadcrumbProps {
pageName: string; pageName: string;
} }
const Breadcrumb = ({ pageName }: BreadcrumbProps) => { const Breadcrumb = ({ pageName }: BreadcrumbProps) => {
return ( return (
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <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"> <h2 className='text-title-md2 font-semibold text-black dark:text-white'>{pageName}</h2>
{pageName}
</h2>
<nav> <nav>
<ol className="flex items-center gap-2"> <ol className='flex items-center gap-2'>
<li> <li>
<Link to="/">Dashboard /</Link> <WaspRouterLink to={routes.AdminRoute.to}>Dashboard /</WaspRouterLink>
</li> </li>
<li className="text-primary">{pageName}</li> <li className='text-primary'>{pageName}</li>
</ol> </ol>
</nav> </nav>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Link } from 'react-router-dom'; import { Link as WaspRouterLink, routes } from 'wasp/client/router';
import { LoginForm } from 'wasp/client/auth'; import { LoginForm } from 'wasp/client/auth';
import { AuthPageLayout } from './AuthPageLayout'; import { AuthPageLayout } from './AuthPageLayout';
@@ -9,17 +9,17 @@ export default function Login() {
<br /> <br />
<span className='text-sm font-medium text-gray-900 dark:text-gray-900'> <span className='text-sm font-medium text-gray-900 dark:text-gray-900'>
Don't have an account yet?{' '} Don't have an account yet?{' '}
<Link to='/signup' className='underline'> <WaspRouterLink to={routes.SignupRoute.to} className='underline'>
go to signup go to signup
</Link> </WaspRouterLink>
. .
</span> </span>
<br /> <br />
<span className='text-sm font-medium text-gray-900'> <span className='text-sm font-medium text-gray-900'>
Forgot your password?{' '} Forgot your password?{' '}
<Link to='/request-password-reset' className='underline'> <WaspRouterLink to={routes.RequestPasswordResetRoute.to} className='underline'>
reset it reset it
</Link> </WaspRouterLink>
. .
</span> </span>
</AuthPageLayout> </AuthPageLayout>

View File

@@ -1,4 +1,4 @@
import { Link } from 'react-router-dom'; import { Link as WaspRouterLink, routes } from 'wasp/client/router';
import { SignupForm } from 'wasp/client/auth'; import { SignupForm } from 'wasp/client/auth';
import { AuthPageLayout } from './AuthPageLayout'; import { AuthPageLayout } from './AuthPageLayout';
@@ -9,9 +9,9 @@ export function Signup() {
<br /> <br />
<span className='text-sm font-medium text-gray-900'> <span className='text-sm font-medium text-gray-900'>
I already have an account ( I already have an account (
<Link to='/login' className='underline'> <WaspRouterLink to={routes.LoginRoute.to} className='underline'>
go to login go to login
</Link> </WaspRouterLink>
). ).
</span> </span>
<br /> <br />

View File

@@ -1,4 +1,4 @@
import { Link } from 'react-router-dom'; import { Link as WaspRouterLink, routes } from 'wasp/client/router';
import { VerifyEmailForm } from 'wasp/client/auth'; import { VerifyEmailForm } from 'wasp/client/auth';
import { AuthPageLayout } from '../AuthPageLayout'; import { AuthPageLayout } from '../AuthPageLayout';
@@ -8,7 +8,7 @@ export function EmailVerificationPage() {
<VerifyEmailForm /> <VerifyEmailForm />
<br /> <br />
<span className='text-sm font-medium text-gray-900'> <span className='text-sm font-medium text-gray-900'>
If everything is okay, <Link to='/login' className='underline'>go to login</Link> If everything is okay, <WaspRouterLink to={routes.LoginRoute.to} className='underline'>go to login</WaspRouterLink>
</span> </span>
</AuthPageLayout> </AuthPageLayout>
); );

View File

@@ -1,4 +1,4 @@
import { Link } from 'react-router-dom'; import { Link as WaspRouterLink, routes } from 'wasp/client/router';
import { ResetPasswordForm } from 'wasp/client/auth'; import { ResetPasswordForm } from 'wasp/client/auth';
import { AuthPageLayout } from '../AuthPageLayout'; import { AuthPageLayout } from '../AuthPageLayout';
@@ -8,7 +8,7 @@ export function PasswordResetPage() {
<ResetPasswordForm /> <ResetPasswordForm />
<br /> <br />
<span className='text-sm font-medium text-gray-900'> <span className='text-sm font-medium text-gray-900'>
If everything is okay, <Link to='/login'>go to login</Link> If everything is okay, <WaspRouterLink to={routes.LoginRoute.to}>go to login</WaspRouterLink>
</span> </span>
</AuthPageLayout> </AuthPageLayout>
); );

View File

@@ -1,10 +1,14 @@
import { useAuth } from 'wasp/client/auth';
import { updateCurrentUser } from 'wasp/client/operations';
import './Main.css'; import './Main.css';
import AppNavBar from './components/AppNavBar'; import NavBar from './components/NavBar/NavBar';
import CookieConsentBanner from './components/cookie-consent/Banner'; 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 { useMemo, useEffect } from 'react';
import { routes } from 'wasp/client/router';
import { Outlet, useLocation } from 'react-router-dom'; import { Outlet, useLocation } from 'react-router-dom';
import { useAuth } from 'wasp/client/auth';
import { useIsLandingPage } from './hooks/useIsLandingPage';
import { updateCurrentUser } from 'wasp/client/operations';
/** /**
* use this component to wrap all child components * use this component to wrap all child components
@@ -13,9 +17,11 @@ import { Outlet, useLocation } from 'react-router-dom';
export default function App() { export default function App() {
const location = useLocation(); const location = useLocation();
const { data: user } = useAuth(); const { data: user } = useAuth();
const isLandingPage = useIsLandingPage();
const navigationItems = isLandingPage ? landingPageNavigationItems : appNavigationItems;
const shouldDisplayAppNavBar = useMemo(() => { const shouldDisplayAppNavBar = useMemo(() => {
return location.pathname !== '/' && location.pathname !== '/login' && location.pathname !== '/signup'; return location.pathname !== routes.LoginRoute.build() && location.pathname !== routes.SignupRoute.build();
}, [location]); }, [location]);
const isAdminDashboard = useMemo(() => { const isAdminDashboard = useMemo(() => {
@@ -49,7 +55,7 @@ export default function App() {
<Outlet /> <Outlet />
) : ( ) : (
<> <>
{shouldDisplayAppNavBar && <AppNavBar />} {shouldDisplayAppNavBar && <NavBar navigationItems={navigationItems} />}
<div className='mx-auto max-w-7xl sm:px-6 lg:px-8'> <div className='mx-auto max-w-7xl sm:px-6 lg:px-8'>
<Outlet /> <Outlet />
</div> </div>

View File

@@ -1,37 +1,48 @@
import { Link, routes } from 'wasp/client/router'; import { Link as ReactRouterLink } from 'react-router-dom';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
import { useAuth } from 'wasp/client/auth'; import { useAuth } from 'wasp/client/auth';
import { useState } from 'react'; import { useState, Dispatch, SetStateAction } from 'react';
import { Dialog } from '@headlessui/react'; import { Dialog } from '@headlessui/react';
import { BiLogIn } from 'react-icons/bi'; import { BiLogIn } from 'react-icons/bi';
import { AiFillCloseCircle } from 'react-icons/ai'; import { AiFillCloseCircle } from 'react-icons/ai';
import { HiBars3 } from 'react-icons/hi2'; import { HiBars3 } from 'react-icons/hi2';
import logo from '../static/logo.webp'; import logo from '../../static/logo.webp';
import DropdownUser from '../../user/DropdownUser'; import DropdownUser from '../../../user/DropdownUser';
import { UserMenuItems } from '../../user/UserMenuItems'; import { UserMenuItems } from '../../../user/UserMenuItems';
import { DocsUrl, BlogUrl } from '../../shared/common'; import DarkModeSwitcher from '../DarkModeSwitcher';
import DarkModeSwitcher from './DarkModeSwitcher'; import { useIsLandingPage } from '../../hooks/useIsLandingPage';
import { cn } from '../../cn';
const navigation = [ export interface NavigationItem {
{ name: 'AI Scheduler (Demo App)', href: routes.DemoAppRoute.build() }, name: string;
{ name: 'File Upload (AWS S3)', href: routes.FileUploadRoute.build() }, to: string;
{ name: 'Pricing', href: routes.PricingPageRoute.build() }, }
{ name: 'Documentation', href: DocsUrl },
{ name: 'Blog', href: BlogUrl },
];
const NavLogo = () => <img className='h-8 w-8' src={logo} alt='Your SaaS App' />; const NavLogo = () => <img className='h-8 w-8' src={logo} alt='Your SaaS App' />;
export default function AppNavBar() { export default function AppNavBar({ navigationItems }: { navigationItems: NavigationItem[] }) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const isLandingPage = useIsLandingPage();
const { data: user, isLoading: isUserLoading } = useAuth(); const { data: user, isLoading: isUserLoading } = useAuth();
return ( return (
<header className='absolute inset-x-0 top-0 z-50 shadow sticky bg-white bg-opacity-50 backdrop-blur-lg backdrop-filter dark:border dark:border-gray-100/10 dark:bg-boxdark-2'> <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,
})}
>
<nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'> <nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'>
<div className='flex lg:flex-1'> <div className='flex items-center lg:flex-1'>
<a href='/' className='-m-1.5 p-1.5'> <WaspRouterLink
<img className='h-8 w-8' src={logo} alt='My SaaS App' /> to={routes.LandingPageRoute.to}
</a> 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>
)}
</WaspRouterLink>
</div> </div>
<div className='flex lg:hidden'> <div className='flex lg:hidden'>
<button <button
@@ -43,30 +54,19 @@ export default function AppNavBar() {
<HiBars3 className='h-6 w-6' aria-hidden='true' /> <HiBars3 className='h-6 w-6' aria-hidden='true' />
</button> </button>
</div> </div>
<div className='hidden lg:flex lg:gap-x-12'> <div className='hidden lg:flex lg:gap-x-12'>{renderNavigationItems(navigationItems)}</div>
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className='text-sm font-semibold leading-6 text-gray-900 duration-300 ease-in-out hover:text-yellow-500 dark:text-white'
>
{item.name}
</a>
))}
</div>
<div className='hidden lg:flex lg:flex-1 gap-3 justify-end items-center'> <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='flex justify-center items-center gap-2 sm:gap-4'>
<DarkModeSwitcher /> <DarkModeSwitcher />
</ul> </ul>
{isUserLoading ? null : !user ? ( {isUserLoading ? null : !user ? (
<a href={!user ? routes.LoginRoute.build() : routes.AccountRoute.build()} className='text-sm font-semibold leading-6 ml-4'> <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'> <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]' /> Log in <BiLogIn size='1.1rem' className='ml-1 mt-[0.1rem]' />
</div> </div>
</a> </WaspRouterLink>
) : ( ) : (
<div className='ml-4'> <div className='ml-3'>
<DropdownUser user={user} /> <DropdownUser user={user} />
</div> </div>
)} )}
@@ -76,10 +76,10 @@ export default function AppNavBar() {
<div className='fixed inset-0 z-50' /> <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'> <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'> <div className='flex items-center justify-between'>
<a href='/' className='-m-1.5 p-1.5'> <WaspRouterLink to={routes.LandingPageRoute.to} className='-m-1.5 p-1.5'>
<span className='sr-only'>Your SaaS</span> <span className='sr-only'>Your SaaS</span>
<NavLogo /> <NavLogo />
</a> </WaspRouterLink>
<button <button
type='button' type='button'
className='-m-2.5 rounded-md p-2.5 text-gray-700 dark:text-gray-50' className='-m-2.5 rounded-md p-2.5 text-gray-700 dark:text-gray-50'
@@ -91,25 +91,14 @@ export default function AppNavBar() {
</div> </div>
<div className='mt-6 flow-root'> <div className='mt-6 flow-root'>
<div className='-my-6 divide-y divide-gray-500/10'> <div className='-my-6 divide-y divide-gray-500/10'>
<div className='space-y-2 py-6'> <div className='space-y-2 py-6'>{renderNavigationItems(navigationItems, setMobileMenuOpen)}</div>
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
onClick={() => setMobileMenuOpen(false)}
className='-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 hover:dark:bg-boxdark-2'
>
{item.name}
</a>
))}
</div>
<div className='py-6'> <div className='py-6'>
{isUserLoading ? null : !user ? ( {isUserLoading ? null : !user ? (
<Link to='/login'> <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'> <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' /> Log in <BiLogIn size='1.1rem' className='ml-1' />
</div> </div>
</Link> </WaspRouterLink>
) : ( ) : (
<UserMenuItems user={user} setMobileMenuOpen={setMobileMenuOpen} /> <UserMenuItems user={user} setMobileMenuOpen={setMobileMenuOpen} />
)} )}
@@ -124,3 +113,28 @@ export default function AppNavBar() {
</header> </header>
); );
} }
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':
!!setMobileMenuOpen,
'text-sm font-semibold leading-6 text-gray-900 duration-300 ease-in-out hover:text-yellow-500 dark:text-white':
!setMobileMenuOpen,
});
return navigationItems.map((item) => {
return (
<ReactRouterLink
to={item.to}
key={item.name}
className={menuStyles}
onClick={setMobileMenuOpen && (() => setMobileMenuOpen(false))}
>
{item.name}
</ReactRouterLink>
);
});
}

View File

@@ -0,0 +1,11 @@
import type { NavigationItem } from '../NavBar/NavBar';
import { routes } from 'wasp/client/router';
import { BlogUrl, DocsUrl } from '../../../shared/common';
export const appNavigationItems: NavigationItem[] = [
{ name: 'AI Scheduler (Demo App)', to: routes.DemoAppRoute.to },
{ name: 'File Upload (AWS S3)', to: routes.FileUploadRoute.to },
{ name: 'Pricing', to: routes.PricingPageRoute.to },
{ name: 'Documentation', to: DocsUrl },
{ name: 'Blog', to: BlogUrl },
];

View File

@@ -1,18 +1,21 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { useAuth } from 'wasp/client/auth';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
export function NotFoundPage() { export function NotFoundPage() {
const { data: user } = useAuth();
return ( return (
<div className='flex items-center justify-center min-h-screen'> <div className='flex items-center justify-center min-h-screen'>
<div className='text-center'> <div className='text-center'>
<h1 className='text-6xl font-bold mb-4'>404</h1> <h1 className='text-6xl font-bold mb-4'>404</h1>
<p className='text-lg text-bodydark mb-8'>Oops! The page you're looking for doesn't exist.</p> <p className='text-lg text-bodydark mb-8'>Oops! The page you're looking for doesn't exist.</p>
<Link <WaspRouterLink
to='/' to={user ? routes.DemoAppRoute.to : routes.LandingPageRoute.to}
className='inline-block px-8 py-3 text-white bg-primary rounded-lg hover:bg-secondary transition duration-300' className='inline-block px-8 py-3 text-white font-semibold bg-yellow-500 rounded-lg hover:bg-yellow-400 transition duration-300'
> >
Go Back Home Go Back Home
</Link> </WaspRouterLink>
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,9 @@
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
export const useIsLandingPage = () => {
const location = useLocation();
return useMemo(() => {
return location.pathname === '/';
}, [location]);
};

View File

@@ -1,5 +1,4 @@
import { features, navigation, faqs, footerNavigation, testimonials } from './contentSections'; import { features, faqs, footerNavigation, testimonials } from './contentSections';
import Header from './components/Header';
import Hero from './components/Hero'; import Hero from './components/Hero';
import Clients from './components/Clients'; import Clients from './components/Clients';
import Features from './components/Features'; import Features from './components/Features';
@@ -10,8 +9,6 @@ import Footer from './components/Footer';
export default function LandingPage() { export default function LandingPage() {
return ( return (
<div className='bg-white dark:text-white dark:bg-boxdark-2'> <div className='bg-white dark:text-white dark:bg-boxdark-2'>
<Header navigation={navigation} />
<main className='isolate dark:bg-boxdark-2'> <main className='isolate dark:bg-boxdark-2'>
<Hero /> <Hero />
<Clients /> <Clients />
@@ -19,7 +16,6 @@ export default function LandingPage() {
<Testimonials testimonials={testimonials} /> <Testimonials testimonials={testimonials} />
<FAQ faqs={faqs} /> <FAQ faqs={faqs} />
</main> </main>
<Footer footerNavigation={footerNavigation} /> <Footer footerNavigation={footerNavigation} />
</div> </div>
); );

View File

@@ -1,127 +0,0 @@
import { useState } from 'react';
import { HiBars3 } from 'react-icons/hi2';
import { BiLogIn } from 'react-icons/bi';
import { AiFillCloseCircle } from 'react-icons/ai';
import { Dialog } from '@headlessui/react';
import { Link } from 'wasp/client/router';
import { useAuth } from 'wasp/client/auth';
import logo from '../../client/static/logo.webp';
import DarkModeSwitcher from '../../client/components/DarkModeSwitcher';
import DropdownUser from '../../user/DropdownUser';
import { UserMenuItems } from '../../user/UserMenuItems';
interface NavigationItem {
name: string;
href: string;
};
export default function Header({ navigation }: { navigation: NavigationItem[] }) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { data: user, isLoading: isUserLoading } = useAuth();
const NavLogo = () => <img className='h-8 w-8' src={logo} alt='Your SaaS App' />;
return (
<header className='absolute inset-x-0 top-0 z-50 dark:bg-boxdark-2'>
<nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'>
<div className='flex items-center lg:flex-1'>
<a
href='/'
className='flex items-center -m-1.5 p-1.5 text-gray-900 duration-300 ease-in-out hover:text-yellow-500'
>
<NavLogo />
<span className='ml-2 text-sm font-semibold leading-6 dark:text-white'>Your SaaS</span>
</a>
</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'>
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className='text-sm font-semibold leading-6 text-gray-900 duration-300 ease-in-out hover:text-yellow-500 dark:text-white'
>
{item.name}
</a>
))}
</div>
<div className='hidden lg:flex lg:flex-1 lg:justify-end lg:align-end'>
{/* <!-- Dark Mode Toggler --> */}
<div className='flex items-center gap-3 2xsm:gap-7'>
<ul className='flex justify-center items-center gap-2 2xsm:gap-4'>
<DarkModeSwitcher />
</ul>
{isUserLoading ? null : !user ? (
<Link to='/login'>
<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>
</Link>
) : (
<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 px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-boxdark dark:text-white'>
<div className='flex items-center justify-between'>
<a href='/' className='-m-1.5 p-1.5'>
<span className='sr-only'>Your SaaS</span>
<NavLogo />
</a>
<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'>
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
onClick={() => setMobileMenuOpen(false)}
className='-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'
>
{item.name}
</a>
))}
</div>
<div className='py-6'>
{isUserLoading ? null : !user ? (
<Link to='/login'>
<div className='flex justify-start 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>
</Link>
) : (
<UserMenuItems user={user} />
)}
</div>
<div className='py-6'>
<DarkModeSwitcher />
</div>
</div>
</div>
</Dialog.Panel>
</Dialog>
</header>
)
}

View File

@@ -1,13 +1,14 @@
import type { NavigationItem } from '../client/components/NavBar/NavBar';
import { routes } from 'wasp/client/router';
import { DocsUrl, BlogUrl } from '../shared/common'; import { DocsUrl, BlogUrl } from '../shared/common';
import daBoiAvatar from '../client/static/da-boi.webp'; import daBoiAvatar from '../client/static/da-boi.webp';
import avatarPlaceholder from '../client/static/avatar-placeholder.webp'; import avatarPlaceholder from '../client/static/avatar-placeholder.webp';
import { routes } from 'wasp/client/router';
export const navigation = [ export const landingPageNavigationItems: NavigationItem[] = [
{ name: 'Features', href: '#features' }, { name: 'Features', to: '#features' },
{ name: 'Pricing', href: routes.PricingPageRoute.build() }, { name: 'Pricing', to: routes.PricingPageRoute.to },
{ name: 'Documentation', href: DocsUrl }, { name: 'Documentation', to: DocsUrl },
{ name: 'Blog', href: BlogUrl }, { name: 'Blog', to: BlogUrl },
]; ];
export const features = [ export const features = [
{ {

View File

@@ -1,11 +1,11 @@
import { Link } from 'wasp/client/router'; import { Link as WaspRouterLink, routes } from 'wasp/client/router';
const MessageButton = () => { const MessageButton = () => {
return ( return (
<li className='relative' x-data='{ dropdownOpen: false, notifying: true }'> <li className='relative' x-data='{ dropdownOpen: false, notifying: true }'>
<Link <WaspRouterLink
className='relative flex h-8.5 w-8.5 items-center justify-center rounded-full border-[0.5px] border-stroke bg-gray hover:text-primary dark:border-strokedark dark:bg-meta-4 dark:text-white' className='relative flex h-8.5 w-8.5 items-center justify-center rounded-full border-[0.5px] border-stroke bg-gray hover:text-primary dark:border-strokedark dark:bg-meta-4 dark:text-white'
to='/admin/messages' to={routes.AdminMessagesRoute.to}
> >
<span className='absolute -top-0.5 -right-0.5 z-1 h-2 w-2 rounded-full bg-meta-1'> <span className='absolute -top-0.5 -right-0.5 z-1 h-2 w-2 rounded-full bg-meta-1'>
{/* TODO: only animate if there are new messages */} {/* TODO: only animate if there are new messages */}
@@ -37,7 +37,7 @@ const MessageButton = () => {
fill='' fill=''
/> />
</svg> </svg>
</Link> </WaspRouterLink>
</li> </li>
); );
}; };

View File

@@ -1,7 +1,7 @@
import type { User } from 'wasp/entities'; import type { User } from 'wasp/entities';
import { type SubscriptionStatus, prettyPaymentPlanName, parsePaymentPlanId } from '../payment/plans'; import { type SubscriptionStatus, prettyPaymentPlanName, parsePaymentPlanId } from '../payment/plans';
import { getCustomerPortalUrl, useQuery } from 'wasp/client/operations'; import { getCustomerPortalUrl, useQuery } from 'wasp/client/operations';
import { Link } from 'wasp/client/router'; import { Link as WaspRouterLink, routes } from 'wasp/client/router';
import { logout } from 'wasp/client/auth'; import { logout } from 'wasp/client/auth';
export default function AccountPage({ user }: { user: User }) { export default function AccountPage({ user }: { user: User }) {
@@ -107,9 +107,9 @@ function prettyPrintEndOfBillingPeriod(date: Date) {
function BuyMoreButton() { function BuyMoreButton() {
return ( return (
<div className='ml-4 flex-shrink-0 sm:col-span-1 sm:mt-0'> <div className='ml-4 flex-shrink-0 sm:col-span-1 sm:mt-0'>
<Link to='/pricing' className='font-medium text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-500'> <WaspRouterLink to={routes.PricingPageRoute.to} className='font-medium text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-500'>
Buy More/Upgrade Buy More/Upgrade
</Link> </WaspRouterLink>
</div> </div>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Link } from 'wasp/client/router'; import { Link as WaspRouterLink, routes } from 'wasp/client/router';
import { type User } from 'wasp/entities'; import { type User } from 'wasp/entities';
import { logout } from 'wasp/client/auth'; import { logout } from 'wasp/client/auth';
import { MdOutlineSpaceDashboard } from 'react-icons/md'; import { MdOutlineSpaceDashboard } from 'react-icons/md';
@@ -7,6 +7,8 @@ import { cn } from '../client/cn';
export const UserMenuItems = ({ user, setMobileMenuOpen }: { user?: Partial<User>; setMobileMenuOpen?: any }) => { export const UserMenuItems = ({ user, setMobileMenuOpen }: { user?: Partial<User>; setMobileMenuOpen?: any }) => {
const path = window.location.pathname; const path = window.location.pathname;
const landingPagePath = routes.LandingPageRoute.to;
const adminDashboardPath = routes.AdminRoute.to;
const handleMobileMenuClick = () => { const handleMobileMenuClick = () => {
if (setMobileMenuOpen) setMobileMenuOpen(false); if (setMobileMenuOpen) setMobileMenuOpen(false);
@@ -16,24 +18,24 @@ export const UserMenuItems = ({ user, setMobileMenuOpen }: { user?: Partial<User
<> <>
<ul <ul
className={cn('flex flex-col gap-5 border-b border-stroke py-4 dark:border-strokedark', { className={cn('flex flex-col gap-5 border-b border-stroke py-4 dark:border-strokedark', {
'sm:px-6': path !== '/admin', 'sm:px-6': path !== adminDashboardPath,
'px-6': path === '/admin', 'px-6': path === adminDashboardPath,
})} })}
> >
{path === '/' || path === '/admin' ? ( {path === landingPagePath || path === adminDashboardPath ? (
<li> <li>
<Link <WaspRouterLink
to='/demo-app' to={routes.DemoAppRoute.to}
className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500' className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500'
> >
<MdOutlineSpaceDashboard size='1.1rem' /> <MdOutlineSpaceDashboard size='1.1rem' />
AI Scheduler (Demo App) AI Scheduler (Demo App)
</Link> </WaspRouterLink>
</li> </li>
) : null} ) : null}
<li> <li>
<Link <WaspRouterLink
to='/account' to={routes.AccountRoute.to}
onClick={handleMobileMenuClick} onClick={handleMobileMenuClick}
className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500' className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500'
> >
@@ -55,25 +57,25 @@ export const UserMenuItems = ({ user, setMobileMenuOpen }: { user?: Partial<User
/> />
</svg> </svg>
Account Settings Account Settings
</Link> </WaspRouterLink>
</li> </li>
</ul> </ul>
{!!user && user.isAdmin && ( {!!user && user.isAdmin && (
<ul <ul
className={cn('flex flex-col gap-5 border-b border-stroke py-4 dark:border-strokedark', { className={cn('flex flex-col gap-5 border-b border-stroke py-4 dark:border-strokedark', {
'sm:px-6': path !== '/admin', 'sm:px-6': path !== adminDashboardPath,
'px-6': path === '/admin', 'px-6': path === adminDashboardPath,
})} })}
> >
<li className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500'> <li className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500'>
<Link <WaspRouterLink
to='/admin' to={routes.AdminRoute.to}
onClick={handleMobileMenuClick} onClick={handleMobileMenuClick}
className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500' className='flex items-center gap-3.5 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500'
> >
<TfiDashboard size='1.1rem' /> <TfiDashboard size='1.1rem' />
Admin Dashboard Admin Dashboard
</Link> </WaspRouterLink>
</li> </li>
</ul> </ul>
)} )}
@@ -82,8 +84,8 @@ export const UserMenuItems = ({ user, setMobileMenuOpen }: { user?: Partial<User
className={cn( className={cn(
'flex items-center gap-3.5 py-4 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500', 'flex items-center gap-3.5 py-4 text-sm font-medium duration-300 ease-in-out hover:text-yellow-500',
{ {
'sm:px-6': path !== '/admin', 'sm:px-6': path !== adminDashboardPath,
'px-6': path === '/admin', 'px-6': path === adminDashboardPath,
} }
)} )}
> >