Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7614244df5 |
@ -1,6 +1,5 @@
|
||||
import React, {useEffect, useRef} from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||
import BaseDivider from './BaseDivider'
|
||||
import BaseIcon from './BaseIcon'
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||
import menuAside from '../menuAside'
|
||||
|
||||
@ -1,166 +1,403 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { FormEvent, ReactElement, useEffect, useMemo, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
mdiArrowRight,
|
||||
mdiCheckCircle,
|
||||
mdiClipboardTextOutline,
|
||||
mdiRocketLaunchOutline,
|
||||
mdiShieldCheckOutline,
|
||||
mdiStarFourPoints,
|
||||
mdiViewDashboardOutline,
|
||||
} from '@mdi/js';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import CardBox from '../components/CardBox';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
|
||||
type Brief = {
|
||||
id: string;
|
||||
idea: string;
|
||||
stage: string;
|
||||
priority: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
const stageOptions = ['Idea', 'Prototype', 'Live product'];
|
||||
const priorityOptions = ['Find users', 'Launch faster', 'Automate operations'];
|
||||
|
||||
const workflowSteps = [
|
||||
'Capture the product direction',
|
||||
'Confirm a focused MVP slice',
|
||||
'Track launch-ready tasks',
|
||||
'Continue in the admin workspace',
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: 'Domain workflow first',
|
||||
text: 'Start with a visible customer journey instead of another generic CRUD table.',
|
||||
},
|
||||
{
|
||||
title: 'Built on your admin core',
|
||||
text: 'Use auth, roles, search, tables, and data tools that already exist in this app.',
|
||||
},
|
||||
{
|
||||
title: 'Ready for iteration',
|
||||
text: 'Turn the selected slice into real entities, API routes, and dashboards as requirements sharpen.',
|
||||
},
|
||||
];
|
||||
|
||||
const makePlan = (brief: Brief | null) => {
|
||||
const priority = brief?.priority || 'Launch faster';
|
||||
|
||||
if (priority === 'Find users') {
|
||||
return [
|
||||
'Publish a landing page with one clear audience and promise.',
|
||||
'Collect early adopter notes in the admin workspace.',
|
||||
'Review requests weekly and convert the strongest ones into features.',
|
||||
];
|
||||
}
|
||||
|
||||
if (priority === 'Automate operations') {
|
||||
return [
|
||||
'Map the repeated manual task into a create → review → status workflow.',
|
||||
'Use role permissions to separate operators, managers, and admins.',
|
||||
'Add dashboard metrics once real workflow records are flowing.',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'Ship one end-to-end path before polishing every module.',
|
||||
'Reuse generated CRUD as the admin control room.',
|
||||
'Add only the missing business rules after the first workflow is validated.',
|
||||
];
|
||||
};
|
||||
|
||||
export default function Starter() {
|
||||
const [illustrationImage, setIllustrationImage] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
photographer_url: undefined,
|
||||
})
|
||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
||||
const [contentType, setContentType] = useState('image');
|
||||
const [contentPosition, setContentPosition] = useState('left');
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
const [idea, setIdea] = useState('AI-powered client onboarding portal');
|
||||
const [stage, setStage] = useState(stageOptions[0]);
|
||||
const [priority, setPriority] = useState(priorityOptions[1]);
|
||||
const [briefs, setBriefs] = useState<Brief[]>([]);
|
||||
const [selectedBriefId, setSelectedBriefId] = useState<string | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const title = 'App Preview'
|
||||
useEffect(() => {
|
||||
try {
|
||||
const storedBriefs = window.localStorage.getItem('flatlogic-mvp-briefs');
|
||||
if (storedBriefs) {
|
||||
const parsedBriefs = JSON.parse(storedBriefs) as Brief[];
|
||||
setBriefs(parsedBriefs);
|
||||
setSelectedBriefId(parsedBriefs[0]?.id || null);
|
||||
}
|
||||
} catch (localStorageError) {
|
||||
console.error('Unable to load saved MVP briefs:', localStorageError);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem('flatlogic-mvp-briefs', JSON.stringify(briefs));
|
||||
} catch (localStorageError) {
|
||||
console.error('Unable to save MVP briefs:', localStorageError);
|
||||
}
|
||||
}, [briefs]);
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div
|
||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||
style={{
|
||||
backgroundImage: `${
|
||||
image
|
||||
? `url(${image?.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={image?.photographer_url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Photo by {image?.photographer} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const selectedBrief = useMemo(
|
||||
() => briefs.find((brief) => brief.id === selectedBriefId) || briefs[0] || null,
|
||||
[briefs, selectedBriefId],
|
||||
);
|
||||
|
||||
const videoBlock = (video) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
return (
|
||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||
<video
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
>
|
||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={video?.user?.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
const plan = useMemo(() => makePlan(selectedBrief), [selectedBrief]);
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const trimmedIdea = idea.trim();
|
||||
|
||||
if (trimmedIdea.length < 8) {
|
||||
setError('Describe the project in at least 8 characters so the MVP slice is meaningful.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newBrief: Brief = {
|
||||
id: `${Date.now()}`,
|
||||
idea: trimmedIdea,
|
||||
stage,
|
||||
priority,
|
||||
createdAt: new Date().toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}),
|
||||
};
|
||||
|
||||
setBriefs((currentBriefs) => [newBrief, ...currentBriefs].slice(0, 4));
|
||||
setSelectedBriefId(newBrief.id);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const clearBriefs = () => {
|
||||
setBriefs([]);
|
||||
setSelectedBriefId(null);
|
||||
setError('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
contentPosition === 'background'
|
||||
? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle('Launch Studio')}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Plan the first valuable SaaS workflow and continue into your Flatlogic admin workspace."
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{contentType === 'image' && contentPosition !== 'background'
|
||||
? imageBlock(illustrationImage)
|
||||
: null}
|
||||
{contentType === 'video' && contentPosition !== 'background'
|
||||
? videoBlock(illustrationVideo)
|
||||
: null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your App Preview app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
<main className="min-h-screen overflow-hidden bg-[#07111f] text-white">
|
||||
<div className="absolute inset-0 overflow-hidden" aria-hidden="true">
|
||||
<div className="absolute -left-24 top-10 h-72 w-72 rounded-full bg-[#33D6A6]/20 blur-3xl" />
|
||||
<div className="absolute right-0 top-24 h-96 w-96 rounded-full bg-[#7C3AED]/25 blur-3xl" />
|
||||
<div className="absolute bottom-0 left-1/3 h-80 w-80 rounded-full bg-[#FFB020]/10 blur-3xl" />
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="relative mx-auto flex min-h-screen w-full max-w-7xl flex-col px-5 py-6 sm:px-8 lg:px-10">
|
||||
<header className="flex items-center justify-between rounded-3xl border border-white/10 bg-white/10 px-4 py-3 shadow-2xl shadow-black/20 backdrop-blur md:px-6">
|
||||
<Link href="/" className="flex items-center gap-3 focus:outline-none focus:ring-2 focus:ring-[#33D6A6]">
|
||||
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#33D6A6] text-[#07111f] shadow-lg shadow-[#33D6A6]/30">
|
||||
<BaseIcon path={mdiRocketLaunchOutline} size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<span className="block text-sm font-semibold uppercase tracking-[0.28em] text-[#33D6A6]">
|
||||
Flatlogic
|
||||
</span>
|
||||
<span className="block text-lg font-black tracking-tight">Launch Studio</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-2 text-sm font-semibold">
|
||||
<Link
|
||||
href="#planner"
|
||||
className="hidden rounded-full px-4 py-2 text-slate-200 transition hover:bg-white/10 hover:text-white focus:outline-none focus:ring-2 focus:ring-[#33D6A6] sm:inline-flex"
|
||||
>
|
||||
Plan MVP
|
||||
</Link>
|
||||
<BaseButton href="/login" label="Admin login" color="info" roundedFull />
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section className="grid flex-1 items-center gap-10 py-14 lg:grid-cols-[1.04fr_0.96fr] lg:py-20">
|
||||
<div className="space-y-8">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-[#33D6A6]/30 bg-[#33D6A6]/10 px-4 py-2 text-sm font-semibold text-[#91F2D6]">
|
||||
<BaseIcon path={mdiStarFourPoints} size={18} />
|
||||
First workflow, not just first screen
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<h1 className="max-w-4xl text-5xl font-black leading-[0.96] tracking-tight text-white sm:text-6xl lg:text-7xl">
|
||||
Turn your SaaS seed into a launchable product direction.
|
||||
</h1>
|
||||
<p className="max-w-2xl text-lg leading-8 text-slate-300 sm:text-xl">
|
||||
Use this quick planner to capture the MVP slice, confirm the next workflow, and jump into the existing admin interface when you are ready to build.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BaseButtons type="justify-start" className="gap-2" classAddon="mr-0 mb-3">
|
||||
<BaseButton href="#planner" label="Create MVP brief" color="success" roundedFull />
|
||||
<BaseButton href="/login" label="Open admin workspace" color="white" outline roundedFull />
|
||||
</BaseButtons>
|
||||
|
||||
<div className="grid max-w-3xl gap-3 sm:grid-cols-4">
|
||||
{workflowSteps.map((step, index) => (
|
||||
<div key={step} className="rounded-2xl border border-white/10 bg-white/10 p-4 backdrop-blur">
|
||||
<div className="mb-3 flex h-8 w-8 items-center justify-center rounded-xl bg-white text-sm font-black text-[#07111f]">
|
||||
{index + 1}
|
||||
</div>
|
||||
<p className="text-sm font-semibold leading-5 text-slate-200">{step}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardBox className="border-white/10 bg-white text-slate-950 shadow-2xl shadow-black/30" cardBoxClassName="p-0">
|
||||
<div id="planner" className="grid gap-0 overflow-hidden rounded-3xl lg:grid-cols-[0.92fr_1.08fr]">
|
||||
<form onSubmit={handleSubmit} className="space-y-5 bg-white p-6 sm:p-8">
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-bold uppercase tracking-[0.24em] text-[#7C3AED]">
|
||||
MVP brief
|
||||
</p>
|
||||
<h2 className="text-3xl font-black tracking-tight text-slate-950">What should we ship first?</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-500">
|
||||
This stays in your browser for now, so you can shape the workflow before adding permanent data models.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-bold text-slate-700">Project idea</span>
|
||||
<textarea
|
||||
value={idea}
|
||||
onChange={(event) => setIdea(event.target.value)}
|
||||
rows={4}
|
||||
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-[#33D6A6] focus:bg-white focus:ring-4 focus:ring-[#33D6A6]/20"
|
||||
placeholder="Example: marketplace for boutique fitness instructors"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-bold text-slate-700">Current stage</span>
|
||||
<select
|
||||
value={stage}
|
||||
onChange={(event) => setStage(event.target.value)}
|
||||
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm font-semibold text-slate-900 outline-none transition focus:border-[#33D6A6] focus:bg-white focus:ring-4 focus:ring-[#33D6A6]/20"
|
||||
>
|
||||
{stageOptions.map((option) => (
|
||||
<option key={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-bold text-slate-700">Primary goal</span>
|
||||
<select
|
||||
value={priority}
|
||||
onChange={(event) => setPriority(event.target.value)}
|
||||
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm font-semibold text-slate-900 outline-none transition focus:border-[#33D6A6] focus:bg-white focus:ring-4 focus:ring-[#33D6A6]/20"
|
||||
>
|
||||
{priorityOptions.map((option) => (
|
||||
<option key={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm font-semibold text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<BaseButton type="submit" label="Generate first slice" color="success" className="w-full" roundedFull />
|
||||
</form>
|
||||
|
||||
<div className="bg-[#F6F8FB] p-6 sm:p-8">
|
||||
<div className="mb-5 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-bold uppercase tracking-[0.24em] text-slate-400">Confirmation</p>
|
||||
<h3 className="text-2xl font-black tracking-tight text-slate-950">Launch board</h3>
|
||||
</div>
|
||||
<span className="rounded-full bg-[#33D6A6]/15 px-3 py-1 text-xs font-black text-[#087A5E]">
|
||||
{briefs.length || 'No'} saved
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{selectedBrief ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-3xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<div className="mb-4 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-[#7C3AED]">
|
||||
Selected slice
|
||||
</p>
|
||||
<h4 className="mt-1 text-xl font-black leading-7 text-slate-950">{selectedBrief.idea}</h4>
|
||||
</div>
|
||||
<BaseIcon path={mdiClipboardTextOutline} size={28} className="text-[#7C3AED]" />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 text-xs font-bold">
|
||||
<span className="rounded-full bg-slate-100 px-3 py-1 text-slate-700">{selectedBrief.stage}</span>
|
||||
<span className="rounded-full bg-[#FFB020]/15 px-3 py-1 text-[#9A5B00]">
|
||||
{selectedBrief.priority}
|
||||
</span>
|
||||
<span className="rounded-full bg-[#33D6A6]/15 px-3 py-1 text-[#087A5E]">
|
||||
{selectedBrief.createdAt}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<p className="mb-4 text-sm font-black text-slate-950">Recommended next actions</p>
|
||||
<div className="space-y-3">
|
||||
{plan.map((item) => (
|
||||
<div key={item} className="flex gap-3 text-sm leading-6 text-slate-600">
|
||||
<BaseIcon path={mdiCheckCircle} size={20} className="mt-0.5 shrink-0 text-[#33D6A6]" />
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{briefs.map((brief) => (
|
||||
<button
|
||||
key={brief.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedBriefId(brief.id)}
|
||||
className={`rounded-2xl border p-4 text-left transition focus:outline-none focus:ring-4 focus:ring-[#33D6A6]/20 ${
|
||||
selectedBrief.id === brief.id
|
||||
? 'border-[#33D6A6] bg-white shadow-sm'
|
||||
: 'border-slate-200 bg-white/70 hover:bg-white'
|
||||
}`}
|
||||
>
|
||||
<p className="line-clamp-2 text-sm font-black text-slate-900">{brief.idea}</p>
|
||||
<p className="mt-2 text-xs font-semibold text-slate-500">{brief.priority}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearBriefs}
|
||||
className="text-sm font-bold text-slate-500 underline-offset-4 hover:text-slate-900 hover:underline focus:outline-none focus:ring-2 focus:ring-[#33D6A6]"
|
||||
>
|
||||
Clear local briefs
|
||||
</button>
|
||||
<BaseButton href="/login" label="Continue in admin" icon={mdiArrowRight} color="info" roundedFull />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[440px] flex-col items-center justify-center rounded-3xl border border-dashed border-slate-300 bg-white p-8 text-center">
|
||||
<div className="mb-5 flex h-16 w-16 items-center justify-center rounded-3xl bg-[#33D6A6]/15 text-[#087A5E]">
|
||||
<BaseIcon path={mdiViewDashboardOutline} size={32} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-black text-slate-950">Your launch board is empty</h4>
|
||||
<p className="mt-3 max-w-sm text-sm leading-6 text-slate-500">
|
||||
Submit the brief to create a confirmation card, a task plan, and a quick path into the admin workspace.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 pb-12 md:grid-cols-3">
|
||||
{features.map((feature) => (
|
||||
<div key={feature.title} className="rounded-3xl border border-white/10 bg-white/10 p-6 shadow-xl shadow-black/10 backdrop-blur">
|
||||
<BaseIcon path={mdiShieldCheckOutline} size={28} className="mb-5 text-[#33D6A6]" />
|
||||
<h3 className="text-xl font-black tracking-tight text-white">{feature.title}</h3>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">{feature.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<footer className="flex flex-col items-center justify-between gap-4 border-t border-white/10 py-6 text-sm text-slate-400 sm:flex-row">
|
||||
<p>© 2026 Launch Studio. Built for your first focused SaaS iteration.</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/privacy-policy/" className="hover:text-white focus:outline-none focus:ring-2 focus:ring-[#33D6A6]">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link href="/login" className="font-bold text-[#91F2D6] hover:text-white focus:outline-none focus:ring-2 focus:ring-[#33D6A6]">
|
||||
Admin interface
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user