Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/newsletter #39

Merged
merged 7 commits into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
#Next
NEXT_PUBLIC_BASE_URL=https://localhost:3000

#Analytics
NEXT_PUBLIC_UMAMI_SCRIPT_URL=
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_WEBSITE_ID=

#Email
EMAIL_API_BASE=
NEXT_PUBLIC_EMAIL_API_KEY=
NEXT_PUBLIC_EMAIL_GROUP_ID=
3 changes: 1 addition & 2 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm lint
pnpm format
pnpm lint-staged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Share post component
- Lint staged files
- Tag generation and routing
- Newsletter feature
- MailerLite integration

### Changed

Expand Down
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ If you love this template and/or use it, please give it a star on GitHub. This w
- [Colors](#colors)
- [Metadata](#metadata)
- [Analytics](#analytics)
- [Vercel](#vercel)
- [Umami](#umami)
- [Other analytics providers](#other-analytics-providers)
- [Newsletter subscription](#newsletter-subscription)
- [MailerLite](#mailerlite)
- [Other newsletter providers](#other-newsletter-providers)
- [Hero section](#hero-section)
- [Other tips & tricks](#other-tips--tricks)
- [Image optimization](#image-optimization)
Expand Down Expand Up @@ -116,13 +121,22 @@ Umami is a simple, easy to use, web analytics solution with self-hosting option!
Configure:
Set `NEXT_PUBLIC_UMAMI_SCRIPT_URL` & `NEXT_PUBLIC_UMAMI_WEBSITE_ID` environment variables on your `.env.local` file and on Vercel dashboard.

#### Others
#### Other analytics providers

Supporting other analytics providers are in progress. Feel free to open an issue if you have any suggestions or a PR if you want to implement it yourself.

### Newsletter subscription

_WIP_ as I'm still deciding which email tools to support. Feel free to open an issue if you have any suggestions or a PR if you want to implement it yourself.
#### MailerLite

MailerLite is a simple email marketing tool for all types of businesses. You can read more about it on [MailerLite website](https://www.mailerlite.com/).

Configure:
Set `EMAIL_API_BASE`, `EMAIL_API_KEY`, and `EMAIL_GROUP_ID` environment variables on your `.env.local` file and on Vercel dashboard.

#### Other newsletter providers

Supporting other newsletter providers are in progress. Feel free to open an issue if you have any suggestions or a PR if you want to implement it yourself.

### Hero section

Expand Down Expand Up @@ -175,11 +189,14 @@ Note: DO NOT overdo it. You can easily make images look bad with lossy compressi
- [x] Social icons component
- [x] Social sharing buttons
- [x] Tags
- [ ] newsletter integration (form, api route, keys, welcome page, previous issues)
- [x] newsletter integration (form, api route, keys, thank you/welcome page, MailerLite provider)
- [ ] Other newsletter providers (Convertkit, Substack, Buttondown, Mailchimp, etc)
- [ ] Other analytics providers (fathom, simplelytics, plausible, etc)
- [ ] CLI and/or recipes
- [ ] Post series page
- [ ] prev/next post links
- [ ] related posts
- [ ] Newsletter previous issues page
- [ ] Layouts/templates system
- [ ] Notion data source
- [ ] Sanity data source
Expand Down
4 changes: 2 additions & 2 deletions app/(site)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { ArrowRight } from "lucide-react";

import { defaultAuthor } from "@/lib/metadata";
import { cn } from "@/lib/utils";
import CTA from "@/components/cta";
import { HeroImage } from "@/components/hero-image";
import { HeroMinimal } from "@/components/hero-minimal";
import { HeroSimple } from "@/components/hero-simple";
import { HeroVideo } from "@/components/hero-video";
import { Sidebar } from "@/components/home-sidebar";
import { Mdx } from "@/components/mdx-components";
import NewsletterSubscribe from "@/components/newsletter-subscribe";
import PostPreview from "@/components/post-preview";

async function getAboutPage() {
Expand Down Expand Up @@ -61,7 +61,7 @@ export default async function Home() {
</aside>
</div>
</div>
<CTA
<NewsletterSubscribe
title="I also write deep dives in email"
description="I write about coding, design, digital nomad life, and solopreneurship. Join over 1,000 other developers in
getting better in business. Unsubscribe whenever."
Expand Down
27 changes: 27 additions & 0 deletions app/(site)/thank-you/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { useRouter } from "next/navigation";

import { Button } from "@/components/ui/button";

export default function ThankYou() {
const router = useRouter();

return (
<div className="mb-16 items-center justify-center text-center">
<span className="bg-gradient-to-b from-foreground to-transparent bg-clip-text text-[10rem] font-extrabold leading-none text-transparent">
Thank You!
</span>
<h2 className="my-2 font-heading text-2xl font-bold">Welcome to the newsletter</h2>
<p>You&apos;re now receiving.</p>
<div className="mt-8 flex justify-center gap-2">
<Button onClick={() => router.back()} variant="default" size="lg">
Go back
</Button>
<Button onClick={() => router.push("/")} variant="ghost" size="lg">
Back to Home
</Button>
</div>
</div>
);
}
4 changes: 2 additions & 2 deletions app/(social)/social/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Mail } from "lucide-react";
import { defaultAuthor } from "@/lib/metadata";
import { projects } from "@/lib/projects-data";
import { CopyButton } from "@/components/copy-button";
import CTA from "@/components/cta";
import NewsletterSubscribe from "@/components/newsletter-subscribe";
import { Signature } from "@/components/signature";
import { SocialButton } from "@/components/social-button";
import { SpotlightCard } from "@/components/spotlight-card";
Expand Down Expand Up @@ -39,7 +39,7 @@ export default async function SocialPage() {
<Signature />
</div>
</div>
<CTA
<NewsletterSubscribe
title="I also write deep dives in email"
description="I write about coding, design, digital nomad life, and solopreneurship. Join over 1,000 other developers in
getting better in business. Unsubscribe whenever."
Expand Down
30 changes: 30 additions & 0 deletions app/newsletter/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextResponse } from "next/server";

const API_URL = `${process.env.EMAIL_API_BASE}api/subscribers`;

export async function POST(request: Request) {
const { email } = await request.json();

if (!email) {
return NextResponse.json({ error: "Email is required" }, { status: 400 });
}
try {
const res = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${process.env.EMAIL_API_KEY || "You didn't set the API key"}`,
},
body: JSON.stringify({ email, groups: [process.env.EMAIL_GROUP_ID || ""] }),
});

if (!res.ok) {
throw new Error(res.statusText);
}
return NextResponse.json({ ok: "ok" }, { status: 200 });
} catch (error: any) {
console.log(error);
return NextResponse.json({ error: error.message || error.toString() }, { status: 500 });
}
}
36 changes: 21 additions & 15 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { MetadataRoute } from "next";
import { allPages, allPosts } from "@/.contentlayer/generated";

import { tagOptions } from "@/lib/content-definitions/post";
import { BASE_URL } from "@/lib/metadata";

export default function sitemap(): MetadataRoute.Sitemap {
const posts = allPosts
.filter((post) => post.status === "published")
.map((post) => ({
url: `${BASE_URL}/posts/${post.slug}`,
lastModified: post.lastUpdatedDate,
}));
const now = new Date();
const loadedPosts = allPosts.filter((post) => post.status === "published");
const tags = tagOptions.map((tag) => ({
url: `${BASE_URL}/tags/${tag}`,
lastModified: now,
}));
const posts = loadedPosts.map((post) => ({
url: `${BASE_URL}/posts/${post.slug}`,
lastModified: post.lastUpdatedDate,
}));
const pages = allPages
.filter((page) => page.status === "published")
.map((page) => ({
Expand All @@ -19,29 +24,30 @@ export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: BASE_URL,
lastModified: new Date(),
},
{
url: `${BASE_URL}/about`,
lastModified: new Date(),
lastModified: now,
},
{
url: `${BASE_URL}/projects`,
lastModified: new Date(),
lastModified: now,
},
{
url: `${BASE_URL}/uses`,
lastModified: new Date(),
lastModified: now,
},
{
url: `${BASE_URL}/social`,
lastModified: new Date(),
lastModified: now,
},
...pages,
{
url: `${BASE_URL}/posts`,
lastModified: new Date(),
lastModified: now,
},
...posts,
{
url: `${BASE_URL}/tags`,
lastModified: now,
},
...tags,
];
}
4 changes: 2 additions & 2 deletions components/mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Code } from "bright";
import { useMDXComponent } from "next-contentlayer/hooks";

import { fileIcons } from "@/lib/bright-config";
import CTA from "@/components/cta";
import NewsletterSubscribe from "@/components/newsletter-subscribe";

function YouTubeVideo({ id }: { id: string }) {
return (
Expand Down Expand Up @@ -44,7 +44,7 @@ Code.lineNumbers = true;

const components = {
Image,
CTA,
NewsletterSubscribe,
YouTubeVideo,
pre: Code,
// a: CustomLink,
Expand Down
2 changes: 1 addition & 1 deletion components/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function Navigation() {
<>
<header
className={cn(
"fixed -bottom-32 z-20 mx-auto mb-4 w-full px-4 animate-out delay-500 sm:static sm:z-auto sm:mb-0 sm:mt-4 sm:h-16 sm:max-w-6xl sm:transition-none sm:delay-0 lg:px-0",
"fixed -bottom-32 z-20 mx-auto mb-4 w-full px-4 delay-500 animate-out sm:static sm:z-auto sm:mb-0 sm:mt-4 sm:h-16 sm:max-w-6xl sm:transition-none sm:delay-0 lg:px-0",
visible && "bottom-0 left-0 animate-in"
)}
>
Expand Down
42 changes: 27 additions & 15 deletions components/cta.tsx → components/newsletter-subscribe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import siteMetadata from "@/lib/metadata";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ToastAction } from "@/components/ui/toast";
import { useToast } from "@/components/ui/use-toast";

interface CTAProps {
Expand All @@ -24,26 +23,39 @@ const formSchema = z.object({
email: z.string().email(),
});

const CTA = ({ title, description, buttonText }: CTAProps) => {
const NewsletterSubscribe = ({ title, description, buttonText }: CTAProps) => {
const { toast } = useToast();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
// TODO: Newsletter subscriptions
toast({
title: "In progress...",
description: "I'm still working on this feature.",
action: (
<ToastAction altText="Go to Newsletter page" asChild>
<Link href={siteMetadata.newsletterUrl}>Go to Newsletter page</Link>
</ToastAction>
),
const onSubmit = async (values: z.infer<typeof formSchema>) => {
const response = await fetch("/newsletter", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: values.email,
}),
});
}

if (!response?.ok) {
return toast({
title: "Something went wrong.",
description: "The subscription did not happen. Please try again.",
variant: "destructive",
});
}

return toast({
title: "Success.",
description: "You'll get the emails now.",
});
};

return (
<section className="relative isolate my-24 overflow-hidden bg-primary py-6 text-primary-foreground">
<div className="p-8 md:p-12">
Expand All @@ -68,7 +80,7 @@ const CTA = ({ title, description, buttonText }: CTAProps) => {
</FormItem>
)}
/>
<Button type="submit" variant="secondary" className="">
<Button type="submit" variant="secondary">
<Mail className="mr-2 h-4 w-4" /> {buttonText}
</Button>
</form>
Expand Down Expand Up @@ -107,4 +119,4 @@ const CTA = ({ title, description, buttonText }: CTAProps) => {
);
};

export default CTA;
export default NewsletterSubscribe;
4 changes: 2 additions & 2 deletions components/ui/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Form = FormProvider;

type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
Expand All @@ -19,7 +19,7 @@ const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFi

const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
Expand Down
Loading