create-ej-app 25.0.0 → 26.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +4 -0
- package/cli.ts +5 -0
- package/package.json +2 -2
- package/template/api/package.json +2 -2
- package/template/nextjs/gitignore +1 -0
- package/template/nextjs/package.json +18 -18
- package/template/saas-starter/.dockerignore +7 -0
- package/template/saas-starter/.env.example +25 -0
- package/template/saas-starter/.vscode/extensions.json +11 -0
- package/template/saas-starter/.vscode/settings.json +3 -0
- package/template/saas-starter/Dockerfile +15 -0
- package/template/saas-starter/README.md +3 -0
- package/template/saas-starter/components.json +25 -0
- package/template/saas-starter/drizzle.config.ts +10 -0
- package/template/saas-starter/eslint.config.mjs +23 -0
- package/template/saas-starter/gitignore +9 -0
- package/template/saas-starter/next-env.d.ts +6 -0
- package/template/saas-starter/next.config.ts +14 -0
- package/template/saas-starter/package.json +92 -0
- package/template/saas-starter/postcss.config.mjs +7 -0
- package/template/saas-starter/prettier.config.mjs +11 -0
- package/template/saas-starter/public/placeholder.svg +1 -0
- package/template/saas-starter/src/actions/uploads.ts +28 -0
- package/template/saas-starter/src/actions/uploads.validation.ts +14 -0
- package/template/saas-starter/src/app/(admin)/actions/users.ts +97 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +21 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +40 -0
- package/template/saas-starter/src/app/(admin)/admin/layout.tsx +57 -0
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +3 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +183 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +129 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +415 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +15 -0
- package/template/saas-starter/src/app/(app)/actions/billing.ts +152 -0
- package/template/saas-starter/src/app/(app)/actions/members.ts +163 -0
- package/template/saas-starter/src/app/(app)/actions/organization.ts +92 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +20 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +34 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +100 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +364 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +155 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +232 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +116 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +169 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +571 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +40 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +69 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +29 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +17 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +27 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +41 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +140 -0
- package/template/saas-starter/src/app/(app)/app/layout.tsx +52 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +129 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +42 -0
- package/template/saas-starter/src/app/(app)/app/page.tsx +81 -0
- package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +109 -0
- package/template/saas-starter/src/app/(auth)/login/page.tsx +87 -0
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +75 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +88 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +167 -0
- package/template/saas-starter/src/app/(marketing)/layout.tsx +14 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +3 -0
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +3 -0
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +3 -0
- package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
- package/template/saas-starter/src/app/favicon.ico +0 -0
- package/template/saas-starter/src/app/globals.css +240 -0
- package/template/saas-starter/src/app/layout.tsx +53 -0
- package/template/saas-starter/src/app/not-found.tsx +27 -0
- package/template/saas-starter/src/app/proxy.ts +28 -0
- package/template/saas-starter/src/app/robots.ts +32 -0
- package/template/saas-starter/src/app/sitemap.ts +20 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +64 -0
- package/template/saas-starter/src/components/data-table/enum.ts +5 -0
- package/template/saas-starter/src/components/data-table/index.tsx +960 -0
- package/template/saas-starter/src/components/data-table/types.ts +75 -0
- package/template/saas-starter/src/components/error.tsx +26 -0
- package/template/saas-starter/src/components/file-upload.tsx +215 -0
- package/template/saas-starter/src/components/impersonation-banner.tsx +44 -0
- package/template/saas-starter/src/components/loader.tsx +25 -0
- package/template/saas-starter/src/components/navigation-search.tsx +140 -0
- package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
- package/template/saas-starter/src/components/page-heading.tsx +32 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +12 -0
- package/template/saas-starter/src/components/screen-size.tsx +40 -0
- package/template/saas-starter/src/components/site-footer.tsx +23 -0
- package/template/saas-starter/src/components/theme-provider.tsx +67 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +22 -0
- package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
- package/template/saas-starter/src/components/ui/badge.tsx +49 -0
- package/template/saas-starter/src/components/ui/button.tsx +67 -0
- package/template/saas-starter/src/components/ui/card.tsx +103 -0
- package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
- package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
- package/template/saas-starter/src/components/ui/command.tsx +192 -0
- package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
- package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/saas-starter/src/components/ui/field.tsx +238 -0
- package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
- package/template/saas-starter/src/components/ui/input.tsx +19 -0
- package/template/saas-starter/src/components/ui/label.tsx +24 -0
- package/template/saas-starter/src/components/ui/popover.tsx +89 -0
- package/template/saas-starter/src/components/ui/select.tsx +195 -0
- package/template/saas-starter/src/components/ui/separator.tsx +28 -0
- package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
- package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
- package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
- package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
- package/template/saas-starter/src/components/ui/switch.tsx +33 -0
- package/template/saas-starter/src/components/ui/table.tsx +116 -0
- package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
- package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
- package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
- package/template/saas-starter/src/components/user-menu.tsx +90 -0
- package/template/saas-starter/src/db/common.ts +31 -0
- package/template/saas-starter/src/db/enums.ts +26 -0
- package/template/saas-starter/src/db/index.ts +51 -0
- package/template/saas-starter/src/db/relations.ts +75 -0
- package/template/saas-starter/src/db/schema.ts +191 -0
- package/template/saas-starter/src/emails/components/email-layout.tsx +63 -0
- package/template/saas-starter/src/emails/invitation.tsx +56 -0
- package/template/saas-starter/src/emails/magic-link.tsx +49 -0
- package/template/saas-starter/src/emails/test.tsx +32 -0
- package/template/saas-starter/src/emails/welcome.tsx +34 -0
- package/template/saas-starter/src/env.ts +66 -0
- package/template/saas-starter/src/hooks/use-mobile.ts +19 -0
- package/template/saas-starter/src/instrumentation.ts +26 -0
- package/template/saas-starter/src/lib/actions.ts +21 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +31 -0
- package/template/saas-starter/src/lib/app-navigation.ts +59 -0
- package/template/saas-starter/src/lib/auth-client.ts +29 -0
- package/template/saas-starter/src/lib/auth-permissions.ts +22 -0
- package/template/saas-starter/src/lib/auth.ts +211 -0
- package/template/saas-starter/src/lib/billing.ts +117 -0
- package/template/saas-starter/src/lib/cn.ts +4 -0
- package/template/saas-starter/src/lib/constants.ts +22 -0
- package/template/saas-starter/src/lib/delay.ts +2 -0
- package/template/saas-starter/src/lib/email-service.ts +89 -0
- package/template/saas-starter/src/lib/format-currency.ts +27 -0
- package/template/saas-starter/src/lib/format-date.ts +12 -0
- package/template/saas-starter/src/lib/nodemailer.ts +43 -0
- package/template/saas-starter/src/lib/organization-access.ts +93 -0
- package/template/saas-starter/src/lib/organization-plan-limits.ts +38 -0
- package/template/saas-starter/src/lib/otel.ts +27 -0
- package/template/saas-starter/src/lib/plans.ts +63 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +75 -0
- package/template/saas-starter/src/lib/safe-action.ts +67 -0
- package/template/saas-starter/src/lib/siteConfig.ts +4 -0
- package/template/saas-starter/src/lib/storage.ts +181 -0
- package/template/saas-starter/src/lib/stripe.ts +6 -0
- package/template/saas-starter/src/lib/toast-message.ts +15 -0
- package/template/saas-starter/src/lib/validations.ts +12 -0
- package/template/saas-starter/src/scripts/reset-db.ts +22 -0
- package/template/saas-starter/src/scripts/seed.ts +11 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +200 -0
- package/template/saas-starter/src/scripts/test-mail.ts +35 -0
- package/template/saas-starter/tsconfig.json +51 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ChevronRight } from 'lucide-react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { usePathname } from 'next/navigation'
|
|
6
|
+
import { ComponentType, ReactNode } from 'react'
|
|
7
|
+
import {
|
|
8
|
+
Collapsible,
|
|
9
|
+
CollapsibleContent,
|
|
10
|
+
CollapsibleTrigger
|
|
11
|
+
} from '~/components/ui/collapsible'
|
|
12
|
+
import {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuContent,
|
|
15
|
+
DropdownMenuItem,
|
|
16
|
+
DropdownMenuLabel,
|
|
17
|
+
DropdownMenuSeparator,
|
|
18
|
+
DropdownMenuTrigger
|
|
19
|
+
} from '~/components/ui/dropdown-menu'
|
|
20
|
+
import {
|
|
21
|
+
Sidebar,
|
|
22
|
+
SidebarContent,
|
|
23
|
+
SidebarGroup,
|
|
24
|
+
SidebarGroupContent,
|
|
25
|
+
SidebarGroupLabel,
|
|
26
|
+
SidebarHeader,
|
|
27
|
+
SidebarMenu,
|
|
28
|
+
SidebarMenuButton,
|
|
29
|
+
SidebarMenuItem,
|
|
30
|
+
SidebarMenuSub,
|
|
31
|
+
SidebarMenuSubButton,
|
|
32
|
+
SidebarMenuSubItem,
|
|
33
|
+
SidebarRail,
|
|
34
|
+
useSidebar
|
|
35
|
+
} from '~/components/ui/sidebar'
|
|
36
|
+
|
|
37
|
+
type NavigationSidebarProps = {
|
|
38
|
+
header: ReactNode
|
|
39
|
+
groups: SidebarNavGroup[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SidebarNavIcon = ComponentType<{ className?: string }>
|
|
43
|
+
|
|
44
|
+
export type SidebarNavSubItem = {
|
|
45
|
+
title: string
|
|
46
|
+
url: string
|
|
47
|
+
exact?: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type SidebarNavItem = {
|
|
51
|
+
title: string
|
|
52
|
+
url: string
|
|
53
|
+
icon?: SidebarNavIcon
|
|
54
|
+
exact?: boolean
|
|
55
|
+
items?: SidebarNavSubItem[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type SidebarNavGroup = {
|
|
59
|
+
label: string
|
|
60
|
+
items: SidebarNavItem[]
|
|
61
|
+
/** When true, the group is only shown to superadmins. */
|
|
62
|
+
superadminOnly?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function NavigationSidebar({ header, groups }: NavigationSidebarProps) {
|
|
66
|
+
const pathname = usePathname()
|
|
67
|
+
const { state, isMobile, setOpenMobile } = useSidebar()
|
|
68
|
+
const isCollapsed = state === 'collapsed' && !isMobile
|
|
69
|
+
|
|
70
|
+
const handleLinkClick = () => {
|
|
71
|
+
if (isMobile) {
|
|
72
|
+
setOpenMobile(false)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const allItems = groups.flatMap(g => g.items)
|
|
77
|
+
|
|
78
|
+
const isItemActive = (item: { url: string; exact?: boolean }) => {
|
|
79
|
+
if (item.exact) {
|
|
80
|
+
return pathname === item.url
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (pathname !== item.url && !pathname.startsWith(`${item.url}/`)) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Yield to a more specific sibling that also matches
|
|
88
|
+
const moreSpecificExists = allItems.some(
|
|
89
|
+
other =>
|
|
90
|
+
other.url !== item.url &&
|
|
91
|
+
other.url.length > item.url.length &&
|
|
92
|
+
other.url.startsWith(item.url) &&
|
|
93
|
+
(pathname === other.url || pathname.startsWith(`${other.url}/`))
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return !moreSpecificExists
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isSubItemActive = (
|
|
100
|
+
item: SidebarNavSubItem,
|
|
101
|
+
siblings: SidebarNavSubItem[]
|
|
102
|
+
) => {
|
|
103
|
+
if (!isItemActive(item)) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return !siblings.some(
|
|
108
|
+
sibling =>
|
|
109
|
+
sibling.url !== item.url &&
|
|
110
|
+
isItemActive(sibling) &&
|
|
111
|
+
sibling.url.length > item.url.length
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isGroupActive = (item: SidebarNavItem) => {
|
|
116
|
+
if (isItemActive(item)) {
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return item.items?.some(subItem => isItemActive(subItem)) ?? false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Sidebar collapsible="icon" variant="sidebar">
|
|
125
|
+
<SidebarHeader>{header}</SidebarHeader>
|
|
126
|
+
<SidebarContent>
|
|
127
|
+
{groups.map(group => (
|
|
128
|
+
<SidebarGroup key={group.label}>
|
|
129
|
+
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
|
130
|
+
<SidebarGroupContent>
|
|
131
|
+
<SidebarMenu>
|
|
132
|
+
{group.items.map(item =>
|
|
133
|
+
item.items ? (
|
|
134
|
+
isCollapsed ? (
|
|
135
|
+
<SidebarMenuItem key={item.title}>
|
|
136
|
+
<DropdownMenu>
|
|
137
|
+
<DropdownMenuTrigger asChild>
|
|
138
|
+
<SidebarMenuButton
|
|
139
|
+
tooltip={item.title}
|
|
140
|
+
isActive={isGroupActive(item)}
|
|
141
|
+
>
|
|
142
|
+
{item.icon ? <item.icon /> : null}
|
|
143
|
+
<span>{item.title}</span>
|
|
144
|
+
<ChevronRight className="ml-auto transition-transform duration-200" />
|
|
145
|
+
</SidebarMenuButton>
|
|
146
|
+
</DropdownMenuTrigger>
|
|
147
|
+
<DropdownMenuContent
|
|
148
|
+
side="right"
|
|
149
|
+
align="start"
|
|
150
|
+
sideOffset={4}
|
|
151
|
+
>
|
|
152
|
+
<DropdownMenuLabel>{item.title}</DropdownMenuLabel>
|
|
153
|
+
<DropdownMenuSeparator />
|
|
154
|
+
{item.items.map(subItem => (
|
|
155
|
+
<DropdownMenuItem
|
|
156
|
+
key={subItem.url}
|
|
157
|
+
asChild
|
|
158
|
+
className={
|
|
159
|
+
isSubItemActive(subItem, item.items!)
|
|
160
|
+
? 'bg-accent font-medium'
|
|
161
|
+
: ''
|
|
162
|
+
}
|
|
163
|
+
>
|
|
164
|
+
<Link
|
|
165
|
+
href={subItem.url}
|
|
166
|
+
onClick={handleLinkClick}
|
|
167
|
+
>
|
|
168
|
+
{subItem.title}
|
|
169
|
+
</Link>
|
|
170
|
+
</DropdownMenuItem>
|
|
171
|
+
))}
|
|
172
|
+
</DropdownMenuContent>
|
|
173
|
+
</DropdownMenu>
|
|
174
|
+
</SidebarMenuItem>
|
|
175
|
+
) : (
|
|
176
|
+
<Collapsible
|
|
177
|
+
key={item.title}
|
|
178
|
+
asChild
|
|
179
|
+
defaultOpen={isGroupActive(item)}
|
|
180
|
+
className="group/collapsible"
|
|
181
|
+
>
|
|
182
|
+
<SidebarMenuItem>
|
|
183
|
+
<CollapsibleTrigger asChild>
|
|
184
|
+
<SidebarMenuButton
|
|
185
|
+
tooltip={item.title}
|
|
186
|
+
isActive={isGroupActive(item)}
|
|
187
|
+
>
|
|
188
|
+
{item.icon ? <item.icon /> : null}
|
|
189
|
+
<span>{item.title}</span>
|
|
190
|
+
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
|
191
|
+
</SidebarMenuButton>
|
|
192
|
+
</CollapsibleTrigger>
|
|
193
|
+
<CollapsibleContent>
|
|
194
|
+
<SidebarMenuSub>
|
|
195
|
+
{item.items.map(subItem => (
|
|
196
|
+
<SidebarMenuSubItem key={subItem.url}>
|
|
197
|
+
<SidebarMenuSubButton
|
|
198
|
+
asChild
|
|
199
|
+
isActive={isSubItemActive(
|
|
200
|
+
subItem,
|
|
201
|
+
item.items!
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
<Link
|
|
205
|
+
href={subItem.url}
|
|
206
|
+
onClick={handleLinkClick}
|
|
207
|
+
>
|
|
208
|
+
<span>{subItem.title}</span>
|
|
209
|
+
</Link>
|
|
210
|
+
</SidebarMenuSubButton>
|
|
211
|
+
</SidebarMenuSubItem>
|
|
212
|
+
))}
|
|
213
|
+
</SidebarMenuSub>
|
|
214
|
+
</CollapsibleContent>
|
|
215
|
+
</SidebarMenuItem>
|
|
216
|
+
</Collapsible>
|
|
217
|
+
)
|
|
218
|
+
) : (
|
|
219
|
+
<SidebarMenuItem key={item.title}>
|
|
220
|
+
<SidebarMenuButton
|
|
221
|
+
asChild
|
|
222
|
+
tooltip={item.title}
|
|
223
|
+
isActive={isItemActive(item)}
|
|
224
|
+
>
|
|
225
|
+
<Link href={item.url} onClick={handleLinkClick}>
|
|
226
|
+
{item.icon ? <item.icon /> : null}
|
|
227
|
+
<span>{item.title}</span>
|
|
228
|
+
</Link>
|
|
229
|
+
</SidebarMenuButton>
|
|
230
|
+
</SidebarMenuItem>
|
|
231
|
+
)
|
|
232
|
+
)}
|
|
233
|
+
</SidebarMenu>
|
|
234
|
+
</SidebarGroupContent>
|
|
235
|
+
</SidebarGroup>
|
|
236
|
+
))}
|
|
237
|
+
</SidebarContent>
|
|
238
|
+
<SidebarRail />
|
|
239
|
+
</Sidebar>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FC, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export const PageHeading = ({
|
|
4
|
+
title,
|
|
5
|
+
description,
|
|
6
|
+
icon: Icon,
|
|
7
|
+
children
|
|
8
|
+
}: {
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
icon?: FC<{ className?: string }>
|
|
12
|
+
children?: ReactNode
|
|
13
|
+
}) => {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex items-center justify-between gap-3">
|
|
16
|
+
<div className="flex items-center gap-3">
|
|
17
|
+
{Icon && (
|
|
18
|
+
<div className="bg-primary/10 text-primary flex h-10 w-10 shrink-0 items-center justify-center rounded-lg">
|
|
19
|
+
<Icon className="h-5 w-5" />
|
|
20
|
+
</div>
|
|
21
|
+
)}
|
|
22
|
+
<div>
|
|
23
|
+
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
|
24
|
+
<p className="text-muted-foreground">{description}</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
{children ? (
|
|
28
|
+
<div className="flex items-center gap-2">{children}</div>
|
|
29
|
+
) : null}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
+
import { FC, ReactNode } from 'react'
|
|
5
|
+
|
|
6
|
+
export const ReactQueryProvider: FC<{
|
|
7
|
+
children?: ReactNode
|
|
8
|
+
}> = ({ children }) => {
|
|
9
|
+
const client = new QueryClient({})
|
|
10
|
+
|
|
11
|
+
return <QueryClientProvider client={client}>{children}</QueryClientProvider>
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { FC, useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
export const ScreenSize: FC = () => {
|
|
6
|
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
function updateDimensions() {
|
|
10
|
+
setDimensions({
|
|
11
|
+
width: window.innerWidth,
|
|
12
|
+
height: window.innerHeight
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
updateDimensions()
|
|
17
|
+
window.addEventListener('resize', updateDimensions)
|
|
18
|
+
|
|
19
|
+
return () => {
|
|
20
|
+
window.removeEventListener('resize', updateDimensions)
|
|
21
|
+
}
|
|
22
|
+
}, [])
|
|
23
|
+
|
|
24
|
+
const { width, height } = dimensions
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="bg-foreground text-background fixed right-4 bottom-4 flex items-center gap-2 rounded-full p-2 text-xs font-medium">
|
|
28
|
+
<span>
|
|
29
|
+
{width.toLocaleString()} x {height.toLocaleString()}
|
|
30
|
+
</span>
|
|
31
|
+
<div className="bg-muted-foreground h-4 w-px" />
|
|
32
|
+
<span className="sm:hidden">XS</span>
|
|
33
|
+
<span className="hidden sm:inline md:hidden">SM</span>
|
|
34
|
+
<span className="hidden md:inline lg:hidden">MD</span>
|
|
35
|
+
<span className="hidden lg:inline xl:hidden">LG</span>
|
|
36
|
+
<span className="hidden xl:inline 2xl:hidden">XL</span>
|
|
37
|
+
<span className="hidden 2xl:inline">2XL</span>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
import { siteConfig } from '~/lib/siteConfig'
|
|
3
|
+
|
|
4
|
+
export const SiteFooter = () => {
|
|
5
|
+
return (
|
|
6
|
+
<footer className="border-t">
|
|
7
|
+
<div className="text-muted-foreground container flex flex-col items-center justify-between gap-4 py-6 text-sm sm:flex-row">
|
|
8
|
+
<p>
|
|
9
|
+
© {new Date().getUTCFullYear()} {siteConfig.name}. All rights
|
|
10
|
+
reserved.
|
|
11
|
+
</p>
|
|
12
|
+
<div className="flex gap-4">
|
|
13
|
+
<Link href="/terms" className="hover:text-foreground">
|
|
14
|
+
Terms
|
|
15
|
+
</Link>
|
|
16
|
+
<Link href="/privacy" className="hover:text-foreground">
|
|
17
|
+
Privacy
|
|
18
|
+
</Link>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</footer>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes'
|
|
4
|
+
import { FC, useEffect } from 'react'
|
|
5
|
+
|
|
6
|
+
function isTypingTarget(target: EventTarget | null) {
|
|
7
|
+
if (!(target instanceof HTMLElement)) {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
target.isContentEditable ||
|
|
13
|
+
target.tagName === 'INPUT' ||
|
|
14
|
+
target.tagName === 'TEXTAREA' ||
|
|
15
|
+
target.tagName === 'SELECT'
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ThemeHotkey() {
|
|
20
|
+
const { resolvedTheme, setTheme } = useTheme()
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
24
|
+
if (event.defaultPrevented || event.repeat) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (event.key.toLowerCase() !== 'd') {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isTypingTarget(event.target)) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
window.addEventListener('keydown', onKeyDown)
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
47
|
+
}
|
|
48
|
+
}, [resolvedTheme, setTheme])
|
|
49
|
+
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ThemeProvider: FC<{
|
|
54
|
+
children: React.ReactNode
|
|
55
|
+
}> = ({ children }) => {
|
|
56
|
+
return (
|
|
57
|
+
<NextThemesProvider
|
|
58
|
+
attribute="class"
|
|
59
|
+
defaultTheme="system"
|
|
60
|
+
enableSystem
|
|
61
|
+
disableTransitionOnChange
|
|
62
|
+
>
|
|
63
|
+
<ThemeHotkey />
|
|
64
|
+
{children}
|
|
65
|
+
</NextThemesProvider>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Moon, Sun } from 'lucide-react'
|
|
4
|
+
import { useTheme } from 'next-themes'
|
|
5
|
+
import { FC } from 'react'
|
|
6
|
+
import { Button } from './ui/button'
|
|
7
|
+
|
|
8
|
+
export const ThemeToggle: FC = () => {
|
|
9
|
+
const { theme, setTheme } = useTheme()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Button
|
|
13
|
+
variant="ghost"
|
|
14
|
+
size="icon"
|
|
15
|
+
aria-label="Toggle theme"
|
|
16
|
+
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
17
|
+
>
|
|
18
|
+
<Sun className="hidden h-5 w-5 dark:inline" />
|
|
19
|
+
<Moon className="inline h-5 w-5 dark:hidden" />
|
|
20
|
+
</Button>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Avatar as AvatarPrimitive } from 'radix-ui'
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
|
|
6
|
+
import { cn } from '~/lib/cn'
|
|
7
|
+
|
|
8
|
+
function Avatar({
|
|
9
|
+
className,
|
|
10
|
+
size = 'default',
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
|
13
|
+
size?: 'default' | 'sm' | 'lg'
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<AvatarPrimitive.Root
|
|
17
|
+
data-slot="avatar"
|
|
18
|
+
data-size={size}
|
|
19
|
+
className={cn(
|
|
20
|
+
'group/avatar after:border-border relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten',
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function AvatarImage({
|
|
29
|
+
className,
|
|
30
|
+
...props
|
|
31
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
32
|
+
return (
|
|
33
|
+
<AvatarPrimitive.Image
|
|
34
|
+
data-slot="avatar-image"
|
|
35
|
+
className={cn(
|
|
36
|
+
'aspect-square size-full rounded-full object-cover',
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function AvatarFallback({
|
|
45
|
+
className,
|
|
46
|
+
...props
|
|
47
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
48
|
+
return (
|
|
49
|
+
<AvatarPrimitive.Fallback
|
|
50
|
+
data-slot="avatar-fallback"
|
|
51
|
+
className={cn(
|
|
52
|
+
'bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs',
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
|
|
61
|
+
return (
|
|
62
|
+
<span
|
|
63
|
+
data-slot="avatar-badge"
|
|
64
|
+
className={cn(
|
|
65
|
+
'bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none',
|
|
66
|
+
'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
|
|
67
|
+
'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
|
|
68
|
+
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
|
|
69
|
+
className
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
data-slot="avatar-group"
|
|
80
|
+
className={cn(
|
|
81
|
+
'group/avatar-group *:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2',
|
|
82
|
+
className
|
|
83
|
+
)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function AvatarGroupCount({
|
|
90
|
+
className,
|
|
91
|
+
...props
|
|
92
|
+
}: React.ComponentProps<'div'>) {
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
data-slot="avatar-group-count"
|
|
96
|
+
className={cn(
|
|
97
|
+
'bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
|
|
98
|
+
className
|
|
99
|
+
)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
Avatar,
|
|
107
|
+
AvatarBadge,
|
|
108
|
+
AvatarFallback,
|
|
109
|
+
AvatarGroup,
|
|
110
|
+
AvatarGroupCount,
|
|
111
|
+
AvatarImage
|
|
112
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
+
import { Slot } from 'radix-ui'
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '~/lib/cn'
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
|
13
|
+
secondary:
|
|
14
|
+
'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
|
|
15
|
+
destructive:
|
|
16
|
+
'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
|
|
17
|
+
outline:
|
|
18
|
+
'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
|
|
19
|
+
ghost:
|
|
20
|
+
'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
|
|
21
|
+
link: 'text-primary underline-offset-4 hover:underline'
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: 'default'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
function Badge({
|
|
31
|
+
className,
|
|
32
|
+
variant = 'default',
|
|
33
|
+
asChild = false,
|
|
34
|
+
...props
|
|
35
|
+
}: React.ComponentProps<'span'> &
|
|
36
|
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
37
|
+
const Comp = asChild ? Slot.Root : 'span'
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Comp
|
|
41
|
+
data-slot="badge"
|
|
42
|
+
data-variant={variant}
|
|
43
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { Badge, badgeVariants }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
+
import { Slot } from 'radix-ui'
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '~/lib/cn'
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
|
13
|
+
outline:
|
|
14
|
+
'border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
|
|
15
|
+
secondary:
|
|
16
|
+
'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
|
17
|
+
ghost:
|
|
18
|
+
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
|
|
19
|
+
destructive:
|
|
20
|
+
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
|
|
21
|
+
link: 'text-primary underline-offset-4 hover:underline'
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default:
|
|
25
|
+
'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
|
26
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
27
|
+
sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5',
|
|
28
|
+
lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
|
29
|
+
icon: 'size-9',
|
|
30
|
+
'icon-xs':
|
|
31
|
+
"size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
|
|
32
|
+
'icon-sm':
|
|
33
|
+
'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
|
|
34
|
+
'icon-lg': 'size-10'
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: {
|
|
38
|
+
variant: 'default',
|
|
39
|
+
size: 'default'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
function Button({
|
|
45
|
+
className,
|
|
46
|
+
variant = 'default',
|
|
47
|
+
size = 'default',
|
|
48
|
+
asChild = false,
|
|
49
|
+
...props
|
|
50
|
+
}: React.ComponentProps<'button'> &
|
|
51
|
+
VariantProps<typeof buttonVariants> & {
|
|
52
|
+
asChild?: boolean
|
|
53
|
+
}) {
|
|
54
|
+
const Comp = asChild ? Slot.Root : 'button'
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Comp
|
|
58
|
+
data-slot="button"
|
|
59
|
+
data-variant={variant}
|
|
60
|
+
data-size={size}
|
|
61
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { Button, buttonVariants }
|