Skip to content

Commit c755ec5

Browse files
HyteqHyteq
authored andcommitted
demo v1, migrating API
1 parent 29acf8d commit c755ec5

39 files changed

Lines changed: 6252 additions & 298 deletions

apps/api2/.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# dependencies (bun install)
2+
node_modules
3+
4+
# output
5+
out
6+
dist
7+
*.tgz
8+
9+
# code coverage
10+
coverage
11+
*.lcov
12+
13+
# logs
14+
logs
15+
_.log
16+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17+
18+
# dotenv environment variable files
19+
.env
20+
.env.development.local
21+
.env.test.local
22+
.env.production.local
23+
.env.local
24+
25+
# caches
26+
.eslintcache
27+
.cache
28+
*.tsbuildinfo
29+
30+
# IntelliJ based IDEs
31+
.idea
32+
33+
# Finder (MacOS) folder config
34+
.DS_Store

apps/api2/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "api2",
3+
"module": "index.ts",
4+
"type": "module",
5+
"private": true,
6+
"devDependencies": {
7+
"@types/bun": "latest"
8+
},
9+
"peerDependencies": {
10+
"typescript": "^5"
11+
},
12+
"dependencies": {
13+
"elysia": "^1.3.5"
14+
}
15+
}

apps/api2/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Elysia } from "elysia";
2+
3+
const app = new Elysia();
4+
5+
app.get('/', () => {
6+
return 'Hello World';
7+
});
8+
9+
app.listen(3000);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { cacheable } from "@/packages/redis";
2+
import { CreateDomainType } from "../types";
3+
import { db, eq, domains, SQL } from "@/packages/db";
4+
5+
const getCachedDomain = async (whereClause: SQL<unknown>) => {
6+
return cacheable(
7+
async () => {
8+
return await db.query.domains.findFirst({ where: whereClause });
9+
},
10+
{
11+
expireInSec: 60 * 60 * 24, // 24 hours
12+
staleWhileRevalidate: true,
13+
},
14+
)();
15+
};
16+
17+
export async function createDomain(domain: CreateDomainType) {
18+
const newDomain = await db.insert(domains).values(domain).returning();
19+
return newDomain;
20+
}
21+
22+
export async function getDomain(id: string, cache: boolean = true) {
23+
const where = eq(domains.id, id);
24+
if (cache) {
25+
return getCachedDomain(where);
26+
}
27+
return await db.query.domains.findFirst({ where });
28+
}
29+
30+
export async function getDomainByDomain(domain: string, cache: boolean = true) {
31+
const where = eq(domains.name, domain);
32+
if (cache) {
33+
return getCachedDomain(where);
34+
}
35+
return await db.query.domains.findFirst({ where });
36+
}
37+
38+
export async function deleteDomain(id: string) {
39+
const deletedDomain = await db.delete(domains).where(eq(domains.id, id)).returning();
40+
return deletedDomain;
41+
}
42+
43+
export async function updateDomain(id: string, domain: Partial<CreateDomainType>) {
44+
const updatedDomain = await db.update(domains).set(domain).where(eq(domains.id, id)).returning();
45+
return updatedDomain;
46+
}

apps/api2/src/services/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './websites.service';
2+
export * from './domains.service';
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { cacheable } from "@/packages/redis";
2+
import { CreateWebsiteType } from "../types";
3+
import { db, eq, websites, SQL } from "@/packages/db";
4+
5+
const getCachedWebsite = async (whereClause: SQL<unknown>) => {
6+
return cacheable(
7+
async () => {
8+
return await db.query.websites.findFirst({ where: whereClause });
9+
},
10+
{
11+
expireInSec: 60 * 60 * 24, // 24 hours
12+
staleWhileRevalidate: true,
13+
},
14+
)();
15+
};
16+
17+
export async function createWebsite(website: CreateWebsiteType) {
18+
const newWebsite = await db.insert(websites).values(website).returning();
19+
return newWebsite;
20+
}
21+
22+
export async function getWebsite(id: string, cache: boolean = true) {
23+
const where = eq(websites.id, id);
24+
if (cache) {
25+
return getCachedWebsite(where);
26+
}
27+
return await db.query.websites.findFirst({ where });
28+
}
29+
30+
export async function getWebsiteByDomain(domain: string, cache: boolean = true) {
31+
const where = eq(websites.domain, domain);
32+
if (cache) {
33+
return getCachedWebsite(where);
34+
}
35+
return await db.query.websites.findFirst({ where });
36+
}
37+
38+
export async function deleteWebsite(id: string) {
39+
const deletedWebsite = await db
40+
.delete(websites)
41+
.where(eq(websites.id, id))
42+
.returning();
43+
return deletedWebsite;
44+
}
45+
46+
export async function updateWebsite(
47+
id: string,
48+
website: Partial<CreateWebsiteType>,
49+
) {
50+
const updatedWebsite = await db
51+
.update(websites)
52+
.set(website)
53+
.where(eq(websites.id, id))
54+
.returning();
55+
return updatedWebsite;
56+
}

