nexstruct 1.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/AGENTS.md +122 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/package.json +99 -0
- package/scaffold/generator.js +409 -0
- package/scaffold/index.js +20 -0
- package/scaffold/prompts.js +108 -0
- package/templates/api/axios/src/api/axios/client.api.ts +30 -0
- package/templates/api/axios/src/api/axios/users.api.ts +15 -0
- package/templates/api/fetch/src/api/fetch/client.api.ts +68 -0
- package/templates/api/fetch/src/api/fetch/users.api.ts +15 -0
- package/templates/api/trpc/src/api/trpc/client.api.ts +4 -0
- package/templates/api/trpc/src/api/trpc/router.api.ts +15 -0
- package/templates/api/trpc/src/api/trpc/server.client.api.ts +4 -0
- package/templates/api/trpc/src/providers/trpc.provider.tsx +24 -0
- package/templates/auth/clerk/src/auth/clerk/auth.service.ts +4 -0
- package/templates/auth/clerk/src/hooks/use-auth.hook.ts +13 -0
- package/templates/auth/clerk/src/middleware.ts +7 -0
- package/templates/auth/clerk/src/providers/auth.provider.tsx +6 -0
- package/templates/auth/next-auth/src/app/api/auth/[...nextauth]/route.ts +5 -0
- package/templates/auth/next-auth/src/auth/next-auth/auth.service.ts +45 -0
- package/templates/auth/next-auth/src/hooks/use-session.hook.ts +13 -0
- package/templates/auth/next-auth/src/providers/session.provider.tsx +6 -0
- package/templates/forms/formik/src/components/forms/login-form.component.tsx +30 -0
- package/templates/forms/formik/src/forms/formik/hooks/use-form-config.hook.ts +7 -0
- package/templates/forms/formik/src/forms/formik/schemas/example.schema.ts +8 -0
- package/templates/forms/react-hook-form/src/components/forms/login-form.component.tsx +27 -0
- package/templates/forms/react-hook-form/src/forms/react-hook-form/hooks/use-form.hook.ts +13 -0
- package/templates/forms/react-hook-form/src/forms/react-hook-form/schemas/example.schema.ts +15 -0
- package/templates/nextjs-base/next.config.ts +5 -0
- package/templates/nextjs-base/postcss.config.mjs +9 -0
- package/templates/nextjs-base/src/app/_components/navbar.tsx +88 -0
- package/templates/nextjs-base/src/app/_components/sidebar.tsx +223 -0
- package/templates/nextjs-base/src/app/error.tsx +39 -0
- package/templates/nextjs-base/src/app/globals.css +71 -0
- package/templates/nextjs-base/src/app/layout.tsx +21 -0
- package/templates/nextjs-base/src/app/loading.tsx +13 -0
- package/templates/nextjs-base/src/app/not-found.tsx +22 -0
- package/templates/nextjs-base/src/app/page.tsx +10 -0
- package/templates/nextjs-base/tailwind.config.ts +69 -0
- package/templates/shared/src/components/common/theme-toggle.component.tsx +31 -0
- package/templates/shared/src/components/common/toast/custom-message.component.tsx +18 -0
- package/templates/shared/src/components/common/toast/index.ts +8 -0
- package/templates/shared/src/components/common/toast/toast-message.component.tsx +112 -0
- package/templates/shared/src/hooks/use-debounce.hook.ts +12 -0
- package/templates/shared/src/hooks/use-fetch.hook.ts +42 -0
- package/templates/shared/src/hooks/use-intersection-observer.hook.ts +39 -0
- package/templates/shared/src/hooks/use-local-storage.hook.ts +30 -0
- package/templates/shared/src/hooks/use-media-query.hook.ts +26 -0
- package/templates/shared/src/hooks/use-toggle.hook.ts +12 -0
- package/templates/shared/src/lib/utils.util.ts +361 -0
- package/templates/shared/src/providers/theme.provider.tsx +17 -0
- package/templates/shared/src/providers/toast.provider.tsx +32 -0
- package/templates/shared/src/types/common.type.ts +34 -0
- package/templates/state/context/src/store/context/auth.context.tsx +47 -0
- package/templates/state/context/src/store/context/counter.context.tsx +41 -0
- package/templates/state/context/src/store/context/index.ts +2 -0
- package/templates/state/redux/src/providers/redux.provider.tsx +7 -0
- package/templates/state/redux/src/store/redux/hooks.store.ts +5 -0
- package/templates/state/redux/src/store/redux/index.ts +4 -0
- package/templates/state/redux/src/store/redux/slices/api.slice.ts +8 -0
- package/templates/state/redux/src/store/redux/slices/counter.slice.ts +24 -0
- package/templates/state/redux/src/store/redux/store.store.ts +13 -0
- package/templates/state/zustand/src/store/zustand/counter.store.ts +15 -0
- package/templates/state/zustand/src/store/zustand/index.ts +2 -0
- package/templates/state/zustand/src/store/zustand/user.store.ts +32 -0
- package/templates/ui/antd/COMPONENT_GUIDE.md +326 -0
- package/templates/ui/antd/src/app/examples/dialog/page.tsx +205 -0
- package/templates/ui/antd/src/app/examples/form/page.tsx +160 -0
- package/templates/ui/antd/src/app/examples/layout.tsx +125 -0
- package/templates/ui/antd/src/app/examples/page.tsx +64 -0
- package/templates/ui/antd/src/app/examples/table/page.tsx +118 -0
- package/templates/ui/antd/src/app/page.tsx +283 -0
- package/templates/ui/antd/src/components/common/DynamicTable/dynamic-table.component.tsx +79 -0
- package/templates/ui/antd/src/components/common/button/action-button.component.tsx +63 -0
- package/templates/ui/antd/src/components/common/dialog/dialog-wrapper.component.tsx +63 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/check-field.component.tsx +55 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/date-picker-field.component.tsx +80 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/limit-field.component.tsx +26 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/multi-check-field.component.tsx +56 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/number-field.component.tsx +100 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/otp-field.component.tsx +63 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/password-field.component.tsx +106 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/phone-number-field.component.tsx +78 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/radio-field.component.tsx +55 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/range-date-picker.component.tsx +66 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/search-field.component.tsx +24 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/select-field.component.tsx +82 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/single-check-field.component.tsx +50 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/single-select-field.component.tsx +86 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/string-number-field.component.tsx +80 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/switch-field.component.tsx +62 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/text-area-field.component.tsx +85 -0
- package/templates/ui/antd/src/components/common/fields/assets/components/text-field.component.tsx +88 -0
- package/templates/ui/antd/src/components/common/fields/assets/interface/input-props.type.ts +233 -0
- package/templates/ui/antd/src/components/common/fields/cusInputField.component.tsx +40 -0
- package/templates/ui/antd/src/components/common/pagination/pagination.component.tsx +27 -0
- package/templates/ui/antd/src/components/ui/avatar.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/badge.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/button.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/card.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/checkbox.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/dialog.component.tsx +9 -0
- package/templates/ui/antd/src/components/ui/dropdown-menu.component.tsx +10 -0
- package/templates/ui/antd/src/components/ui/form.component.tsx +12 -0
- package/templates/ui/antd/src/components/ui/input.component.tsx +13 -0
- package/templates/ui/antd/src/components/ui/label.component.tsx +18 -0
- package/templates/ui/antd/src/components/ui/popover.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/progress.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/radio-group.component.tsx +10 -0
- package/templates/ui/antd/src/components/ui/scroll-area.component.tsx +25 -0
- package/templates/ui/antd/src/components/ui/select.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/separator.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/sheet.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/switch.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/table.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/tabs.component.tsx +8 -0
- package/templates/ui/antd/src/components/ui/textarea.component.tsx +9 -0
- package/templates/ui/antd/src/components/ui/tooltip.component.tsx +8 -0
- package/templates/ui/antd/src/lib/theme.util.ts +40 -0
- package/templates/ui/antd/src/providers/antd.provider.tsx +13 -0
- package/templates/ui/mui/src/app/examples/layout.tsx +113 -0
- package/templates/ui/mui/src/app/examples/page.tsx +716 -0
- package/templates/ui/mui/src/app/page.tsx +298 -0
- package/templates/ui/mui/src/components/common/DynamicTable/dynamic-table.component.tsx +131 -0
- package/templates/ui/mui/src/components/common/button/action-button.component.tsx +57 -0
- package/templates/ui/mui/src/components/common/dialog/dialog-wrapper.component.tsx +55 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/check-field.component.tsx +51 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/date-picker-field.component.tsx +50 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/multi-check-field.component.tsx +14 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/number-field.component.tsx +59 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/password-field.component.tsx +87 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/phone-number-field.component.tsx +48 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/radio-field.component.tsx +37 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/search-field.component.tsx +41 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/select-field.component.tsx +77 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/single-check-field.component.tsx +39 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/single-select-field.component.tsx +56 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/string-number-field.component.tsx +52 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/switch-field.component.tsx +35 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/text-area-field.component.tsx +46 -0
- package/templates/ui/mui/src/components/common/fields/assets/components/text-field.component.tsx +51 -0
- package/templates/ui/mui/src/components/common/fields/assets/interface/input-props.type.ts +193 -0
- package/templates/ui/mui/src/components/common/fields/cusInputField.component.tsx +34 -0
- package/templates/ui/mui/src/components/common/pagination/pagination.component.tsx +59 -0
- package/templates/ui/mui/src/components/ui/avatar.component.tsx +19 -0
- package/templates/ui/mui/src/components/ui/badge.component.tsx +18 -0
- package/templates/ui/mui/src/components/ui/button.component.tsx +22 -0
- package/templates/ui/mui/src/components/ui/card.component.tsx +39 -0
- package/templates/ui/mui/src/components/ui/checkbox.component.tsx +21 -0
- package/templates/ui/mui/src/components/ui/dialog.component.tsx +38 -0
- package/templates/ui/mui/src/components/ui/dropdown-menu.component.tsx +43 -0
- package/templates/ui/mui/src/components/ui/form.component.tsx +98 -0
- package/templates/ui/mui/src/components/ui/input.component.tsx +15 -0
- package/templates/ui/mui/src/components/ui/label.component.tsx +15 -0
- package/templates/ui/mui/src/components/ui/popover.component.tsx +20 -0
- package/templates/ui/mui/src/components/ui/progress.component.tsx +19 -0
- package/templates/ui/mui/src/components/ui/radio-group.component.tsx +25 -0
- package/templates/ui/mui/src/components/ui/scroll-area.component.tsx +27 -0
- package/templates/ui/mui/src/components/ui/select.component.tsx +26 -0
- package/templates/ui/mui/src/components/ui/separator.component.tsx +11 -0
- package/templates/ui/mui/src/components/ui/sheet.component.tsx +44 -0
- package/templates/ui/mui/src/components/ui/switch.component.tsx +23 -0
- package/templates/ui/mui/src/components/ui/table.component.tsx +34 -0
- package/templates/ui/mui/src/components/ui/tabs.component.tsx +38 -0
- package/templates/ui/mui/src/components/ui/textarea.component.tsx +18 -0
- package/templates/ui/mui/src/components/ui/tooltip.component.tsx +24 -0
- package/templates/ui/mui/src/lib/theme.util.ts +73 -0
- package/templates/ui/mui/src/providers/mui.provider.tsx +13 -0
- package/templates/ui/shadcn/COMPONENT_GUIDE.md +306 -0
- package/templates/ui/shadcn/src/app/examples/dialog/page.tsx +122 -0
- package/templates/ui/shadcn/src/app/examples/form/page.tsx +107 -0
- package/templates/ui/shadcn/src/app/examples/layout.tsx +24 -0
- package/templates/ui/shadcn/src/app/examples/page.tsx +30 -0
- package/templates/ui/shadcn/src/app/examples/table/page.tsx +77 -0
- package/templates/ui/shadcn/src/app/page.tsx +20 -0
- package/templates/ui/shadcn/src/components/common/DynamicTable/dynamic-table.component.tsx +136 -0
- package/templates/ui/shadcn/src/components/common/button/action-button.component.tsx +68 -0
- package/templates/ui/shadcn/src/components/common/dialog/dialog-wrapper.component.tsx +58 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/check-field.component.tsx +52 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/date-picker-field.component.tsx +62 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/dynamic-file-upload-field.component.tsx +152 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/limit-field.component.tsx +73 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/multi-check-field.component.tsx +46 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/number-field.component.tsx +124 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/otp-field.component.tsx +61 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/password-field.component.tsx +110 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/phone-number-field.component.tsx +90 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/radio-field.component.tsx +41 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/range-date-picker.component.tsx +71 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/rich-text-editor.component.tsx +91 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/search-field.component.tsx +34 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/select-field.component.tsx +231 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/single-check-field.component.tsx +42 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/single-select-field.component.tsx +82 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/string-number-field.component.tsx +68 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/switch-field.component.tsx +61 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/text-area-field.component.tsx +62 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/text-area-with-file.component.tsx +142 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/text-field.component.tsx +80 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/tiny-editor.component.tsx +51 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/upload-profile-picture.component.tsx +103 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/components/upload-video-file.component.tsx +86 -0
- package/templates/ui/shadcn/src/components/common/fields/assets/interface/input-props.type.ts +198 -0
- package/templates/ui/shadcn/src/components/common/fields/cusInputField.component.tsx +52 -0
- package/templates/ui/shadcn/src/components/common/pagination/pagination.component.tsx +68 -0
- package/templates/ui/shadcn/src/components/ui/avatar.component.tsx +37 -0
- package/templates/ui/shadcn/src/components/ui/badge.component.tsx +28 -0
- package/templates/ui/shadcn/src/components/ui/button.component.tsx +52 -0
- package/templates/ui/shadcn/src/components/ui/card.component.tsx +46 -0
- package/templates/ui/shadcn/src/components/ui/checkbox.component.tsx +25 -0
- package/templates/ui/shadcn/src/components/ui/dialog.component.tsx +98 -0
- package/templates/ui/shadcn/src/components/ui/dropdown-menu.component.tsx +163 -0
- package/templates/ui/shadcn/src/components/ui/form.component.tsx +110 -0
- package/templates/ui/shadcn/src/components/ui/input-otp.component.tsx +64 -0
- package/templates/ui/shadcn/src/components/ui/input.component.tsx +23 -0
- package/templates/ui/shadcn/src/components/ui/label.component.tsx +23 -0
- package/templates/ui/shadcn/src/components/ui/popover.component.tsx +27 -0
- package/templates/ui/shadcn/src/components/ui/progress.component.tsx +22 -0
- package/templates/ui/shadcn/src/components/ui/radio-group.component.tsx +33 -0
- package/templates/ui/shadcn/src/components/ui/scroll-area.component.tsx +37 -0
- package/templates/ui/shadcn/src/components/ui/select.component.tsx +139 -0
- package/templates/ui/shadcn/src/components/ui/separator.component.tsx +23 -0
- package/templates/ui/shadcn/src/components/ui/sheet.component.tsx +89 -0
- package/templates/ui/shadcn/src/components/ui/switch.component.tsx +26 -0
- package/templates/ui/shadcn/src/components/ui/table.component.tsx +71 -0
- package/templates/ui/shadcn/src/components/ui/tabs.component.tsx +52 -0
- package/templates/ui/shadcn/src/components/ui/textarea.component.tsx +20 -0
- package/templates/ui/shadcn/src/components/ui/tooltip.component.tsx +25 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { httpBatchLink } from '@trpc/client';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { trpc } from '@/api/trpc/client.api';
|
|
6
|
+
|
|
7
|
+
export function TrpcProvider({ children }: { children: React.ReactNode }) {
|
|
8
|
+
const [queryClient] = useState(() => new QueryClient());
|
|
9
|
+
const [trpcClient] = useState(() =>
|
|
10
|
+
trpc.createClient({
|
|
11
|
+
links: [
|
|
12
|
+
httpBatchLink({
|
|
13
|
+
url: '/api/trpc',
|
|
14
|
+
}),
|
|
15
|
+
],
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
21
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
22
|
+
</trpc.Provider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useUser, useAuth as useClerkAuth } from '@clerk/nextjs';
|
|
2
|
+
|
|
3
|
+
export function useAuth() {
|
|
4
|
+
const { user, isLoaded } = useUser();
|
|
5
|
+
const { signOut } = useClerkAuth();
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
user: user ?? null,
|
|
9
|
+
isAuthenticated: !!user,
|
|
10
|
+
isLoading: !isLoaded,
|
|
11
|
+
logout: () => signOut(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { NextAuthOptions } from 'next-auth';
|
|
2
|
+
import CredentialsProvider from 'next-auth/providers/credentials';
|
|
3
|
+
import GithubProvider from 'next-auth/providers/github';
|
|
4
|
+
|
|
5
|
+
export const authOptions: NextAuthOptions = {
|
|
6
|
+
providers: [
|
|
7
|
+
GithubProvider({
|
|
8
|
+
clientId: process.env.GITHUB_ID || '',
|
|
9
|
+
clientSecret: process.env.GITHUB_SECRET || '',
|
|
10
|
+
}),
|
|
11
|
+
CredentialsProvider({
|
|
12
|
+
name: 'Credentials',
|
|
13
|
+
credentials: {
|
|
14
|
+
email: { label: 'Email', type: 'email' },
|
|
15
|
+
password: { label: 'Password', type: 'password' },
|
|
16
|
+
},
|
|
17
|
+
async authorize(credentials) {
|
|
18
|
+
if (!credentials?.email || !credentials?.password) return null;
|
|
19
|
+
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
body: JSON.stringify(credentials),
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
});
|
|
24
|
+
const user = await res.json();
|
|
25
|
+
if (res.ok && user) return user;
|
|
26
|
+
return null;
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
pages: {
|
|
31
|
+
signIn: '/auth/login',
|
|
32
|
+
error: '/auth/error',
|
|
33
|
+
},
|
|
34
|
+
session: { strategy: 'jwt' },
|
|
35
|
+
callbacks: {
|
|
36
|
+
async jwt({ token, user }) {
|
|
37
|
+
if (user) token.id = user.id;
|
|
38
|
+
return token;
|
|
39
|
+
},
|
|
40
|
+
async session({ session, token }) {
|
|
41
|
+
if (session.user) session.user.id = token.id as string;
|
|
42
|
+
return session;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useSession, signIn, signOut } from 'next-auth/react';
|
|
2
|
+
|
|
3
|
+
export function useAuthSession() {
|
|
4
|
+
const { data: session, status } = useSession();
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
user: session?.user ?? null,
|
|
8
|
+
isAuthenticated: !!session,
|
|
9
|
+
isLoading: status === 'loading',
|
|
10
|
+
login: signIn,
|
|
11
|
+
logout: () => signOut(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useFormik } from 'formik';
|
|
3
|
+
import { loginSchema, type LoginFormData } from '@/forms/formik/schemas/example.schema';
|
|
4
|
+
|
|
5
|
+
export function LoginForm({ onSubmit }: { onSubmit: (values: LoginFormData) => Promise<void> }) {
|
|
6
|
+
const formik = useFormik({
|
|
7
|
+
initialValues: { email: '', password: '' },
|
|
8
|
+
validationSchema: loginSchema,
|
|
9
|
+
onSubmit: async (values, { setSubmitting }) => {
|
|
10
|
+
await onSubmit(values);
|
|
11
|
+
setSubmitting(false);
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<form onSubmit={formik.handleSubmit} className="space-y-4">
|
|
17
|
+
<div>
|
|
18
|
+
<input {...formik.getFieldProps('email')} placeholder="Email" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2" />
|
|
19
|
+
{formik.touched.email && formik.errors.email && <p className="text-sm text-destructive">{formik.errors.email}</p>}
|
|
20
|
+
</div>
|
|
21
|
+
<div>
|
|
22
|
+
<input {...formik.getFieldProps('password')} type="password" placeholder="Password" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2" />
|
|
23
|
+
{formik.touched.password && formik.errors.password && <p className="text-sm text-destructive">{formik.errors.password}</p>}
|
|
24
|
+
</div>
|
|
25
|
+
<button type="submit" disabled={formik.isSubmitting} className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground">
|
|
26
|
+
{formik.isSubmitting ? 'Loading...' : 'Login'}
|
|
27
|
+
</button>
|
|
28
|
+
</form>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as yup from 'yup';
|
|
2
|
+
|
|
3
|
+
export const loginSchema = yup.object({
|
|
4
|
+
email: yup.string().email('Invalid email').required('Email is required'),
|
|
5
|
+
password: yup.string().min(6, 'Minimum 6 characters').required('Password is required'),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export type LoginFormData = yup.InferType<typeof loginSchema>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useZodForm } from '@/forms/react-hook-form/hooks/use-form.hook';
|
|
3
|
+
import { loginSchema, type LoginFormData } from '@/forms/react-hook-form/schemas/example.schema';
|
|
4
|
+
|
|
5
|
+
interface LoginFormProps {
|
|
6
|
+
onSubmit: (data: LoginFormData) => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function LoginForm({ onSubmit }: LoginFormProps) {
|
|
10
|
+
const { register, handleSubmit, formState: { errors, isSubmitting } } = useZodForm(loginSchema);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
14
|
+
<div>
|
|
15
|
+
<input {...register('email')} placeholder="Email" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2" />
|
|
16
|
+
{errors.email && <p className="text-sm text-destructive">{errors.email.message}</p>}
|
|
17
|
+
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<input {...register('password')} type="password" placeholder="Password" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2" />
|
|
20
|
+
{errors.password && <p className="text-sm text-destructive">{errors.password.message}</p>}
|
|
21
|
+
</div>
|
|
22
|
+
<button type="submit" disabled={isSubmitting} className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground">
|
|
23
|
+
{isSubmitting ? 'Loading...' : 'Login'}
|
|
24
|
+
</button>
|
|
25
|
+
</form>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useForm, type UseFormProps } from 'react-hook-form';
|
|
2
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
3
|
+
import { z, type ZodSchema } from 'zod';
|
|
4
|
+
|
|
5
|
+
export function useZodForm<T extends ZodSchema>(
|
|
6
|
+
schema: T,
|
|
7
|
+
options?: Omit<UseFormProps<z.infer<T>>, 'resolver'>
|
|
8
|
+
) {
|
|
9
|
+
return useForm<z.infer<T>>({
|
|
10
|
+
resolver: zodResolver(schema),
|
|
11
|
+
...options,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const loginSchema = z.object({
|
|
4
|
+
email: z.string().email('Invalid email address'),
|
|
5
|
+
password: z.string().min(6, 'Password must be at least 6 characters'),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const signupSchema = z.object({
|
|
9
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
10
|
+
email: z.string().email('Invalid email address'),
|
|
11
|
+
password: z.string().min(6, 'Password must be at least 6 characters'),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type LoginFormData = z.infer<typeof loginSchema>;
|
|
15
|
+
export type SignupFormData = z.infer<typeof signupSchema>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { ThemeToggle } from '@/components/common/theme-toggle.component';
|
|
7
|
+
|
|
8
|
+
const links = [
|
|
9
|
+
{ href: '/lab', label: 'Component Lab' },
|
|
10
|
+
{ href: '/test/form', label: 'Form Test' },
|
|
11
|
+
{ href: '/test/dialog', label: 'Dialog Test' },
|
|
12
|
+
{ href: '/test/table', label: 'Table Test' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export default function NavBar() {
|
|
16
|
+
const pathname = usePathname();
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
21
|
+
<div className="max-w-6xl mx-auto flex h-14 items-center px-6">
|
|
22
|
+
<Link href="/" className="font-bold text-foreground shrink-0 tracking-tight">
|
|
23
|
+
Nex<span className="text-primary">struct</span>
|
|
24
|
+
</Link>
|
|
25
|
+
|
|
26
|
+
<nav className="hidden md:flex items-center gap-1 ml-8">
|
|
27
|
+
{links.map((link) => {
|
|
28
|
+
const active = pathname === link.href || pathname.startsWith(link.href + '/');
|
|
29
|
+
return (
|
|
30
|
+
<Link
|
|
31
|
+
key={link.href}
|
|
32
|
+
href={link.href}
|
|
33
|
+
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
34
|
+
active
|
|
35
|
+
? 'bg-primary/10 text-primary'
|
|
36
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
37
|
+
}`}
|
|
38
|
+
>
|
|
39
|
+
{link.label}
|
|
40
|
+
</Link>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</nav>
|
|
44
|
+
|
|
45
|
+
<div className="ml-auto flex items-center gap-1">
|
|
46
|
+
<ThemeToggle />
|
|
47
|
+
<button
|
|
48
|
+
onClick={() => setOpen(!open)}
|
|
49
|
+
className="md:hidden p-2 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent"
|
|
50
|
+
aria-label="Toggle menu"
|
|
51
|
+
>
|
|
52
|
+
{open ? (
|
|
53
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
|
54
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
|
55
|
+
</svg>
|
|
56
|
+
) : (
|
|
57
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
|
58
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
|
59
|
+
</svg>
|
|
60
|
+
)}
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{open && (
|
|
66
|
+
<nav className="md:hidden border-t px-6 py-3 space-y-1 bg-background">
|
|
67
|
+
{links.map((link) => {
|
|
68
|
+
const active = pathname === link.href || pathname.startsWith(link.href + '/');
|
|
69
|
+
return (
|
|
70
|
+
<Link
|
|
71
|
+
key={link.href}
|
|
72
|
+
href={link.href}
|
|
73
|
+
onClick={() => setOpen(false)}
|
|
74
|
+
className={`block px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
75
|
+
active
|
|
76
|
+
? 'bg-primary/10 text-primary'
|
|
77
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
78
|
+
}`}
|
|
79
|
+
>
|
|
80
|
+
{link.label}
|
|
81
|
+
</Link>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</nav>
|
|
85
|
+
)}
|
|
86
|
+
</header>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { useState, useCallback, type ReactNode } from 'react';
|
|
6
|
+
import { ThemeToggle } from '@/components/common/theme-toggle.component';
|
|
7
|
+
|
|
8
|
+
type NavItem = {
|
|
9
|
+
label: string;
|
|
10
|
+
href?: string;
|
|
11
|
+
icon: React.ReactNode;
|
|
12
|
+
children?: { label: string; href: string }[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const navItems: NavItem[] = [
|
|
16
|
+
{
|
|
17
|
+
label: 'Component Lab',
|
|
18
|
+
href: '/lab',
|
|
19
|
+
icon: (
|
|
20
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
21
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 0-6.23.693L5 14.5m14.8.8 1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0 1 12 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
|
|
22
|
+
</svg>
|
|
23
|
+
),
|
|
24
|
+
children: [
|
|
25
|
+
{ label: 'All Components', href: '/lab' },
|
|
26
|
+
{ label: 'Form Fields', href: '/lab?category=field' },
|
|
27
|
+
{ label: 'Common', href: '/lab?category=common' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: 'Form Test',
|
|
32
|
+
href: '/test/form',
|
|
33
|
+
icon: (
|
|
34
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
35
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
|
|
36
|
+
</svg>
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: 'Dialog Test',
|
|
41
|
+
href: '/test/dialog',
|
|
42
|
+
icon: (
|
|
43
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
44
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
45
|
+
</svg>
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: 'Table Test',
|
|
50
|
+
href: '/test/table',
|
|
51
|
+
icon: (
|
|
52
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
53
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15a2.25 2.25 0 0 1 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" />
|
|
54
|
+
</svg>
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export default function Sidebar({ children }: { children: ReactNode }) {
|
|
60
|
+
const pathname = usePathname();
|
|
61
|
+
const [hovered, setHovered] = useState(false);
|
|
62
|
+
const [pinned, setPinned] = useState(false);
|
|
63
|
+
const [openMenus, setOpenMenus] = useState<Set<string>>(new Set());
|
|
64
|
+
|
|
65
|
+
const expanded = hovered || pinned;
|
|
66
|
+
|
|
67
|
+
const toggleMenu = useCallback((label: string) => {
|
|
68
|
+
setOpenMenus((prev) => {
|
|
69
|
+
const next = new Set(prev);
|
|
70
|
+
if (next.has(label)) next.delete(label);
|
|
71
|
+
else next.add(label);
|
|
72
|
+
return next;
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const isActive = (href?: string) => href && (pathname === href || pathname.startsWith(href + '/'));
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex min-h-screen">
|
|
80
|
+
<nav
|
|
81
|
+
onMouseEnter={() => setHovered(true)}
|
|
82
|
+
onMouseLeave={() => { if (!pinned) setHovered(false); }}
|
|
83
|
+
className="fixed left-0 top-0 z-40 flex flex-col h-full bg-card border-r border-border overflow-hidden scrollbar-none transition-[width] duration-300 ease-in-out"
|
|
84
|
+
style={{ width: expanded ? '260px' : '60px' }}
|
|
85
|
+
>
|
|
86
|
+
{/* Brand */}
|
|
87
|
+
<div className="flex items-center h-14 px-4 border-b border-border shrink-0">
|
|
88
|
+
<Link href="/" className="flex items-center gap-3 min-w-0 group">
|
|
89
|
+
<div className="h-9 w-9 rounded-xl bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center shrink-0 shadow-sm group-hover:shadow-md transition-shadow relative overflow-hidden">
|
|
90
|
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(255,255,255,0.2),transparent_70%)]" />
|
|
91
|
+
<svg className="h-5 w-5 text-white relative" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
|
|
92
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.087 4.113" />
|
|
93
|
+
</svg>
|
|
94
|
+
</div>
|
|
95
|
+
<span
|
|
96
|
+
className="font-bold text-foreground text-sm whitespace-nowrap transition-opacity duration-300"
|
|
97
|
+
style={{ opacity: expanded ? 1 : 0 }}
|
|
98
|
+
>
|
|
99
|
+
Nex<span className="text-primary">struct</span>
|
|
100
|
+
</span>
|
|
101
|
+
</Link>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Nav items */}
|
|
105
|
+
<div className="flex-1 py-3 px-2 space-y-0.5 overflow-y-auto scrollbar-none">
|
|
106
|
+
{navItems.map((item) => {
|
|
107
|
+
const active = isActive(item.href);
|
|
108
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
109
|
+
const menuOpen = openMenus.has(item.label);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div key={item.label}>
|
|
113
|
+
<div
|
|
114
|
+
role="button"
|
|
115
|
+
tabIndex={0}
|
|
116
|
+
onClick={() => {
|
|
117
|
+
if (hasChildren && expanded) {
|
|
118
|
+
toggleMenu(item.label);
|
|
119
|
+
} else if (item.href) {
|
|
120
|
+
window.location.href = item.href;
|
|
121
|
+
}
|
|
122
|
+
}}
|
|
123
|
+
onKeyDown={(e) => {
|
|
124
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
125
|
+
if (hasChildren && expanded) toggleMenu(item.label);
|
|
126
|
+
else if (item.href) window.location.href = item.href;
|
|
127
|
+
}
|
|
128
|
+
}}
|
|
129
|
+
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-colors whitespace-nowrap ${
|
|
130
|
+
active
|
|
131
|
+
? 'bg-primary/10 text-primary'
|
|
132
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
133
|
+
}`}
|
|
134
|
+
>
|
|
135
|
+
<span className="shrink-0">{item.icon}</span>
|
|
136
|
+
<span
|
|
137
|
+
className="text-sm font-medium transition-opacity duration-300"
|
|
138
|
+
style={{ opacity: expanded ? 1 : 0 }}
|
|
139
|
+
>
|
|
140
|
+
{item.label}
|
|
141
|
+
</span>
|
|
142
|
+
{hasChildren && expanded && (
|
|
143
|
+
<svg
|
|
144
|
+
className={`h-4 w-4 ml-auto transition-transform duration-200 ${menuOpen ? 'rotate-90' : ''}`}
|
|
145
|
+
fill="none"
|
|
146
|
+
viewBox="0 0 24 24"
|
|
147
|
+
strokeWidth={2}
|
|
148
|
+
stroke="currentColor"
|
|
149
|
+
>
|
|
150
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
151
|
+
</svg>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Nested sub-menu */}
|
|
156
|
+
{hasChildren && expanded && (
|
|
157
|
+
<div
|
|
158
|
+
className="overflow-hidden scrollbar-none transition-all duration-250 ease-in-out"
|
|
159
|
+
style={{
|
|
160
|
+
maxHeight: menuOpen ? `${(item.children?.length || 0) * 44}px` : '0',
|
|
161
|
+
opacity: menuOpen ? 1 : 0,
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{item.children?.map((child) => {
|
|
165
|
+
const childActive = pathname === child.href;
|
|
166
|
+
return (
|
|
167
|
+
<Link
|
|
168
|
+
key={child.href}
|
|
169
|
+
href={child.href}
|
|
170
|
+
onClick={() => setHovered(false)}
|
|
171
|
+
className={`flex items-center gap-3 ml-9 px-3 py-2 rounded-md text-sm transition-colors ${
|
|
172
|
+
childActive
|
|
173
|
+
? 'text-primary font-medium'
|
|
174
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
175
|
+
}`}
|
|
176
|
+
>
|
|
177
|
+
<span className="h-1 w-1 rounded-full bg-current shrink-0" />
|
|
178
|
+
{child.label}
|
|
179
|
+
</Link>
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
})}
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Toggle pin + Theme */}
|
|
190
|
+
<div className="border-t border-border px-2 py-2 shrink-0 space-y-1">
|
|
191
|
+
<button
|
|
192
|
+
onClick={() => { setPinned((p) => !p); }}
|
|
193
|
+
className={`flex items-center gap-3 w-full px-3 py-2.5 rounded-lg transition-colors ${
|
|
194
|
+
pinned ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
195
|
+
}`}
|
|
196
|
+
>
|
|
197
|
+
<svg className="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
|
198
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9V4.5M9 9H4.5M9 9 3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5 5.25 5.25" />
|
|
199
|
+
</svg>
|
|
200
|
+
<span
|
|
201
|
+
className="text-sm font-medium whitespace-nowrap transition-opacity duration-300"
|
|
202
|
+
style={{ opacity: expanded ? 1 : 0 }}
|
|
203
|
+
>
|
|
204
|
+
{pinned ? 'Unpin sidebar' : 'Pin sidebar'}
|
|
205
|
+
</span>
|
|
206
|
+
</button>
|
|
207
|
+
|
|
208
|
+
</div>
|
|
209
|
+
</nav>
|
|
210
|
+
|
|
211
|
+
{/* Main content */}
|
|
212
|
+
<main
|
|
213
|
+
className="flex-1 transition-[padding] duration-300 ease-in-out relative"
|
|
214
|
+
style={{ paddingLeft: expanded ? '260px' : '60px' }}
|
|
215
|
+
>
|
|
216
|
+
<div className="fixed top-3 right-4 z-50">
|
|
217
|
+
<ThemeToggle />
|
|
218
|
+
</div>
|
|
219
|
+
{children}
|
|
220
|
+
</main>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export default function Error({
|
|
4
|
+
error,
|
|
5
|
+
reset,
|
|
6
|
+
}: {
|
|
7
|
+
error: Error & { digest?: string };
|
|
8
|
+
reset: () => void;
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="min-h-screen flex flex-col items-center justify-center bg-background px-4">
|
|
12
|
+
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-destructive/10 mb-6">
|
|
13
|
+
<svg
|
|
14
|
+
className="h-10 w-10 text-destructive"
|
|
15
|
+
fill="none"
|
|
16
|
+
viewBox="0 0 24 24"
|
|
17
|
+
strokeWidth={1.5}
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
>
|
|
20
|
+
<path
|
|
21
|
+
strokeLinecap="round"
|
|
22
|
+
strokeLinejoin="round"
|
|
23
|
+
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
|
|
24
|
+
/>
|
|
25
|
+
</svg>
|
|
26
|
+
</div>
|
|
27
|
+
<h1 className="text-2xl font-semibold text-foreground mb-2">Something went wrong</h1>
|
|
28
|
+
<p className="text-sm text-muted-foreground text-center max-w-md mb-6">
|
|
29
|
+
{error.message || 'An unexpected error occurred. Please try again.'}
|
|
30
|
+
</p>
|
|
31
|
+
<button
|
|
32
|
+
onClick={reset}
|
|
33
|
+
className="inline-flex items-center justify-center rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 transition-colors"
|
|
34
|
+
>
|
|
35
|
+
Try again
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|