feat: add basic webui
This commit is contained in:
15
apps/app/.env.development
Normal file
15
apps/app/.env.development
Normal file
@@ -0,0 +1,15 @@
|
||||
# Server
|
||||
BETTER_AUTH_SECRET="konobangu"
|
||||
DATABASE_URL="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu"
|
||||
BETTERSTACK_API_KEY=""
|
||||
BETTERSTACK_URL=""
|
||||
FLAGS_SECRET=""
|
||||
ARCJET_KEY=""
|
||||
SVIX_TOKEN=""
|
||||
LIVEBLOCKS_SECRET=""
|
||||
|
||||
# Client
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="https://webui.konobangu.com"
|
||||
15
apps/app/.env.example
Normal file
15
apps/app/.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
# Server
|
||||
BETTER_AUTH_SECRET=""
|
||||
DATABASE_URL=""
|
||||
BETTERSTACK_API_KEY=""
|
||||
BETTERSTACK_URL=""
|
||||
FLAGS_SECRET=""
|
||||
ARCJET_KEY=""
|
||||
SVIX_TOKEN=""
|
||||
LIVEBLOCKS_SECRET=""
|
||||
|
||||
# Client
|
||||
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_WEB_URL="http://localhost:3001"
|
||||
NEXT_PUBLIC_DOCS_URL="http://localhost:3004"
|
||||
NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3000"
|
||||
45
apps/app/.gitignore
vendored
Normal file
45
apps/app/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# 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*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# prisma
|
||||
.env
|
||||
|
||||
# react.email
|
||||
.react-email
|
||||
|
||||
# Sentry
|
||||
.sentryclirc
|
||||
13
apps/app/__tests__/sign-in.test.tsx
Normal file
13
apps/app/__tests__/sign-in.test.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { expect, test } from 'vitest';
|
||||
import Page from '../app/(unauthenticated)/sign-in/[[...sign-in]]/page';
|
||||
|
||||
test('Sign In Page', () => {
|
||||
render(<Page />);
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
level: 1,
|
||||
name: 'Welcome back',
|
||||
})
|
||||
).toBeDefined();
|
||||
});
|
||||
13
apps/app/__tests__/sign-up.test.tsx
Normal file
13
apps/app/__tests__/sign-up.test.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { expect, test } from 'vitest';
|
||||
import Page from '../app/(unauthenticated)/sign-up/[[...sign-up]]/page';
|
||||
|
||||
test('Sign Up Page', () => {
|
||||
render(<Page />);
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
level: 1,
|
||||
name: 'Create an account',
|
||||
})
|
||||
).toBeDefined();
|
||||
});
|
||||
59
apps/app/app/(authenticated)/components/avatar-stack.tsx
Normal file
59
apps/app/app/(authenticated)/components/avatar-stack.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { useOthers, useSelf } from '@konobangu/collaboration/hooks';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from '@konobangu/design-system/components/ui/avatar';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@konobangu/design-system/components/ui/tooltip';
|
||||
import { tailwind } from '@konobangu/tailwind-config';
|
||||
|
||||
type PresenceAvatarProps = {
|
||||
info?: Liveblocks['UserMeta']['info'];
|
||||
};
|
||||
|
||||
const PresenceAvatar = ({ info }: PresenceAvatarProps) => (
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger>
|
||||
<Avatar className="h-7 w-7 bg-secondary ring-1 ring-background">
|
||||
<AvatarImage src={info?.avatar} alt={info?.name} />
|
||||
<AvatarFallback className="text-xs">
|
||||
{info?.name?.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent collisionPadding={4}>
|
||||
<p>{info?.name ?? 'Unknown'}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const AvatarStack = () => {
|
||||
const others = useOthers();
|
||||
const self = useSelf();
|
||||
const hasMoreUsers = others.length > 3;
|
||||
|
||||
return (
|
||||
<div className="-space-x-1 flex items-center px-4">
|
||||
{others.slice(0, 3).map(({ connectionId, info }) => (
|
||||
<PresenceAvatar key={connectionId} info={info} />
|
||||
))}
|
||||
|
||||
{hasMoreUsers && (
|
||||
<PresenceAvatar
|
||||
info={{
|
||||
name: `+${others.length - 3}`,
|
||||
color: tailwind.theme.colors.gray[500],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{self && <PresenceAvatar info={self.info} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { getUsers } from '@/app/actions/users/get';
|
||||
import { searchUsers } from '@/app/actions/users/search';
|
||||
import { Room } from '@konobangu/collaboration/room';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const CollaborationProvider = ({
|
||||
orgId,
|
||||
children,
|
||||
}: {
|
||||
orgId: string;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const resolveUsers = async ({ userIds }: { userIds: string[] }) => {
|
||||
const response = await getUsers(userIds);
|
||||
|
||||
if ('error' in response) {
|
||||
throw new Error('Problem resolving users');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const resolveMentionSuggestions = async ({ text }: { text: string }) => {
|
||||
const response = await searchUsers(text);
|
||||
|
||||
if ('error' in response) {
|
||||
throw new Error('Problem resolving mention suggestions');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
return (
|
||||
<Room
|
||||
id={`${orgId}:presence`}
|
||||
authEndpoint="/api/collaboration/auth"
|
||||
fallback={
|
||||
<div className="px-3 text-muted-foreground text-xs">Loading...</div>
|
||||
}
|
||||
resolveUsers={resolveUsers}
|
||||
resolveMentionSuggestions={resolveMentionSuggestions}
|
||||
>
|
||||
{children}
|
||||
</Room>
|
||||
);
|
||||
};
|
||||
106
apps/app/app/(authenticated)/components/cursors.tsx
Normal file
106
apps/app/app/(authenticated)/components/cursors.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import { useMyPresence, useOthers } from '@konobangu/collaboration/hooks';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const Cursor = ({
|
||||
name,
|
||||
color,
|
||||
x,
|
||||
y,
|
||||
}: {
|
||||
name: string | undefined;
|
||||
color: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}) => (
|
||||
<div
|
||||
className="pointer-events-none absolute top-0 left-0 z-[999] select-none transition-transform duration-100"
|
||||
style={{
|
||||
transform: `translateX(${x}px) translateY(${y}px)`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="absolute top-0 left-0"
|
||||
width="24"
|
||||
height="36"
|
||||
viewBox="0 0 24 36"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Cursor</title>
|
||||
<path
|
||||
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
className="absolute top-4 left-1.5 whitespace-nowrap rounded-full px-2 py-0.5 text-white text-xs"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Cursors = () => {
|
||||
/**
|
||||
* useMyPresence returns the presence of the current user and a function to update it.
|
||||
* updateMyPresence is different than the setState function returned by the useState hook from React.
|
||||
* You don't need to pass the full presence object to update it.
|
||||
* See https://liveblocks.io/docs/api-reference/liveblocks-react#useMyPresence for more information
|
||||
*/
|
||||
const [_cursor, updateMyPresence] = useMyPresence();
|
||||
|
||||
/**
|
||||
* Return all the other users in the room and their presence (a cursor position in this case)
|
||||
*/
|
||||
const others = useOthers();
|
||||
|
||||
useEffect(() => {
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
// Update the user cursor position on every pointer move
|
||||
updateMyPresence({
|
||||
cursor: {
|
||||
x: Math.round(event.clientX),
|
||||
y: Math.round(event.clientY),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onPointerLeave = () => {
|
||||
// When the pointer goes out, set cursor to null
|
||||
updateMyPresence({
|
||||
cursor: null,
|
||||
});
|
||||
};
|
||||
|
||||
document.body.addEventListener('pointermove', onPointerMove);
|
||||
document.body.addEventListener('pointerleave', onPointerLeave);
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener('pointermove', onPointerMove);
|
||||
document.body.removeEventListener('pointerleave', onPointerLeave);
|
||||
};
|
||||
}, [updateMyPresence]);
|
||||
|
||||
return others.map(({ connectionId, presence, info }) => {
|
||||
if (!presence.cursor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Cursor
|
||||
key={`cursor-${connectionId}`}
|
||||
// connectionId is an integer that is incremented at every new connections
|
||||
// Assigning a color with a modulo makes sure that a specific user has the same colors on every clients
|
||||
color={info.color}
|
||||
x={presence.cursor.x}
|
||||
y={presence.cursor.y}
|
||||
name={info?.name}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
43
apps/app/app/(authenticated)/components/header.tsx
Normal file
43
apps/app/app/(authenticated)/components/header.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from '@konobangu/design-system/components/ui/breadcrumb';
|
||||
import { Separator } from '@konobangu/design-system/components/ui/separator';
|
||||
import { SidebarTrigger } from '@konobangu/design-system/components/ui/sidebar';
|
||||
import { Fragment, type ReactNode } from 'react';
|
||||
|
||||
type HeaderProps = {
|
||||
pages: string[];
|
||||
page: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const Header = ({ pages, page, children }: HeaderProps) => (
|
||||
<header className="flex h-16 shrink-0 items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{pages.map((page, index) => (
|
||||
<Fragment key={page}>
|
||||
{index > 0 && <BreadcrumbSeparator className="hidden md:block" />}
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink href="#">{page}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</Fragment>
|
||||
))}
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{page}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
{children}
|
||||
</header>
|
||||
);
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { analytics } from '@konobangu/analytics/client';
|
||||
import { useSession } from '@konobangu/auth/client';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const PostHogIdentifier = () => {
|
||||
const session = useSession();
|
||||
const user = session?.data?.user;
|
||||
const identified = useRef(false);
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
// Track pageviews
|
||||
if (pathname && analytics) {
|
||||
let url = window.origin + pathname;
|
||||
if (searchParams.toString()) {
|
||||
url = `${url}?${searchParams.toString()}`;
|
||||
}
|
||||
analytics.capture('$pageview', {
|
||||
$current_url: url,
|
||||
});
|
||||
}
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || identified.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
analytics.identify(user.id, {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
createdAt: user.createdAt,
|
||||
avatar: user.image,
|
||||
});
|
||||
|
||||
identified.current = true;
|
||||
}, [user]);
|
||||
|
||||
return null;
|
||||
};
|
||||
342
apps/app/app/(authenticated)/components/sidebar.tsx
Normal file
342
apps/app/app/(authenticated)/components/sidebar.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
'use client';
|
||||
|
||||
// import { OrganizationSwitcher, UserButton } from '@konobangu/auth/client';
|
||||
import { ModeToggle } from '@konobangu/design-system/components/mode-toggle';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@konobangu/design-system/components/ui/collapsible';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@konobangu/design-system/components/ui/dropdown-menu';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
useSidebar,
|
||||
} from '@konobangu/design-system/components/ui/sidebar';
|
||||
import { cn } from '@konobangu/design-system/lib/utils';
|
||||
import {
|
||||
AnchorIcon,
|
||||
BookOpenIcon,
|
||||
BotIcon,
|
||||
ChevronRightIcon,
|
||||
FolderIcon,
|
||||
FrameIcon,
|
||||
LifeBuoyIcon,
|
||||
MapIcon,
|
||||
MoreHorizontalIcon,
|
||||
PieChartIcon,
|
||||
SendIcon,
|
||||
Settings2Icon,
|
||||
ShareIcon,
|
||||
SquareTerminalIcon,
|
||||
Trash2Icon,
|
||||
} from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type GlobalSidebarProperties = {
|
||||
readonly children: ReactNode;
|
||||
};
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: 'shadcn',
|
||||
email: 'm@example.com',
|
||||
avatar: '/avatars/shadcn.jpg',
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: 'Playground',
|
||||
url: '#',
|
||||
icon: SquareTerminalIcon,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: 'History',
|
||||
url: '#',
|
||||
},
|
||||
{
|
||||
title: 'Starred',
|
||||
url: '#',
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
url: '#',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Models',
|
||||
url: '#',
|
||||
icon: BotIcon,
|
||||
items: [
|
||||
{
|
||||
title: 'Genesis',
|
||||
url: '#',
|
||||
},
|
||||
{
|
||||
title: 'Explorer',
|
||||
url: '#',
|
||||
},
|
||||
{
|
||||
title: 'Quantum',
|
||||
url: '#',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Documentation',
|
||||
url: '#',
|
||||
icon: BookOpenIcon,
|
||||
items: [
|
||||
{
|
||||
title: 'Introduction',
|
||||
url: '#',
|
||||
},
|
||||
{
|
||||
title: 'Get Started',
|
||||
url: '#',
|
||||
},
|
||||
{
|
||||
title: 'Tutorials',
|
||||
url: '#',
|
||||
},
|
||||
{
|
||||
title: 'Changelog',
|
||||
url: '#',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
url: '#',
|
||||
icon: Settings2Icon,
|
||||
items: [
|
||||
{
|
||||
title: 'General',
|
||||
url: '#',
|
||||
},
|
||||
{
|
||||
title: 'Team',
|
||||
url: '#',
|
||||
},
|
||||
{
|
||||
title: 'Billing',
|
||||
url: '#',
|
||||
},
|
||||
{
|
||||
title: 'Limits',
|
||||
url: '#',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: 'Webhooks',
|
||||
url: '/webhooks',
|
||||
icon: AnchorIcon,
|
||||
},
|
||||
{
|
||||
title: 'Support',
|
||||
url: '#',
|
||||
icon: LifeBuoyIcon,
|
||||
},
|
||||
{
|
||||
title: 'Feedback',
|
||||
url: '#',
|
||||
icon: SendIcon,
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
name: 'Design Engineering',
|
||||
url: '#',
|
||||
icon: FrameIcon,
|
||||
},
|
||||
{
|
||||
name: 'Sales & Marketing',
|
||||
url: '#',
|
||||
icon: PieChartIcon,
|
||||
},
|
||||
{
|
||||
name: 'Travel',
|
||||
url: '#',
|
||||
icon: MapIcon,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const GlobalSidebar = ({ children }: GlobalSidebarProperties) => {
|
||||
const sidebar = useSidebar();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar variant="inset">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<div
|
||||
className={cn(
|
||||
'h-[36px] overflow-hidden transition-all [&>div]:w-full',
|
||||
sidebar.open ? '' : '-mx-1'
|
||||
)}
|
||||
>
|
||||
{/* <OrganizationSwitcher
|
||||
hidePersonal
|
||||
afterSelectOrganizationUrl="/"
|
||||
/> */}
|
||||
</div>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{data.navMain.map((item) => (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
asChild
|
||||
defaultOpen={item.isActive}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip={item.title}>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
{item.items?.length ? (
|
||||
<>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||
<ChevronRightIcon />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</SidebarMenuAction>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<a href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</>
|
||||
) : null}
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{data.projects.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.name}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction showOnHover>
|
||||
<MoreHorizontalIcon />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-48"
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<FolderIcon className="text-muted-foreground" />
|
||||
<span>View Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<ShareIcon className="text-muted-foreground" />
|
||||
<span>Share Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Trash2Icon className="text-muted-foreground" />
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<MoreHorizontalIcon />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup className="mt-auto">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{data.navSecondary.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem className="flex items-center gap-2">
|
||||
{/* <UserButton
|
||||
showName
|
||||
appearance={{
|
||||
elements: {
|
||||
rootBox: 'flex overflow-hidden w-full',
|
||||
userButtonBox: 'flex-row-reverse',
|
||||
userButtonOuterIdentifier: 'truncate pl-0',
|
||||
},
|
||||
}}
|
||||
/> */}
|
||||
<ModeToggle />
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
<SidebarInset>{children}</SidebarInset>
|
||||
</>
|
||||
);
|
||||
};
|
||||
42
apps/app/app/(authenticated)/layout.tsx
Normal file
42
apps/app/app/(authenticated)/layout.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getSessionFromHeaders } from '@konobangu/auth/server';
|
||||
import { SidebarProvider } from '@konobangu/design-system/components/ui/sidebar';
|
||||
import { env } from '@konobangu/env';
|
||||
import { showBetaFeature } from '@konobangu/feature-flags';
|
||||
import { secure } from '@konobangu/security';
|
||||
import { redirect } from 'next/navigation';
|
||||
import type { ReactNode } from 'react';
|
||||
import { PostHogIdentifier } from './components/posthog-identifier';
|
||||
import { GlobalSidebar } from './components/sidebar';
|
||||
|
||||
type AppLayoutProperties = {
|
||||
readonly children: ReactNode;
|
||||
};
|
||||
|
||||
const AppLayout = async ({ children }: AppLayoutProperties) => {
|
||||
if (env.ARCJET_KEY) {
|
||||
await secure(['CATEGORY:PREVIEW']);
|
||||
}
|
||||
|
||||
const { user } = await getSessionFromHeaders();
|
||||
|
||||
if (!user) {
|
||||
return redirect('/sign-in'); // from next/navigation
|
||||
}
|
||||
const betaFeature = await showBetaFeature();
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<GlobalSidebar>
|
||||
{betaFeature && (
|
||||
<div className="m-4 rounded-full bg-success p-1.5 text-center text-sm text-success-foreground">
|
||||
Beta feature now available
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</GlobalSidebar>
|
||||
<PostHogIdentifier />
|
||||
</SidebarProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
57
apps/app/app/(authenticated)/page.tsx
Normal file
57
apps/app/app/(authenticated)/page.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { getSessionFromHeaders } from '@konobangu/auth/server';
|
||||
import { database } from '@konobangu/database';
|
||||
import { env } from '@konobangu/env';
|
||||
import type { Metadata } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { AvatarStack } from './components/avatar-stack';
|
||||
import { Cursors } from './components/cursors';
|
||||
import { Header } from './components/header';
|
||||
|
||||
const title = 'Acme Inc';
|
||||
const description = 'My application.';
|
||||
|
||||
const CollaborationProvider = dynamic(() =>
|
||||
import('./components/collaboration-provider').then(
|
||||
(mod) => mod.CollaborationProvider
|
||||
)
|
||||
);
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
};
|
||||
|
||||
const App = async () => {
|
||||
const pages = await database.selectFrom('page').selectAll().execute();
|
||||
const { orgId } = await getSessionFromHeaders();
|
||||
|
||||
if (!orgId) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header pages={['Building Your Application']} page="Data Fetching">
|
||||
{env.LIVEBLOCKS_SECRET && (
|
||||
<CollaborationProvider orgId={orgId}>
|
||||
<AvatarStack />
|
||||
<Cursors />
|
||||
</CollaborationProvider>
|
||||
)}
|
||||
</Header>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
{pages.map((page) => (
|
||||
<div key={page.id} className="aspect-video rounded-xl bg-muted/50">
|
||||
{page.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
29
apps/app/app/(authenticated)/webhooks/page.tsx
Normal file
29
apps/app/app/(authenticated)/webhooks/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { webhooks } from '@konobangu/webhooks';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Webhooks',
|
||||
description: 'Send webhooks to your users.',
|
||||
};
|
||||
|
||||
const WebhooksPage = async () => {
|
||||
const response = await webhooks.getAppPortal();
|
||||
|
||||
if (!response?.url) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<iframe
|
||||
title="Webhooks"
|
||||
src={response.url}
|
||||
className="h-full w-full border-none"
|
||||
allow="clipboard-write"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebhooksPage;
|
||||
58
apps/app/app/(unauthenticated)/layout.tsx
Normal file
58
apps/app/app/(unauthenticated)/layout.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ModeToggle } from '@konobangu/design-system/components/mode-toggle';
|
||||
import { env } from '@konobangu/env';
|
||||
import { CommandIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type AuthLayoutProps = {
|
||||
readonly children: ReactNode;
|
||||
};
|
||||
|
||||
const AuthLayout = ({ children }: AuthLayoutProps) => (
|
||||
<div className="container relative grid h-dvh flex-col items-center justify-center lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div className="relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r">
|
||||
<div className="absolute inset-0 bg-zinc-900" />
|
||||
<div className="relative z-20 flex items-center font-medium text-lg">
|
||||
<CommandIcon className="mr-2 h-6 w-6" />
|
||||
Acme Inc
|
||||
</div>
|
||||
<div className="absolute top-4 right-4">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
<div className="relative z-20 mt-auto">
|
||||
<blockquote className="space-y-2">
|
||||
<p className="text-lg">
|
||||
“This library has saved me countless hours of work and helped
|
||||
me deliver stunning designs to my clients faster than ever
|
||||
before.”
|
||||
</p>
|
||||
<footer className="text-sm">Sofia Davis</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:p-8">
|
||||
<div className="mx-auto flex w-full max-w-[400px] flex-col justify-center space-y-6">
|
||||
{children}
|
||||
<p className="px-8 text-center text-muted-foreground text-sm">
|
||||
By clicking continue, you agree to our{' '}
|
||||
<Link
|
||||
href={new URL('/legal/terms', env.NEXT_PUBLIC_WEB_URL).toString()}
|
||||
className="underline underline-offset-4 hover:text-primary"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link
|
||||
href={new URL('/legal/privacy', env.NEXT_PUBLIC_WEB_URL).toString()}
|
||||
className="underline underline-offset-4 hover:text-primary"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AuthLayout;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createMetadata } from '@konobangu/seo/metadata';
|
||||
import type { Metadata } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const title = 'Welcome back';
|
||||
const description = 'Enter your details to sign in.';
|
||||
const SignIn = dynamic(() =>
|
||||
import('@konobangu/auth/components/sign-in').then((mod) => mod.SignIn)
|
||||
);
|
||||
|
||||
export const metadata: Metadata = createMetadata({ title, description });
|
||||
|
||||
const SignInPage = () => (
|
||||
<>
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<h1 className="font-semibold text-2xl tracking-tight">{title}</h1>
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
</div>
|
||||
<SignIn />
|
||||
</>
|
||||
);
|
||||
|
||||
export default SignInPage;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createMetadata } from '@konobangu/seo/metadata';
|
||||
import type { Metadata } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const title = 'Create an account';
|
||||
const description = 'Enter your details to get started.';
|
||||
const SignUp = dynamic(() =>
|
||||
import('@konobangu/auth/components/sign-up').then((mod) => mod.SignUp)
|
||||
);
|
||||
|
||||
export const metadata: Metadata = createMetadata({ title, description });
|
||||
|
||||
const SignUpPage = () => (
|
||||
<>
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<h1 className="font-semibold text-2xl tracking-tight">{title}</h1>
|
||||
<p className="text-muted-foreground text-sm">{description}</p>
|
||||
</div>
|
||||
<SignUp />
|
||||
</>
|
||||
);
|
||||
|
||||
export default SignUpPage;
|
||||
3
apps/app/app/.well-known/vercel/flags/route.ts
Normal file
3
apps/app/app/.well-known/vercel/flags/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getFlags } from '@konobangu/feature-flags/access';
|
||||
|
||||
export const GET = getFlags;
|
||||
63
apps/app/app/actions/users/get.ts
Normal file
63
apps/app/app/actions/users/get.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
getFullOrganizationFromSession,
|
||||
getSessionFromHeaders,
|
||||
} from '@konobangu/auth/server';
|
||||
import { tailwind } from '@konobangu/tailwind-config';
|
||||
|
||||
const colors = [
|
||||
tailwind.theme.colors.red[500],
|
||||
tailwind.theme.colors.orange[500],
|
||||
tailwind.theme.colors.amber[500],
|
||||
tailwind.theme.colors.yellow[500],
|
||||
tailwind.theme.colors.lime[500],
|
||||
tailwind.theme.colors.green[500],
|
||||
tailwind.theme.colors.emerald[500],
|
||||
tailwind.theme.colors.teal[500],
|
||||
tailwind.theme.colors.cyan[500],
|
||||
tailwind.theme.colors.sky[500],
|
||||
tailwind.theme.colors.blue[500],
|
||||
tailwind.theme.colors.indigo[500],
|
||||
tailwind.theme.colors.violet[500],
|
||||
tailwind.theme.colors.purple[500],
|
||||
tailwind.theme.colors.fuchsia[500],
|
||||
tailwind.theme.colors.pink[500],
|
||||
tailwind.theme.colors.rose[500],
|
||||
];
|
||||
|
||||
export const getUsers = async (
|
||||
userIds: string[]
|
||||
): Promise<
|
||||
| {
|
||||
data: Liveblocks['UserMeta']['info'][];
|
||||
}
|
||||
| {
|
||||
error: unknown;
|
||||
}
|
||||
> => {
|
||||
try {
|
||||
const session = await getSessionFromHeaders();
|
||||
const { orgId } = session;
|
||||
|
||||
if (!orgId) {
|
||||
throw new Error('Not logged in');
|
||||
}
|
||||
|
||||
const { fullOrganization } = await getFullOrganizationFromSession(session);
|
||||
|
||||
const members = fullOrganization?.members || [];
|
||||
|
||||
const data: Liveblocks['UserMeta']['info'][] = members
|
||||
.filter((user) => user?.userId && userIds.includes(user?.userId))
|
||||
.map((user) => ({
|
||||
name: user.user.name ?? user.user.email ?? 'Unknown user',
|
||||
picture: user.user.image,
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
}));
|
||||
|
||||
return { data };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
50
apps/app/app/actions/users/search.ts
Normal file
50
apps/app/app/actions/users/search.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
getFullOrganizationFromSession,
|
||||
getSessionFromHeaders,
|
||||
} from '@konobangu/auth/server';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
export const searchUsers = async (
|
||||
query: string
|
||||
): Promise<
|
||||
| {
|
||||
data: string[];
|
||||
}
|
||||
| {
|
||||
error: unknown;
|
||||
}
|
||||
> => {
|
||||
try {
|
||||
const session = await getSessionFromHeaders();
|
||||
const { orgId } = session;
|
||||
|
||||
if (!orgId) {
|
||||
throw new Error('Not logged in');
|
||||
}
|
||||
|
||||
const { fullOrganization } = await getFullOrganizationFromSession(session);
|
||||
|
||||
const members = fullOrganization?.members || [];
|
||||
|
||||
const users = members.map((user) => ({
|
||||
id: user.id,
|
||||
name: user.user.name ?? user.user.email ?? 'Unknown user',
|
||||
imageUrl: user.user.image,
|
||||
}));
|
||||
|
||||
const fuse = new Fuse(users, {
|
||||
keys: ['name'],
|
||||
minMatchCharLength: 1,
|
||||
threshold: 0.3,
|
||||
});
|
||||
|
||||
const results = fuse.search(query);
|
||||
const data = results.map((result) => result.item.id);
|
||||
|
||||
return { data };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
42
apps/app/app/api/collaboration/auth/route.ts
Normal file
42
apps/app/app/api/collaboration/auth/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { getSessionFromHeaders } from '@konobangu/auth/server';
|
||||
import { authenticate } from '@konobangu/collaboration/auth';
|
||||
import { tailwind } from '@konobangu/tailwind-config';
|
||||
|
||||
const COLORS = [
|
||||
tailwind.theme.colors.red[500],
|
||||
tailwind.theme.colors.orange[500],
|
||||
tailwind.theme.colors.amber[500],
|
||||
tailwind.theme.colors.yellow[500],
|
||||
tailwind.theme.colors.lime[500],
|
||||
tailwind.theme.colors.green[500],
|
||||
tailwind.theme.colors.emerald[500],
|
||||
tailwind.theme.colors.teal[500],
|
||||
tailwind.theme.colors.cyan[500],
|
||||
tailwind.theme.colors.sky[500],
|
||||
tailwind.theme.colors.blue[500],
|
||||
tailwind.theme.colors.indigo[500],
|
||||
tailwind.theme.colors.violet[500],
|
||||
tailwind.theme.colors.purple[500],
|
||||
tailwind.theme.colors.fuchsia[500],
|
||||
tailwind.theme.colors.pink[500],
|
||||
tailwind.theme.colors.rose[500],
|
||||
];
|
||||
|
||||
export const POST = async () => {
|
||||
const session = await getSessionFromHeaders();
|
||||
const { orgId, user } = session;
|
||||
|
||||
if (!user || !orgId) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
return authenticate({
|
||||
userId: user.id,
|
||||
orgId,
|
||||
userInfo: {
|
||||
name: user.name ?? user.email ?? undefined,
|
||||
avatar: user.image ?? undefined,
|
||||
color: COLORS[Math.floor(Math.random() * COLORS.length)],
|
||||
},
|
||||
});
|
||||
};
|
||||
BIN
apps/app/app/apple-icon.png
Normal file
BIN
apps/app/app/apple-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 216 B |
29
apps/app/app/global-error.tsx
Normal file
29
apps/app/app/global-error.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@konobangu/design-system/components/ui/button';
|
||||
import { fonts } from '@konobangu/design-system/lib/fonts';
|
||||
import { captureException } from '@sentry/nextjs';
|
||||
import type NextError from 'next/error';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type GlobalErrorProperties = {
|
||||
readonly error: NextError & { digest?: string };
|
||||
readonly reset: () => void;
|
||||
};
|
||||
|
||||
const GlobalError = ({ error, reset }: GlobalErrorProperties) => {
|
||||
useEffect(() => {
|
||||
captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="en" className={fonts}>
|
||||
<body>
|
||||
<h1>Oops, something went wrong</h1>
|
||||
<Button onClick={() => reset()}>Try again</Button>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalError;
|
||||
BIN
apps/app/app/icon.png
Normal file
BIN
apps/app/app/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 B |
18
apps/app/app/layout.tsx
Normal file
18
apps/app/app/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import '@konobangu/design-system/styles/globals.css';
|
||||
import { DesignSystemProvider } from '@konobangu/design-system';
|
||||
import { fonts } from '@konobangu/design-system/lib/fonts';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type RootLayoutProperties = {
|
||||
readonly children: ReactNode;
|
||||
};
|
||||
|
||||
const RootLayout = ({ children }: RootLayoutProperties) => (
|
||||
<html lang="en" className={fonts} suppressHydrationWarning>
|
||||
<body>
|
||||
<DesignSystemProvider>{children}</DesignSystemProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
export default RootLayout;
|
||||
BIN
apps/app/app/opengraph-image.png
Normal file
BIN
apps/app/app/opengraph-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
3
apps/app/instrumentation.ts
Normal file
3
apps/app/instrumentation.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { initializeSentry } from '@konobangu/next-config/instrumentation';
|
||||
|
||||
export const register = initializeSentry();
|
||||
1
apps/app/liveblocks.config.ts
Normal file
1
apps/app/liveblocks.config.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@konobangu/collaboration/config';
|
||||
22
apps/app/middleware.ts
Normal file
22
apps/app/middleware.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { authMiddleware } from '@konobangu/auth/middleware';
|
||||
import {
|
||||
noseconeConfig,
|
||||
noseconeMiddleware,
|
||||
} from '@konobangu/security/middleware';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
const securityHeaders = noseconeMiddleware(noseconeConfig);
|
||||
|
||||
export async function middleware (_request: NextRequest) {
|
||||
const response = await securityHeaders();
|
||||
return authMiddleware(response as any);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Skip Next.js internals and all static files, unless found in search params
|
||||
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
||||
// Always run for API routes
|
||||
'/(api|trpc)(.*)',
|
||||
],
|
||||
};
|
||||
15
apps/app/next.config.ts
Normal file
15
apps/app/next.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { env } from '@konobangu/env';
|
||||
import { config, withAnalyzer, withSentry } from '@konobangu/next-config';
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
let nextConfig: NextConfig = { ...config };
|
||||
|
||||
if (env.VERCEL) {
|
||||
nextConfig = withSentry(nextConfig);
|
||||
}
|
||||
|
||||
if (env.ANALYZE === 'true') {
|
||||
nextConfig = withAnalyzer(nextConfig);
|
||||
}
|
||||
|
||||
export default nextConfig;
|
||||
51
apps/app/package.json
Normal file
51
apps/app/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000 --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"analyze": "ANALYZE=true pnpm build",
|
||||
"test": "vitest run",
|
||||
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "6.0.1",
|
||||
"@konobangu/analytics": "workspace:*",
|
||||
"@konobangu/auth": "workspace:*",
|
||||
"@konobangu/collaboration": "workspace:*",
|
||||
"@konobangu/database": "workspace:*",
|
||||
"@konobangu/migrate": "workspace:*",
|
||||
"@konobangu/design-system": "workspace:*",
|
||||
"@konobangu/env": "workspace:*",
|
||||
"@konobangu/feature-flags": "workspace:*",
|
||||
"@konobangu/next-config": "workspace:*",
|
||||
"@konobangu/security": "workspace:*",
|
||||
"@konobangu/seo": "workspace:*",
|
||||
"@konobangu/tailwind-config": "workspace:*",
|
||||
"@konobangu/webhooks": "workspace:*",
|
||||
"@sentry/nextjs": "^8.43.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"import-in-the-middle": "^1.11.3",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "^15.1.3",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"require-in-the-middle": "^7.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@konobangu/testing": "workspace:*",
|
||||
"@konobangu/typescript-config": "workspace:*",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@types/node": "22.10.1",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"jsdom": "^25.0.1",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^2.1.8"
|
||||
}
|
||||
}
|
||||
1
apps/app/postcss.config.mjs
Normal file
1
apps/app/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@konobangu/design-system/postcss.config.mjs';
|
||||
34
apps/app/sentry.client.config.ts
Normal file
34
apps/app/sentry.client.config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file configures the initialization of Sentry on the client.
|
||||
* The config you add here will be used whenever a users loads a page in their browser.
|
||||
* https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
*/
|
||||
|
||||
import { init, replayIntegration } from '@sentry/nextjs';
|
||||
|
||||
init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
replaysOnErrorSampleRate: 1,
|
||||
|
||||
/*
|
||||
* This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
* in development and sample at a lower rate in production
|
||||
*/
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||
integrations: [
|
||||
replayIntegration({
|
||||
// Additional Replay configuration goes in here, for example:
|
||||
maskAllText: true,
|
||||
blockAllMedia: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
1
apps/app/tailwind.config.ts
Normal file
1
apps/app/tailwind.config.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { config as default } from '@konobangu/tailwind-config/config';
|
||||
17
apps/app/tsconfig.json
Normal file
17
apps/app/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@konobangu/typescript-config/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@konobangu/*": ["../../packages/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"next.config.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
]
|
||||
}
|
||||
1
apps/app/vitest.config.ts
Normal file
1
apps/app/vitest.config.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@konobangu/testing';
|
||||
Reference in New Issue
Block a user