Clean project from marketing content

This commit is contained in:
Sebastien Castiel
2024-01-18 18:11:24 -05:00
parent f9040f8bed
commit b999117ad8
9 changed files with 11 additions and 537 deletions

View File

@@ -1,9 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [{ hostname: 'avatars.githubusercontent.com' }],
},
}
const nextConfig = {}
const { withPlausibleProxy } = require('next-plausible')
module.exports = withPlausibleProxy()(nextConfig)
module.exports = nextConfig

View File

@@ -1,8 +1,3 @@
import {
FeedbackButton,
FeedbackModal,
} from '@/components/feedback-button/feedback-button'
import { env } from '@/lib/env'
import { PropsWithChildren } from 'react'
export default function GroupsLayout({ children }: PropsWithChildren<{}>) {
@@ -11,9 +6,6 @@ export default function GroupsLayout({ children }: PropsWithChildren<{}>) {
<main className="flex-1 max-w-screen-md w-full mx-auto px-4 py-6 flex flex-col gap-6">
{children}
</main>
<FeedbackModal donationUrl={env.STRIPE_DONATION_LINK}>
<FeedbackButton />
</FeedbackModal>
</>
)
}

View File

@@ -1,13 +1,10 @@
import { FeedbackModal } from '@/components/feedback-button/feedback-button'
import { ProgressBar } from '@/components/progress-bar'
import { ThemeProvider } from '@/components/theme-provider'
import { ThemeToggle } from '@/components/theme-toggle'
import { Button } from '@/components/ui/button'
import { Toaster } from '@/components/ui/toaster'
import { env } from '@/lib/env'
import { HeartFilledIcon } from '@radix-ui/react-icons'
import type { Metadata, Viewport } from 'next'
import PlausibleProvider from 'next-plausible'
import Image from 'next/image'
import Link from 'next/link'
import './globals.css'
@@ -67,9 +64,6 @@ export default function RootLayout({
}) {
return (
<html lang="en" suppressHydrationWarning>
{env.PLAUSIBLE_DOMAIN && (
<PlausibleProvider domain={env.PLAUSIBLE_DOMAIN} trackOutboundLinks />
)}
<body className="pt-16 min-h-[100dvh] flex flex-col items-stretch bg-slate-50 bg-opacity-30 dark:bg-background">
<ThemeProvider
attribute="class"
@@ -111,7 +105,7 @@ export default function RootLayout({
</div>
</header>
{children}
<div className="flex-1 flex flex-col">{children}</div>
<footer className="sm:p-8 md:p-16 sm:mt-16 sm:text-sm md:text-base md:mt-32 bg-slate-50 dark:bg-card border-t p-6 mt-8 flex flex-col sm:flex-row sm:justify-between gap-4 text-xs [&_a]:underline">
<div className="flex flex-col space-y-2">
@@ -142,17 +136,6 @@ export default function RootLayout({
contributors
</a>
</span>
<span>
<FeedbackModal
donationUrl={env.STRIPE_DONATION_LINK}
defaultTab="support"
>
<Button variant="link" className="text-pink-600 -mx-4">
<HeartFilledIcon className="w-4 h-4 mr-2" />
Support us
</Button>
</FeedbackModal>
</span>
</div>
</div>
</footer>

View File

@@ -1,17 +1,5 @@
import { Button } from '@/components/ui/button'
import {
BarChartHorizontalBig,
CircleDollarSign,
Divide,
FolderTree,
Github,
List,
LucideIcon,
Share,
ShieldX,
Users,
} from 'lucide-react'
import Image from 'next/image'
import { Github, LucideIcon } from 'lucide-react'
import Link from 'next/link'
import { ReactNode } from 'react'
@@ -28,164 +16,18 @@ export default function HomePage() {
& <strong>Family</strong>
</h1>
<p className="max-w-[42rem] leading-normal text-muted-foreground sm:text-xl sm:leading-8">
No ads. No account. <br className="sm:hidden" /> Open Source.
Forever Free.
Welcome to your new <strong>Spliit</strong> instance! <br />
Customize this page by editing <em>src/app/page.tsx</em>.
</p>
<div className="flex gap-2">
<Button asChild size="lg">
<Link
className="inline-flex items-center justify-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90 h-11 px-8 rounded-md"
href="/groups/create"
>
Create a group
</Link>
<Button asChild>
<Link href="/groups">Go to groups</Link>
</Button>
</div>
</div>
</section>
<section className="bg-slate-50 dark:bg-card py-16 md:py-24 lg:py-32">
<div className="p-4 flex mx-auto max-w-screen-md flex-col items-center text-center">
<h2 className="font-bold text-3xl leading-[1.1] sm:text-3xl md:text-6xl">
Features
</h2>
<p
className="mt-2 md:mt-3 leading-normal text-muted-foreground sm:text-lg sm:leading-7"
style={{ textWrap: 'balance' } as any}
>
Spliit is a minimalist application to track and share expenses with
your friends and family.
</p>
<div className="mt-8 md:mt-6 w-full grid grid-cols-2 sm:grid-cols-3 gap-2 sm:gap-4 text-left">
<Feature
Icon={Users}
name="Groups"
description="Create a group for a travel, an event, a gift…"
/>
<Feature
Icon={List}
name="Expenses"
description="Create and list expenses in your group."
/>
<Feature
Icon={FolderTree}
name="Categories"
description="Assign categories to your expenses."
/>
<Feature
Icon={Divide}
name="Advanced split"
description="Split expenses by percentage, shares or amount."
/>
<Feature
Icon={Share}
name="Share"
description="Send the group link to participants."
/>
<Feature
Icon={BarChartHorizontalBig}
name="Balances"
description="Visualize how much each participant spent."
/>
<Feature
Icon={CircleDollarSign}
name="Reimbursements"
description="Optimize money transfers between participants."
/>
<Feature
Icon={ShieldX}
name="No ads"
description="No account. No limitation. No problem."
/>
</div>
</div>
</section>
<section className="py-16 md:py-24 lg:py-32">
<div className="container flex max-w-screen-md flex-col items-center text-center">
<h2 className="font-bold text-3xl leading-[1.1] sm:text-3xl md:text-6xl">
Proudly Open Source
</h2>
<p
className="mt-2 leading-normal text-muted-foreground sm:text-lg sm:leading-7"
style={{ textWrap: 'balance' } as any}
>
Spliit is open source and lives thanks to amazing{' '}
<a
className="underline"
target="_blank"
href="https://github.com/scastiel/spliit2/graphs/contributors"
>
contributors
</a>
!
</p>
<ul className="flex gap-4 mt-6">
{[
{
avatar:
'https://avatars.githubusercontent.com/u/301948?s=120&v=4',
user: 'scastiel',
name: 'Sebastien Castiel',
},
{
avatar:
'https://avatars.githubusercontent.com/u/3932568?s=120&v=4',
user: 'ChristopherJohnston',
name: 'Chris Johnston',
},
{
avatar:
'https://avatars.githubusercontent.com/u/11523186?s=120&v=4',
user: 'acuteengle',
name: 'Brandon Eng',
},
{
avatar:
'https://avatars.githubusercontent.com/u/24687853?s=120&v=4',
user: 'Max-TheCat',
name: 'Max',
},
{
avatar:
'https://avatars.githubusercontent.com/u/10518723?s=120&v=4',
user: 'ankitbahl',
name: 'Ankit Bahl',
},
{
avatar:
'https://avatars.githubusercontent.com/u/13032812?s=120&v=4',
user: '174n',
name: 'Ivan Alexandrov',
},
].map((contributor) => (
<li key={contributor.user}>
<a
href={`https://github.com/${contributor.user}`}
target="_blank"
rel="nofollow"
>
<Image
src={contributor.avatar}
width={60}
height={60}
alt={contributor.user}
className="rounded-full border hover:scale-110 transition-transform"
/>
</a>
</li>
))}
</ul>
<div className="mt-4 md:mt-6">
<Button asChild variant="secondary" size="lg">
<a
target="_blank"
rel="noreferrer"
href="https://github.com/scastiel/spliit2"
>
<Button asChild variant="secondary">
<Link href="https://github.com/spliit-app/spliit">
<Github className="w-4 h-4 mr-2" />
GitHub
</a>
</Link>
</Button>
</div>
</div>

View File

@@ -1,23 +0,0 @@
'use server'
import { formSchema } from '@/components/feedback-button/feedback-button-common'
import { FeedbackButtonEmail } from '@/components/feedback-button/feedback-button-email'
import { getResend } from '@/lib/resend'
import { env } from 'process'
export async function sendFeedback(values: unknown) {
'use server'
const { email, message } = formSchema.parse(values)
const resend = getResend()
if (!resend || !env.FEEDBACK_EMAIL_FROM || !env.FEEDBACK_EMAIL_TO) {
console.warn(
'Resend is not properly configured. Feedback email wont be sent.',
)
return
}
await resend.emails.send({
from: env.FEEDBACK_EMAIL_FROM,
to: env.FEEDBACK_EMAIL_TO,
subject: `Spliit: new feedback from ${email || 'anonymous user'}`,
react: <FeedbackButtonEmail email={email} message={message} />,
})
}

View File

@@ -1,9 +0,0 @@
import { z } from 'zod'
export const formSchema = z.object({
email: z.union([
z.string().email('Please enter a valid email address.'),
z.string().max(0),
]),
message: z.string().min(10, 'Please enter at least 10 characters.').max(5000),
})

View File

@@ -1,24 +0,0 @@
import { Heading } from '@react-email/heading'
import { Html } from '@react-email/html'
import { Preview } from '@react-email/preview'
import { Text } from '@react-email/text'
type Props = {
email?: string
message: string
}
export function FeedbackButtonEmail({ email, message }: Props) {
return (
<Html>
<Preview>New feedback from {email || 'anonymous user'}</Preview>
<Heading>New feedback on Spliit</Heading>
<Text>
Email address: <strong>{email || 'not provided'}</strong>
</Text>
<pre style={{ padding: 16, borderLeft: '2px solid lightgray' }}>
{message}
</pre>
</Html>
)
}

View File

@@ -1,277 +0,0 @@
'use client'
import { sendFeedback } from '@/components/feedback-button/feedback-button-actions'
import { formSchema } from '@/components/feedback-button/feedback-button-common'
import { Button, ButtonProps } from '@/components/ui/button'
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea'
import { useToast } from '@/components/ui/use-toast'
import { useMediaQuery } from '@/lib/hooks'
import { zodResolver } from '@hookform/resolvers/zod'
import { Heart, HeartIcon, Loader2, MessageCircle } from 'lucide-react'
import { PropsWithChildren, ReactNode, SetStateAction, useState } from 'react'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
type FormValues = z.infer<typeof formSchema>
type Props = {
donationUrl: string
defaultTab?: 'feedback' | 'support'
}
export function FeedbackModal({
donationUrl,
defaultTab = 'feedback',
children,
}: PropsWithChildren<Props>) {
const { toast } = useToast()
const isDesktop = useMediaQuery('(min-width: 640px)')
const [open, setOpen] = useState(false)
async function onSubmit(values: FormValues) {
await sendFeedback(values)
toast({
title: 'Thank you for your feedback!',
description:
'We will have a look at it as soon as possible, and will get back to you if needed.',
})
}
const Wrapper = isDesktop ? FeedbackDialog : FeedbackDrawer
return (
<Wrapper open={open} setOpen={setOpen} button={children}>
<FeedbackContent
onSubmit={async (values) => {
await onSubmit(values)
setOpen(false)
}}
donationUrl={donationUrl}
defaultTab={defaultTab}
/>
</Wrapper>
)
}
function FeedbackDrawer({
children,
open,
setOpen,
button,
}: PropsWithChildren<{
open: boolean
setOpen: (open: SetStateAction<boolean>) => void
button: ReactNode
}>) {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>{button}</DrawerTrigger>
<DrawerContent>
<div className="p-4">{children}</div>
</DrawerContent>
</Drawer>
)
}
function FeedbackDialog({
children,
open,
setOpen,
button,
}: PropsWithChildren<{
open: boolean
setOpen: (open: SetStateAction<boolean>) => void
button: ReactNode
}>) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{button}</DialogTrigger>
<DialogContent>
<div className="pt-4">{children}</div>
</DialogContent>
</Dialog>
)
}
function FeedbackContent({
onSubmit,
donationUrl,
defaultTab,
}: {
onSubmit: (values: FormValues) => Promise<void>
donationUrl: string
defaultTab: 'feedback' | 'support'
}) {
return (
<Tabs defaultValue={defaultTab}>
<div className="mt-2 mb-6">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="feedback">Give feedback</TabsTrigger>
<TabsTrigger value="support">Support us</TabsTrigger>
</TabsList>
</div>
<TabsContent value="feedback">
<FeedbackForm onSubmit={onSubmit} />
</TabsContent>
<TabsContent value="support">
<DonationForm donationUrl={donationUrl} />
</TabsContent>
</Tabs>
)
}
function FeedbackForm({
onSubmit,
}: {
onSubmit: (values: FormValues) => Promise<void>
}) {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: { email: '', message: '' },
})
const isSubmitting = form.formState.isSubmitting
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-4">
<div>
<h2 className="text-lg font-semibold leading-none tracking-tight pb-1.5">
Give us your feedback
</h2>
<p className="text-sm text-muted-foreground">
We are always working to improve the user experience, and your
feedback helps us a lot.
</p>
</div>
<div className="grid gap-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Your email address</FormLabel>
<FormControl>
<Input
placeholder="your@email.com"
className="text-base"
{...field}
/>
</FormControl>
<FormDescription>
Optional. Provide it if you want us to get back to you.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Your feedback</FormLabel>
<FormControl>
<Textarea
placeholder="Enter your feedback"
className="text-base"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="text-center mt-1">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> Submitting
</>
) : (
<>
<MessageCircle className="w-4 h-4 mr-2" /> Send
</>
)}
</Button>
</div>
</form>
</Form>
)
}
function DonationForm({ donationUrl }: { donationUrl: string }) {
return (
<div className="flex flex-col gap-4">
<div>
<h2 className="text-lg font-semibold leading-none tracking-tight pb-1.5">
Support us
</h2>
<p className="text-sm text-muted-foreground">
Help keep <strong>Spliit</strong> free and without ads!
</p>
</div>
<div className="prose prose-sm dark:prose-invert">
<p>
Spliit is offered for free, but costs money and energy. If you like
the app, you can choose to support it by buying me (Sebastien) a
coffee with a one-time small donation.
</p>
<p>By supporting Spliit:</p>
<ul>
<li>
You contribute to the <strong>hosting costs</strong> for the app
(currently ~$150/year).
</li>
<li>
You help us keeping the application{' '}
<strong>free and without ads</strong>.
</li>
<li>
You give me energy to build <strong>new features</strong> and
improve the application.
</li>
</ul>
<p>
You will be redirected to <strong>Stripe</strong>, our payment
provider, where you can choose an amount to donate and complete the
payment.
</p>
</div>
<div className="text-center">
<Button
asChild
className="bg-pink-700 hover:bg-pink-600 dark:bg-pink-500 dark:hover:bg-pink-600"
>
<a href={donationUrl} target="_blank">
<Heart className="w-4 h-4 mr-2" /> Support us
</a>
</Button>
</div>
</div>
)
}
export function FeedbackButton({ ...props }: ButtonProps) {
return (
<Button
className="bg-pink-700 hover:bg-pink-600 dark:bg-pink-500 dark:hover:bg-pink-600 fixed right-0 bottom-4 rounded-r-none gap-2"
{...props}
>
<MessageCircle className="w-4 h-4" />
<HeartIcon className="w-4 h-4" />
</Button>
)
}

View File

@@ -4,11 +4,6 @@ const envSchema = z.object({
NEXT_PUBLIC_BASE_URL: z.string().url(),
POSTGRES_URL_NON_POOLING: z.string().url(),
POSTGRES_PRISMA_URL: z.string().url(),
PLAUSIBLE_DOMAIN: z.string().optional(),
FEEDBACK_EMAIL_FROM: z.string().email().optional(),
FEEDBACK_EMAIL_TO: z.string().email().optional(),
RESEND_API_KEY: z.string().optional(),
STRIPE_DONATION_LINK: z.string().optional().default('https://example.com'),
})
export const env = envSchema.parse(process.env)