move releases and expression of interest into landing (#848)

* move releases and expression of interest into landing

* fix eslint on landing

* fs routing kinda sucks

* fix waitlist link

* cringe
This commit is contained in:
Oscar Beaumont 2023-05-23 14:36:25 +08:00 committed by GitHub
parent a470844f85
commit 9fb72a1dc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1428 additions and 725 deletions

View file

@ -49,7 +49,7 @@
"active": false,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEZBMURCMkU5NEU3NDAyOEMKUldTTUFuUk82YklkK296dlkxUGkrTXhCT3ZMNFFVOWROcXNaS0RqWU1kMUdRV2tDdFdIS0Y3YUsK",
"endpoints": [
"https://releases-6oxwxxryr-spacedrive.vercel.app/{{target}}/{{arch}}/{{current_version}}"
"https://spacedrive.com/api/releases/{{target}}/{{arch}}/{{current_version}}"
]
},
"allowlist": {

View file

@ -0,0 +1,8 @@
import 'dotenv/config';
import { Config } from 'drizzle-kit';
import { env } from './src/env';
export default {
schema: ['./src/server/db.ts'],
connectionString: env.DATABASE_URL
} satisfies Config;

View file

@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -1,4 +1,6 @@
const { withContentlayer } = require('next-contentlayer');
import { withContentlayer } from 'next-contentlayer';
// Validate env on build
import './src/env.js';
/** @type {import('next').NextConfig} */
const nextConfig = {
@ -37,4 +39,4 @@ const nextConfig = {
}
};
module.exports = withContentlayer(nextConfig);
export default withContentlayer(nextConfig);

View file

@ -1,20 +1,27 @@
{
"name": "@sd/landing",
"type": "module",
"scripts": {
"dev": "next dev",
"build": "contentlayer build && next build",
"start": "next start",
"prod": "pnpm build && pnpm start",
"lint": "next lint",
"typecheck": "contentlayer build && tsc -b"
"typecheck": "contentlayer build && tsc -b",
"push": "drizzle-kit push:mysql"
},
"dependencies": {
"@aws-sdk/client-ses": "^3.337.0",
"@icons-pack/react-simple-icons": "^7.2.0",
"@planetscale/database": "^1.7.0",
"@sd/assets": "workspace:*",
"@sd/ui": "workspace:*",
"@t3-oss/env-nextjs": "^0.3.1",
"@vercel/edge-config": "^0.1.11",
"autoprefixer": "^10.4.14",
"clsx": "^1.2.1",
"contentlayer": "^0.3.2",
"drizzle-orm": "^0.26.0",
"markdown-to-jsx": "^7.2.0",
"next": "13.4.3",
"next-contentlayer": "^0.3.2",
@ -33,7 +40,8 @@
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sharp": "^0.32.1",
"tsparticles": "^2.9.3"
"tsparticles": "^2.9.3",
"zod": "^3.21.4"
},
"devDependencies": {
"@sd/config": "workspace:*",
@ -43,6 +51,7 @@
"@types/react-burger-menu": "^2.8.3",
"@types/react-dom": "18.2.4",
"@types/react-helmet": "^6.1.6",
"drizzle-kit": "db-push",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
"typescript": "5.0.4"

View file

@ -1,9 +1,7 @@
import { get } from '@vercel/edge-config';
import { NextResponse } from 'next/server';
export const config = {
runtime: 'edge'
};
export const runtime = 'edge';
export async function GET(
_: Request,

View file

@ -0,0 +1,112 @@
import type { NextRequest } from 'next/server';
import { z } from 'zod';
import { sendEmail } from '~/server/aws';
import { db, eq, waitlistTable } from '~/server/db';
import { welcomeTemplate } from './welcomeEmail';
export const runtime = 'edge';
const emailSchema = z.object({
email: z
.string({
required_error: 'Email is required',
invalid_type_error: 'Email must be a string'
})
.email({
message: 'Invalid email address'
})
.transform((value) => value.toLowerCase())
});
function randomId(len = 10) {
if (len % 2 !== 0) throw new Error('len must be a multiple of 2');
const array = new Uint8Array(len / 2); // 1 char int to 2 chars hex
self.crypto.getRandomValues(array);
return [...array].map((c) => c.toString(16).padStart(2, '0')).join('');
}
export async function POST(req: NextRequest) {
const result = emailSchema.safeParse(await req.json());
if (!result.success) {
return new Response(
JSON.stringify({
message: result.error.toString()
}),
{
status: 400
}
);
}
const { email } = result.data;
try {
const emailExist = await db.select({ email: waitlistTable.email }).from(waitlistTable);
if (emailExist.length > 0) {
return new Response(undefined, {
status: 204
});
}
const unsubId = randomId(26);
await db.insert(waitlistTable).values({
cuid: unsubId,
email,
created_at: new Date()
});
await sendEmail(
email,
'Welcome to Spacedrive',
welcomeTemplate(`https://spacedrive.com/?wunsub=${unsubId}`)
);
return new Response(null, {
status: 204
});
} catch (err) {
console.error(err);
return new Response(
JSON.stringify({
message: 'Something went wrong while trying to create invite'
}),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
}
}
export async function DELETE(req: NextRequest) {
const url = new URL(req.url);
const id = url.searchParams.get('i');
if (!id)
return new Response(JSON.stringify(undefined), {
status: 400
});
try {
await db.delete(waitlistTable).where(eq(waitlistTable.cuid, id));
return new Response(null, {
status: 204
});
} catch (err) {
console.error(err);
return new Response(
JSON.stringify({
message: 'Something went wrong while trying to unsubscribe from waitlist'
}),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
}
}

File diff suppressed because one or more lines are too long

View file

@ -20,17 +20,12 @@ export function HomeCTA() {
const [waitlistSubmitted, setWaitlistSubmitted] = useState(false);
const [fire, setFire] = useState<boolean | number>(false);
const url =
process.env.NODE_ENV === 'production'
? 'https://waitlist-api.spacedrive.com'
: 'http://localhost:3000';
async function handleWaitlistSubmit<SubmitHandler>({ email }: WaitlistInputs) {
if (!email.trim().length) return;
setLoading(true);
const req = await fetch(`${url}/api/waitlist`, {
const req = await fetch(`/api/waitlist`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'

31
apps/landing/src/env.js Normal file
View file

@ -0,0 +1,31 @@
// @ts-check
//
// Has to be `.mjs` so it can be imported in `next.config.mjs`.
// Next.js are so cringe for not having support for Typescript config files.
//
// Using `.mjs` with Drizzle Kit is seemingly impossible without `.ts` so we resort to `.js`.
// Why does JS make this shit so hard, I just wanna import the file.
//
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
AWS_SES_ACCESS_KEY: z.string(),
AWS_SES_SECRET_KEY: z.string(),
AWS_SES_REGION: z.string(),
MAILER_FROM: z.string().default('Spacedrive <no-reply@spacedrive.com>')
},
client: {},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
AWS_SES_ACCESS_KEY: process.env.AWS_SES_ACCESS_KEY,
AWS_SES_SECRET_KEY: process.env.AWS_SES_SECRET_KEY,
AWS_SES_REGION: process.env.AWS_SES_REGION,
MAILER_FROM: process.env.MAILER_FROM
},
// In dev or in eslint disable checking.
// Kinda sucks for in dev but you don't need the whole setup to change the docs.
skipValidation: process.env.VERCEL !== '1'
});

View file

@ -1,13 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

View file

@ -57,9 +57,8 @@ export default function HomePage() {
(async () => {
console.log('Unsubscribing from waitlist', process.env.NODE_ENV);
const prod = process.env.NODE_ENV === 'production';
const url = prod ? 'https://waitlist-api.spacedrive.com' : 'http://localhost:3000';
const req = await fetch(`${url}/api/waitlist?i=${cuid}`, {
const req = await fetch(`/api/waitlist?i=${cuid}`, {
method: 'DELETE'
});

View file

@ -0,0 +1,33 @@
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
import { env } from '~/env';
export const ses = new SESClient({
region: env.AWS_SES_REGION,
credentials: {
accessKeyId: env.AWS_SES_ACCESS_KEY,
secretAccessKey: env.AWS_SES_SECRET_KEY
}
});
export async function sendEmail(email: string, subject: string, body: string) {
await ses.send(
new SendEmailCommand({
Destination: {
ToAddresses: [email]
},
Message: {
Body: {
Html: {
Charset: 'UTF-8',
Data: body
}
},
Subject: {
Charset: 'UTF-8',
Data: subject
}
},
Source: env.MAILER_FROM
})
);
}

View file

@ -0,0 +1,23 @@
import { connect } from '@planetscale/database';
import { mysqlTable, serial, timestamp, varchar } from 'drizzle-orm/mysql-core';
import { drizzle } from 'drizzle-orm/planetscale-serverless';
import { env } from '~/env';
export { eq, and, or, type InferModel } from 'drizzle-orm';
const dbConnection = connect({
url: env.DATABASE_URL
});
export const db = drizzle(dbConnection);
export const waitlistTable = mysqlTable('waitlist', {
id: serial('id').primaryKey(),
cuid: varchar('cuid', {
length: 26
}).notNull(),
email: varchar('email', {
length: 255
}).notNull(),
created_at: timestamp('created_at').notNull()
});

View file

@ -21,8 +21,20 @@
"paths": {
"~/*": ["./src/*"],
"@contentlayer/generated": ["./.contentlayer/generated"]
}
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"src/env.js",
"src/drizzle.config.ts",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}

View file

@ -1,35 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View file

@ -1,8 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true
}
};
module.exports = nextConfig;

View file

@ -1,23 +0,0 @@
{
"name": "releases",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint --strict"
},
"dependencies": {
"@types/node": "18.15.11",
"@types/react": "18.0.35",
"@types/react-dom": "18.0.11",
"@vercel/edge-config": "^0.1.7",
"eslint-config-next": "13.3.0",
"next": "13.3.0",
"octokit": "^2.0.14",
"react": "18.2.0",
"react-dom": "^18.2.0",
"typescript": "5.0.4"
}
}

View file

@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"~/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load diff