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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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
+++ opensaas-sh/app/src/auth/LoginPage.tsx
@@ -1,8 +1,14 @@
-import { Link } from 'react-router-dom';
@@ -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 { Navigate, Link } from 'react-router-dom';
+import { LoginForm, useAuth } from 'wasp/client/auth';
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
+++ 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 daBoiAvatar from '../client/static/da-boi.webp';
-import avatarPlaceholder from '../client/static/avatar-placeholder.webp';
-import { routes } from 'wasp/client/router';
+import { DocsUrl, BlogUrl, GithubUrl } from '../shared/common';
export const navigation = [
{ name: 'Features', href: '#features' },
- { name: 'Pricing', href: routes.PricingPageRoute.build() },
{ name: 'Documentation', href: DocsUrl },
{ name: 'Blog', href: BlogUrl },
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 = [
{

View File

@ -1,20 +1,18 @@
import { Link } from 'react-router-dom';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
interface BreadcrumbProps {
pageName: string;
}
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>
<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>
<nav>
<ol className="flex items-center gap-2">
<ol className='flex items-center gap-2'>
<li>
<Link to="/">Dashboard /</Link>
<WaspRouterLink to={routes.AdminRoute.to}>Dashboard /</WaspRouterLink>
</li>
<li className="text-primary">{pageName}</li>
<li className='text-primary'>{pageName}</li>
</ol>
</nav>
</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 { AuthPageLayout } from './AuthPageLayout';
@ -9,17 +9,17 @@ export default function Login() {
<br />
<span className='text-sm font-medium text-gray-900 dark:text-gray-900'>
Don't have an account yet?{' '}
<Link to='/signup' className='underline'>
<WaspRouterLink to={routes.SignupRoute.to} className='underline'>
go to signup
</Link>
</WaspRouterLink>
.
</span>
<br />
<span className='text-sm font-medium text-gray-900'>
Forgot your password?{' '}
<Link to='/request-password-reset' className='underline'>
<WaspRouterLink to={routes.RequestPasswordResetRoute.to} className='underline'>
reset it
</Link>
</WaspRouterLink>
.
</span>
</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 { AuthPageLayout } from './AuthPageLayout';
@ -9,9 +9,9 @@ export function Signup() {
<br />
<span className='text-sm font-medium text-gray-900'>
I already have an account (
<Link to='/login' className='underline'>
<WaspRouterLink to={routes.LoginRoute.to} className='underline'>
go to login
</Link>
</WaspRouterLink>
).
</span>
<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 { AuthPageLayout } from '../AuthPageLayout';
@ -8,7 +8,7 @@ export function EmailVerificationPage() {
<VerifyEmailForm />
<br />
<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>
</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 { AuthPageLayout } from '../AuthPageLayout';
@ -8,7 +8,7 @@ export function PasswordResetPage() {
<ResetPasswordForm />
<br />
<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>
</AuthPageLayout>
);

View File

@ -1,10 +1,14 @@
import { useAuth } from 'wasp/client/auth';
import { updateCurrentUser } from 'wasp/client/operations';
import './Main.css';
import AppNavBar from './components/AppNavBar';
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 { 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
@ -13,9 +17,11 @@ import { Outlet, useLocation } from 'react-router-dom';
export default function App() {
const location = useLocation();
const { data: user } = useAuth();
const isLandingPage = useIsLandingPage();
const navigationItems = isLandingPage ? landingPageNavigationItems : appNavigationItems;
const shouldDisplayAppNavBar = useMemo(() => {
return location.pathname !== '/' && location.pathname !== '/login' && location.pathname !== '/signup';
return location.pathname !== routes.LoginRoute.build() && location.pathname !== routes.SignupRoute.build();
}, [location]);
const isAdminDashboard = useMemo(() => {
@ -49,7 +55,7 @@ export default function App() {
<Outlet />
) : (
<>
{shouldDisplayAppNavBar && <AppNavBar />}
{shouldDisplayAppNavBar && <NavBar navigationItems={navigationItems} />}
<div className='mx-auto max-w-7xl sm:px-6 lg:px-8'>
<Outlet />
</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 { useState } from 'react';
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 DropdownUser from '../../user/DropdownUser';
import { UserMenuItems } from '../../user/UserMenuItems';
import { DocsUrl, BlogUrl } from '../../shared/common';
import DarkModeSwitcher from './DarkModeSwitcher';
import logo from '../../static/logo.webp';
import DropdownUser from '../../../user/DropdownUser';
import { UserMenuItems } from '../../../user/UserMenuItems';
import DarkModeSwitcher from '../DarkModeSwitcher';
import { useIsLandingPage } from '../../hooks/useIsLandingPage';
import { cn } from '../../cn';
const navigation = [
{ name: 'AI Scheduler (Demo App)', href: routes.DemoAppRoute.build() },
{ name: 'File Upload (AWS S3)', href: routes.FileUploadRoute.build() },
{ name: 'Pricing', href: routes.PricingPageRoute.build() },
{ name: 'Documentation', href: DocsUrl },
{ name: 'Blog', href: BlogUrl },
];
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() {
export default function AppNavBar({ navigationItems }: { navigationItems: NavigationItem[] }) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const isLandingPage = useIsLandingPage();
const { data: user, isLoading: isUserLoading } = useAuth();
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'>
<div className='flex lg:flex-1'>
<a href='/' className='-m-1.5 p-1.5'>
<img className='h-8 w-8' src={logo} alt='My SaaS App' />
</a>
<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'
>
<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
@ -43,30 +54,19 @@ export default function AppNavBar() {
<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: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 ? (
<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'>
Log in <BiLogIn size='1.1rem' className='ml-1 mt-[0.1rem]' />
</div>
</a>
</WaspRouterLink>
) : (
<div className='ml-4'>
<div className='ml-3'>
<DropdownUser user={user} />
</div>
)}
@ -76,10 +76,10 @@ export default function AppNavBar() {
<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'>
<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>
<NavLogo />
</a>
</WaspRouterLink>
<button
type='button'
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 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 hover:dark:bg-boxdark-2'
>
{item.name}
</a>
))}
</div>
<div className='space-y-2 py-6'>{renderNavigationItems(navigationItems, setMobileMenuOpen)}</div>
<div className='py-6'>
{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'>
Log in <BiLogIn size='1.1rem' className='ml-1' />
</div>
</Link>
</WaspRouterLink>
) : (
<UserMenuItems user={user} setMobileMenuOpen={setMobileMenuOpen} />
)}
@ -124,3 +113,28 @@ export default function AppNavBar() {
</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 { Link } from 'react-router-dom';
import { useAuth } from 'wasp/client/auth';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
export function NotFoundPage() {
const { data: user } = useAuth();
return (
<div className='flex items-center justify-center min-h-screen'>
<div className='text-center'>
<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>
<Link
to='/'
className='inline-block px-8 py-3 text-white bg-primary rounded-lg hover:bg-secondary transition duration-300'
<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'
>
Go Back Home
</Link>
</WaspRouterLink>
</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 Header from './components/Header';
import { features, faqs, footerNavigation, testimonials } from './contentSections';
import Hero from './components/Hero';
import Clients from './components/Clients';
import Features from './components/Features';
@ -10,8 +9,6 @@ import Footer from './components/Footer';
export default function LandingPage() {
return (
<div className='bg-white dark:text-white dark:bg-boxdark-2'>
<Header navigation={navigation} />
<main className='isolate dark:bg-boxdark-2'>
<Hero />
<Clients />
@ -19,7 +16,6 @@ export default function LandingPage() {
<Testimonials testimonials={testimonials} />
<FAQ faqs={faqs} />
</main>
<Footer footerNavigation={footerNavigation} />
</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 daBoiAvatar from '../client/static/da-boi.webp';
import avatarPlaceholder from '../client/static/avatar-placeholder.webp';
import { routes } from 'wasp/client/router';
export const navigation = [
{ name: 'Features', href: '#features' },
{ name: 'Pricing', href: routes.PricingPageRoute.build() },
{ name: 'Documentation', href: DocsUrl },
{ name: 'Blog', href: BlogUrl },
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 = [
{

View File

@ -1,11 +1,11 @@
import { Link } from 'wasp/client/router';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
const MessageButton = () => {
return (
<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'
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'>
{/* TODO: only animate if there are new messages */}
@ -37,7 +37,7 @@ const MessageButton = () => {
fill=''
/>
</svg>
</Link>
</WaspRouterLink>
</li>
);
};

View File

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