Add new pages and refactor existing code
This commit adds new Admin and Accounts pages, while also improving code by refactoring various portions such as extracting services from the join page and dynamically importing packages in logging and monitoring code. The build command is also removed from the WordPress package, and SWC minification is enabled in the Next.js configuration. Updated marketing content is also included in this commit.
This commit is contained in:
@@ -28,8 +28,9 @@ function Home() {
|
|||||||
</Pill>
|
</Pill>
|
||||||
|
|
||||||
<HeroTitle>
|
<HeroTitle>
|
||||||
<span>The SaaS Solution for</span>
|
<span>The SaaS Starter Kit</span>
|
||||||
<span>developers and founders</span>
|
|
||||||
|
<span>straight from the future</span>
|
||||||
</HeroTitle>
|
</HeroTitle>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -221,8 +222,8 @@ function HeroTitle({ children }: React.PropsWithChildren) {
|
|||||||
return (
|
return (
|
||||||
<h1
|
<h1
|
||||||
className={
|
className={
|
||||||
'text-center text-4xl md:text-5xl' +
|
'text-center font-sans text-4xl md:text-5xl' +
|
||||||
' flex flex-col font-semibold xl:text-7xl'
|
' flex flex-col font-bold xl:text-7xl 2xl:text-[5rem]'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -232,14 +233,7 @@ function HeroTitle({ children }: React.PropsWithChildren) {
|
|||||||
|
|
||||||
function Pill(props: React.PropsWithChildren) {
|
function Pill(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<h2
|
<h2 className={'rounded-full px-4 py-2 text-sm shadow'}>
|
||||||
className={
|
|
||||||
'inline-flex w-auto items-center space-x-2' +
|
|
||||||
' rounded-full bg-gradient-to-br dark:from-gray-200 dark:via-gray-400' +
|
|
||||||
' bg-clip-text px-4 py-2 text-center text-sm dark:to-gray-700' +
|
|
||||||
' border font-normal text-muted-foreground shadow-sm dark:text-transparent'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
|
|||||||
35
apps/web/app/admin/_components/admin-sidebar.tsx
Normal file
35
apps/web/app/admin/_components/admin-sidebar.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Home, Users } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarItem,
|
||||||
|
} from '@kit/ui/sidebar';
|
||||||
|
|
||||||
|
import { AppLogo } from '~/components/app-logo';
|
||||||
|
|
||||||
|
export function AdminSidebar() {
|
||||||
|
return (
|
||||||
|
<Sidebar>
|
||||||
|
<SidebarContent className={'py-4'}>
|
||||||
|
<AppLogo href={'/admin'} />
|
||||||
|
</SidebarContent>
|
||||||
|
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarGroup label={'Admin'} collapsible={false}>
|
||||||
|
<SidebarItem end path={'/admin'} Icon={<Home className={'h-4'} />}>
|
||||||
|
Home
|
||||||
|
</SidebarItem>
|
||||||
|
|
||||||
|
<SidebarItem
|
||||||
|
path={'/admin/accounts'}
|
||||||
|
Icon={<Users className={'h-4'} />}
|
||||||
|
>
|
||||||
|
Accounts
|
||||||
|
</SidebarItem>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
apps/web/app/admin/accounts/[account]/page.tsx
Normal file
0
apps/web/app/admin/accounts/[account]/page.tsx
Normal file
10
apps/web/app/admin/accounts/page.tsx
Normal file
10
apps/web/app/admin/accounts/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
|
export default function AccountsPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title={'Accounts'} />
|
||||||
|
<PageBody></PageBody>;
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
apps/web/app/admin/layout.tsx
Normal file
7
apps/web/app/admin/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Page } from '@kit/ui/page';
|
||||||
|
|
||||||
|
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
|
||||||
|
|
||||||
|
export default function AdminLayout(props: React.PropsWithChildren) {
|
||||||
|
return <Page sidebar={<AdminSidebar />}>{props.children}</Page>;
|
||||||
|
}
|
||||||
11
apps/web/app/admin/page.tsx
Normal file
11
apps/web/app/admin/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
|
export default function AdminPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title={'Admin'} />
|
||||||
|
|
||||||
|
<PageBody></PageBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
apps/web/app/join/_lib/server/join-team.service.ts
Normal file
47
apps/web/app/join/_lib/server/join-team.service.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
|
|
||||||
|
export class JoinTeamService {
|
||||||
|
async isCurrentUserAlreadyInAccount(accountId: string) {
|
||||||
|
const client = getSupabaseServerComponentClient();
|
||||||
|
|
||||||
|
const { data } = await client
|
||||||
|
.from('accounts')
|
||||||
|
.select('id')
|
||||||
|
.eq('id', accountId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
return !!data?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInviteDataFromInviteToken(token: string) {
|
||||||
|
// we use an admin client to be able to read the pending membership
|
||||||
|
// without having to be logged in
|
||||||
|
const adminClient = getSupabaseServerComponentClient({ admin: true });
|
||||||
|
|
||||||
|
const { data: invitation, error } = await adminClient
|
||||||
|
.from('invitations')
|
||||||
|
.select<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
account: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
picture_url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
'id, expires_at, account: account_id !inner (id, name, slug, picture_url)',
|
||||||
|
)
|
||||||
|
.eq('invite_token', token)
|
||||||
|
.gte('expires_at', new Date().toISOString())
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!invitation ?? error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return invitation;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import { notFound, redirect } from 'next/navigation';
|
|||||||
|
|
||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
|
||||||
import { Logger } from '@kit/shared/logger';
|
|
||||||
import { requireUser } from '@kit/supabase/require-user';
|
import { requireUser } from '@kit/supabase/require-user';
|
||||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
import { AcceptInvitationContainer } from '@kit/team-accounts/components';
|
import { AcceptInvitationContainer } from '@kit/team-accounts/components';
|
||||||
@@ -15,6 +14,8 @@ import pathsConfig from '~/config/paths.config';
|
|||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
import { JoinTeamService } from './_lib/server/join-team.service';
|
||||||
|
|
||||||
interface Context {
|
interface Context {
|
||||||
searchParams: {
|
searchParams: {
|
||||||
invite_token: string;
|
invite_token: string;
|
||||||
@@ -47,19 +48,23 @@ async function JoinTeamAccountPage({ searchParams }: Context) {
|
|||||||
redirect(pathsConfig.auth.signUp + '?invite_token=' + token);
|
redirect(pathsConfig.auth.signUp + '?invite_token=' + token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const service = new JoinTeamService();
|
||||||
|
|
||||||
// the user is logged in, we can now check if the token is valid
|
// the user is logged in, we can now check if the token is valid
|
||||||
const invitation = await getInviteDataFromInviteToken(token);
|
const invitation = await service.getInviteDataFromInviteToken(token);
|
||||||
|
|
||||||
if (!invitation) {
|
if (!invitation) {
|
||||||
return <InviteNotFoundOrExpired />;
|
return <InviteNotFoundOrExpired />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to verify the user isn't already in the account
|
// we need to verify the user isn't already in the account
|
||||||
const isInAccount = await isCurrentUserAlreadyInAccount(
|
const isInAccount = await service.isCurrentUserAlreadyInAccount(
|
||||||
invitation.account.id,
|
invitation.account.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isInAccount) {
|
if (isInAccount) {
|
||||||
|
const { Logger } = await import('@kit/shared/logger');
|
||||||
|
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
{
|
{
|
||||||
name: 'join-team-account',
|
name: 'join-team-account',
|
||||||
@@ -97,56 +102,6 @@ async function JoinTeamAccountPage({ searchParams }: Context) {
|
|||||||
|
|
||||||
export default withI18n(JoinTeamAccountPage);
|
export default withI18n(JoinTeamAccountPage);
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that the current user is not already in the account by
|
|
||||||
* reading the document from the `accounts` table. If the user can read it
|
|
||||||
* it means they are already in the account.
|
|
||||||
* @param accountId
|
|
||||||
*/
|
|
||||||
async function isCurrentUserAlreadyInAccount(accountId: string) {
|
|
||||||
const client = getSupabaseServerComponentClient();
|
|
||||||
|
|
||||||
const { data } = await client
|
|
||||||
.from('accounts')
|
|
||||||
.select('id')
|
|
||||||
.eq('id', accountId)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
return !!data?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getInviteDataFromInviteToken(token: string) {
|
|
||||||
// we use an admin client to be able to read the pending membership
|
|
||||||
// without having to be logged in
|
|
||||||
const adminClient = getSupabaseServerComponentClient({ admin: true });
|
|
||||||
|
|
||||||
const { data: invitation, error } = await adminClient
|
|
||||||
.from('invitations')
|
|
||||||
.select<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
id: string;
|
|
||||||
account: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
picture_url: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
'id, expires_at, account: account_id !inner (id, name, slug, picture_url)',
|
|
||||||
)
|
|
||||||
.eq('invite_token', token)
|
|
||||||
.gte('expires_at', new Date().toISOString())
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (!invitation ?? error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return invitation;
|
|
||||||
}
|
|
||||||
|
|
||||||
function InviteNotFoundOrExpired() {
|
function InviteNotFoundOrExpired() {
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { registerInstrumentation } from '@kit/monitoring';
|
import { registerInstrumentation } from '@kit/monitoring';
|
||||||
|
|
||||||
export async function register() {
|
export function register() {
|
||||||
// Register monitoring instrumentation based on the
|
if (process.env.NEXT_RUNTIME !== 'nodejs') {
|
||||||
// MONITORING_INSTRUMENTATION_PROVIDER environment variable.
|
// Register monitoring instrumentation based on the
|
||||||
await registerInstrumentation();
|
// MONITORING_INSTRUMENTATION_PROVIDER environment variable.
|
||||||
|
return registerInstrumentation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const INTERNAL_PACKAGES = [
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
swcMinify: true,
|
||||||
/** Enables hot reloading for local packages without a build step */
|
/** Enables hot reloading for local packages without a build step */
|
||||||
transpilePackages: INTERNAL_PACKAGES,
|
transpilePackages: INTERNAL_PACKAGES,
|
||||||
pageExtensions: ['ts', 'tsx'],
|
pageExtensions: ['ts', 'tsx'],
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"build": "contentlayer build",
|
|
||||||
"start": "docker compose up"
|
"start": "docker compose up"
|
||||||
},
|
},
|
||||||
"prettier": "@kit/prettier-config",
|
"prettier": "@kit/prettier-config",
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
import {
|
|
||||||
BaselimeSDK,
|
|
||||||
BetterHttpInstrumentation,
|
|
||||||
VercelPlugin,
|
|
||||||
} from '@baselime/node-opentelemetry';
|
|
||||||
|
|
||||||
const INSTRUMENTATION_SERVICE_NAME = process.env.INSTRUMENTATION_SERVICE_NAME;
|
const INSTRUMENTATION_SERVICE_NAME = process.env.INSTRUMENTATION_SERVICE_NAME;
|
||||||
|
|
||||||
if (!INSTRUMENTATION_SERVICE_NAME) {
|
if (!INSTRUMENTATION_SERVICE_NAME) {
|
||||||
@@ -18,7 +12,11 @@ if (!INSTRUMENTATION_SERVICE_NAME) {
|
|||||||
*
|
*
|
||||||
* Please set the MONITORING_INSTRUMENTATION_PROVIDER environment variable to 'baselime' to register Baselime instrumentation.
|
* Please set the MONITORING_INSTRUMENTATION_PROVIDER environment variable to 'baselime' to register Baselime instrumentation.
|
||||||
*/
|
*/
|
||||||
export function registerBaselimeInstrumentation() {
|
export async function registerBaselimeInstrumentation() {
|
||||||
|
const { BaselimeSDK, BetterHttpInstrumentation, VercelPlugin } = await import(
|
||||||
|
'@baselime/node-opentelemetry'
|
||||||
|
);
|
||||||
|
|
||||||
const sdk = new BaselimeSDK({
|
const sdk = new BaselimeSDK({
|
||||||
serverless: true,
|
serverless: true,
|
||||||
service: INSTRUMENTATION_SERVICE_NAME,
|
service: INSTRUMENTATION_SERVICE_NAME,
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
import { Resource } from '@opentelemetry/resources';
|
|
||||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
||||||
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
|
|
||||||
import {
|
|
||||||
SentryPropagator,
|
|
||||||
SentrySpanProcessor,
|
|
||||||
} from '@sentry/opentelemetry-node';
|
|
||||||
|
|
||||||
const INSTRUMENTATION_SERVICE_NAME = process.env.INSTRUMENTATION_SERVICE_NAME;
|
const INSTRUMENTATION_SERVICE_NAME = process.env.INSTRUMENTATION_SERVICE_NAME;
|
||||||
|
|
||||||
if (!INSTRUMENTATION_SERVICE_NAME) {
|
if (!INSTRUMENTATION_SERVICE_NAME) {
|
||||||
@@ -20,7 +12,18 @@ if (!INSTRUMENTATION_SERVICE_NAME) {
|
|||||||
*
|
*
|
||||||
* Please set the MONITORING_INSTRUMENTATION_PROVIDER environment variable to 'sentry' to register Sentry instrumentation.
|
* Please set the MONITORING_INSTRUMENTATION_PROVIDER environment variable to 'sentry' to register Sentry instrumentation.
|
||||||
*/
|
*/
|
||||||
export function registerSentryInstrumentation() {
|
export async function registerSentryInstrumentation() {
|
||||||
|
const { Resource } = await import('@opentelemetry/resources');
|
||||||
|
const { NodeSDK } = await import('@opentelemetry/sdk-node');
|
||||||
|
|
||||||
|
const { SEMRESATTRS_SERVICE_NAME } = await import(
|
||||||
|
'@opentelemetry/semantic-conventions'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { SentrySpanProcessor, SentryPropagator } = await import(
|
||||||
|
'@sentry/opentelemetry-node'
|
||||||
|
);
|
||||||
|
|
||||||
const sdk = new NodeSDK({
|
const sdk = new NodeSDK({
|
||||||
resource: new Resource({
|
resource: new Resource({
|
||||||
[SEMRESATTRS_SERVICE_NAME]: INSTRUMENTATION_SERVICE_NAME,
|
[SEMRESATTRS_SERVICE_NAME]: INSTRUMENTATION_SERVICE_NAME,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const DEFAULT_INSTRUMENTATION_PROVIDER = process.env
|
|||||||
* Please set the MONITORING_INSTRUMENTATION_PROVIDER environment variable to register the monitoring instrumentation provider.
|
* Please set the MONITORING_INSTRUMENTATION_PROVIDER environment variable to register the monitoring instrumentation provider.
|
||||||
*/
|
*/
|
||||||
export async function registerInstrumentation() {
|
export async function registerInstrumentation() {
|
||||||
// Only run instrumentation in Node.js environment
|
|
||||||
if (
|
if (
|
||||||
process.env.NEXT_RUNTIME !== 'nodejs' ||
|
process.env.NEXT_RUNTIME !== 'nodejs' ||
|
||||||
!DEFAULT_INSTRUMENTATION_PROVIDER
|
!DEFAULT_INSTRUMENTATION_PROVIDER
|
||||||
@@ -39,6 +38,8 @@ export async function registerInstrumentation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown instrumentation provider`);
|
throw new Error(
|
||||||
|
`Unknown instrumentation provider: ${DEFAULT_INSTRUMENTATION_PROVIDER as string}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Logger as LoggerInstance } from './logger';
|
import { Logger as LoggerInstance } from './logger';
|
||||||
|
|
||||||
|
const LOGGER = process.env.LOGGER ?? 'pino';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Logger
|
* Logger
|
||||||
* By default, the logger is set to use Pino. To change the logger, update the import statement below.
|
* By default, the logger is set to use Pino. To change the logger, update the import statement below.
|
||||||
* to your desired logger implementation.
|
* to your desired logger implementation.
|
||||||
*/
|
*/
|
||||||
async function getLogger(): Promise<LoggerInstance> {
|
async function getLogger(): Promise<LoggerInstance> {
|
||||||
switch (process.env.LOGGER ?? 'pino') {
|
switch (LOGGER) {
|
||||||
case 'pino': {
|
case 'pino': {
|
||||||
const { Logger: PinoLogger } = await import('./impl/pino');
|
const { Logger: PinoLogger } = await import('./impl/pino');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user