apps/api2/src/types/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { websites, user, domains, projects, organization } from "@databuddy/db";
2+
3+
export type WebsiteType = typeof websites.$inferSelect;
4+
export type CreateWebsiteType = typeof websites.$inferInsert;
5+
6+
export type UserType = typeof user.$inferSelect;
7+
export type CreateUserType = typeof user.$inferInsert;
8+
9+
export type DomainType = typeof domains.$inferSelect;
10+
export type CreateDomainType = typeof domains.$inferInsert;
11+
12+
export type ProjectType = typeof projects.$inferSelect;
13+
export type CreateProjectType = typeof projects.$inferInsert;
14+
15+
export type OrganizationType = typeof organization.$inferSelect;
16+
export type CreateOrganizationType = typeof organization.$inferInsert;

apps/api2/tsconfig.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"compilerOptions": {
3+
// Environment setup & latest features
4+
"lib": ["ESNext"],
5+
"target": "ESNext",
6+
"module": "Preserve",
7+
"moduleDetection": "force",
8+
"jsx": "react-jsx",
9+
"allowJs": true,
10+
11+
// Bundler mode
12+
"moduleResolution": "bundler",
13+
"allowImportingTsExtensions": true,
14+
"verbatimModuleSyntax": true,
15+
"noEmit": true,
16+
17+
// Best practices
18+
"strict": true,
19+
"skipLibCheck": true,
20+
"noFallthroughCasesInSwitch": true,
21+
"noUncheckedIndexedAccess": true,
22+
"noImplicitOverride": true,
23+
24+
// Some stricter flags (disabled by default)
25+
"noUnusedLocals": false,
26+
"noUnusedParameters": false,
27+
"noPropertyAccessFromIndexSignature": false
28+
}
29+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"use client";
2+
3+
import { useState, useEffect, useCallback } from "react";
4+
import { usePathname } from "next/navigation";
5+
import { cn } from "@/lib/utils";
6+
import { ScrollArea } from "@/components/ui/scroll-area";
7+
import { GlobeIcon, XIcon, HouseIcon, ClockIcon, UsersIcon, MapPinIcon, ListIcon, InfoIcon, GitBranchIcon, BugIcon, FunnelIcon } from "@phosphor-icons/react";
8+
import { Button } from "@/components/ui/button";
9+
import Link from "next/link";
10+
import { Logo } from "@/components/layout/logo";
11+
import { ThemeToggle } from "@/components/layout/theme-toggle";
12+
import { UserMenu } from "@/components/layout/user-menu";
13+
import { NotificationsPopover } from "@/components/notifications/notifications-popover";
14+
15+
const demoNavigation = [
16+
{
17+
title: "Analytics",
18+
items: [
19+
{ name: "Overview", icon: HouseIcon, href: "/demo/OXmNQsViBT-FOS_wZCTHc", highlight: true },
20+
{ name: "Sessions", icon: ClockIcon, href: "/demo/OXmNQsViBT-FOS_wZCTHc/sessions", highlight: true },
21+
{ name: "Funnels", icon: FunnelIcon, href: "/demo/OXmNQsViBT-FOS_wZCTHc/funnels", highlight: true },
22+
{ name: "Journeys", icon: GitBranchIcon, href: "/demo/OXmNQsViBT-FOS_wZCTHc/journeys", highlight: true },
23+
{ name: "Errors", icon: BugIcon, href: "/demo/OXmNQsViBT-FOS_wZCTHc/errors", highlight: true },
24+
{ name: "Profiles", icon: UsersIcon, href: "/demo/OXmNQsViBT-FOS_wZCTHc/profiles", highlight: true },
25+
{ name: "Map", icon: MapPinIcon, href: "/demo/OXmNQsViBT-FOS_wZCTHc/map", highlight: true },
26+
],
27+
}
28+
];
29+
30+
31+
export function Sidebar() {
32+
const pathname = usePathname();
33+
const [isMobileOpen, setIsMobileOpen] = useState(false);
34+
35+
const closeSidebar = useCallback(() => {
36+
setIsMobileOpen(false);
37+
}, []);
38+
39+
// Handle keyboard navigation
40+
useEffect(() => {
41+
const handleKeyDown = (e: KeyboardEvent) => {
42+
if (e.key === 'Escape' && isMobileOpen) {
43+
closeSidebar();
44+
}
45+
};
46+
47+
document.addEventListener('keydown', handleKeyDown);
48+
return () => document.removeEventListener('keydown', handleKeyDown);
49+
}, [isMobileOpen, closeSidebar]);
50+
51+
return (
52+
<>
53+
{/* Top Header */}
54+
<header className="fixed top-0 left-0 right-0 z-50 w-full h-16 border-b bg-background/95 backdrop-blur-md">
55+
<div className="flex items-center h-full px-4 md:px-6">
56+
{/* Left side: Logo + Mobile menu */}
57+
<div className="flex items-center gap-4">
58+
<Button
59+
variant="ghost"
60+
size="icon"
61+
className="md:hidden"
62+
onClick={() => setIsMobileOpen(true)}
63+
>
64+
<ListIcon size={32} weight="duotone" className="h-5 w-5" />
65+
<span className="sr-only">Toggle menu</span>
66+
</Button>
67+
68+
<div className="flex items-center gap-3">
69+
<div className="flex flex-row items-center gap-3">
70+
<Logo />
71+
</div>
72+
</div>
73+
</div>
74+
75+
{/* Right Side - User Controls */}
76+
<div className="flex items-center gap-2 ml-auto">
77+
<ThemeToggle />
78+
79+
{/* Help */}
80+
<Button
81+
variant="ghost"
82+
size="icon"
83+
className="hidden md:flex h-8 w-8"
84+
>
85+
<InfoIcon size={32} weight="duotone" className="h-6 w-6" />
86+
<span className="sr-only">Help</span>
87+
</Button>
88+
89+
{/* Notifications */}
90+
<NotificationsPopover />
91+
92+
{/* User Menu */}
93+
<UserMenu />
94+
</div>
95+
</div>
96+
</header>
97+
98+
{/* Mobile backdrop */}
99+
{isMobileOpen && (
100+
<div
101+
className="fixed inset-0 bg-black/20 z-30 md:hidden"
102+
onClick={closeSidebar}
103+
/>
104+
)}
105+
106+
{/* Sidebar */}
107+
<div
108+
className={cn(
109+
"fixed inset-y-0 left-0 z-40 w-64 bg-background",
110+
"border-r transition-transform duration-200 ease-out md:translate-x-0 pt-16",
111+
isMobileOpen ? "translate-x-0" : "-translate-x-full"
112+
)}
113+
>
114+
{/* Mobile close button */}
115+
<Button
116+
variant="ghost"
117+
size="sm"
118+
className="absolute top-3 right-3 z-50 md:hidden h-8 w-8 p-0"
119+
onClick={closeSidebar}
120+
>
121+
<XIcon size={32} weight="duotone" className="h-4 w-4" />
122+
<span className="sr-only">Close sidebar</span>
123+
</Button>
124+
125+
<ScrollArea className="h-[calc(100vh-4rem)]">
126+
<div className="p-3 space-y-4">
127+
{/* Demo Website Header */}
128+
<div className="flex items-center gap-3 p-3 bg-muted/50 rounded border">
129+
<div className="p-2 rounded bg-primary/10 border border-primary/20">
130+
<GlobeIcon size={32} weight="duotone" className="h-5 w-5 text-primary" />
131+
</div>
132+
<div className="min-w-0 flex-1">
133+
<h2 className="font-semibold text-sm truncate">Landing Page</h2>
134+
<Link href="https://www.databuddy.cc" target="_blank" className="text-xs text-muted-foreground truncate">www.databuddy.cc</Link>
135+
</div>
136+
</div>
137+
138+
{/* Demo Navigation */}
139+
{demoNavigation.map((section) => (
140+
<div key={section.title}>
141+
<h3 className="px-2 mb-2 text-xs font-semibold text-muted-foreground tracking-wider uppercase">
142+
{section.title}
143+
</h3>
144+
<div className="space-y-1 ml-1">
145+
{section.items.map((item) => {
146+
const isActive = pathname === item.href;
147+
const Icon = item.icon;
148+
149+
return (
150+
<Link
151+
key={item.name}
152+
href={item.href}
153+
className={cn(
154+
"flex items-center gap-3 px-3 py-2 text-sm rounded transition-all cursor-pointer",
155+
isActive
156+
? "bg-primary/15 text-primary font-medium"
157+
: "text-foreground hover:bg-accent/70"
158+
)}
159+
>
160+
<Icon size={32} weight="duotone" className={cn("h-4 w-4", isActive && "text-primary")} />
161+
<span className="truncate">{item.name}</span>
162+
</Link>
163+
);
164+
})}
165+
</div>
166+
</div>
167+
))}
168+
</div>
169+
</ScrollArea>
170+
</div>
171+
</>
172+
);
173+
}

0 commit comments

Comments
 (0)