mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-03-29 11:12:19 +01:00
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:
parent
304c49daff
commit
a8654e3d64
@ -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";
|
@ -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';
|
||||
|
||||
|
@ -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
|
@ -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>
|
||||
+ );
|
||||
+}
|
@ -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 = [
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
}
|
11
template/app/src/client/components/NavBar/contentSections.ts
Normal file
11
template/app/src/client/components/NavBar/contentSections.ts
Normal 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 },
|
||||
];
|
@ -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>
|
||||
);
|
||||
|
9
template/app/src/client/hooks/useIsLandingPage.tsx
Normal file
9
template/app/src/client/hooks/useIsLandingPage.tsx
Normal 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]);
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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 = [
|
||||
{
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
Loading…
x
Reference in New Issue
Block a user