Spaces:
Sleeping
Sleeping
Merge remote-tracking branch 'upstream/main'
Browse files- CHANGELOG.md +5 -4
- package-lock.json +15 -1
- package.json +1 -0
- src/app/dashboard/page.tsx +76 -0
- src/app/layout.tsx +10 -3
- src/components/AuthLayout/index.tsx +45 -0
- src/components/HeroSection.tsx +1 -1
- src/components/Navbar.tsx +44 -13
- src/components/{atoms β base}/Badge/index.tsx +2 -0
- src/components/{atoms β base}/Button/index.tsx +3 -1
- src/components/{atoms β base}/Card/index.tsx +2 -0
- src/components/{atoms β base}/Input/index.tsx +2 -0
- src/components/base/Label/index.tsx +23 -0
- src/components/base/Select/Select.tsx +37 -0
- src/components/base/Select/SelectContent.tsx +27 -0
- src/components/base/Select/SelectContext.tsx +12 -0
- src/components/base/Select/SelectItem.tsx +39 -0
- src/components/base/Select/SelectTrigger.tsx +27 -0
- src/components/base/Select/SelectValue.tsx +13 -0
- src/components/base/Select/index.tsx +5 -0
- src/components/base/Switch/index.tsx +33 -0
- src/components/{atoms β base}/index.ts +3 -0
- src/components/dashboard/JobCard.tsx +45 -0
- src/components/dashboard/JobFilters.tsx +55 -0
- src/components/dashboard/SubscribeJob.tsx +22 -0
- src/components/homepage/AnnouncementBanner.tsx +1 -1
- src/components/homepage/CustomizationSection.tsx +1 -1
- src/components/homepage/FeatureSection.tsx +1 -1
CHANGELOG.md
CHANGED
@@ -1,16 +1,17 @@
|
|
1 |
-
# [1.1.0](https://github.com/
|
2 |
|
3 |
|
4 |
### Features
|
5 |
|
6 |
-
* **
|
7 |
|
8 |
-
# 1.0.0 (2025-
|
9 |
|
10 |
|
11 |
### Features
|
12 |
|
13 |
-
* **homepage:** implement banner section ([
|
|
|
14 |
|
15 |
## [3.60.5](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.60.4...v3.60.5) (2024-12-20)
|
16 |
|
|
|
1 |
+
# [1.1.0](https://github.com/thanhhoang021095/x-app/compare/v1.0.0...v1.1.0) (2025-02-15)
|
2 |
|
3 |
|
4 |
### Features
|
5 |
|
6 |
+
* **dashoard:** implement page dashboard ([#4](https://github.com/thanhhoang021095/x-app/issues/4)) ([e0eaa09](https://github.com/thanhhoang021095/x-app/commit/e0eaa09b7c0327827c39fc939ec9496cae6cbda0))
|
7 |
|
8 |
+
# 1.0.0 (2025-02-12)
|
9 |
|
10 |
|
11 |
### Features
|
12 |
|
13 |
+
* **homepage:** implement banner section ([#3](https://github.com/thanhhoang021095/x-app/issues/3)) ([bc70bde](https://github.com/thanhhoang021095/x-app/commit/bc70bdec7f7aa04fc5c0ce35aabc668b285ab41f))
|
14 |
+
* **login:** init form signin + signup ([#2](https://github.com/thanhhoang021095/x-app/issues/2)) ([a0eae6d](https://github.com/thanhhoang021095/x-app/commit/a0eae6d6c0eb3b02fa81e61f4586669bdfb4b3a3))
|
15 |
|
16 |
## [3.60.5](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.60.4...v3.60.5) (2024-12-20)
|
17 |
|
package-lock.json
CHANGED
@@ -30,6 +30,7 @@
|
|
30 |
"react-dom": "19.0.0",
|
31 |
"react-hook-form": "^7.54.0",
|
32 |
"tailwind-merge": "^2.6.0",
|
|
|
33 |
"zod": "^3.24.0"
|
34 |
},
|
35 |
"devDependencies": {
|
@@ -24392,7 +24393,6 @@
|
|
24392 |
"version": "4.0.8",
|
24393 |
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
24394 |
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
24395 |
-
"dev": true,
|
24396 |
"license": "MIT"
|
24397 |
},
|
24398 |
"node_modules/lodash.escaperegexp": {
|
@@ -37429,6 +37429,20 @@
|
|
37429 |
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
37430 |
}
|
37431 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37432 |
"node_modules/util": {
|
37433 |
"version": "0.12.5",
|
37434 |
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
|
|
30 |
"react-dom": "19.0.0",
|
31 |
"react-hook-form": "^7.54.0",
|
32 |
"tailwind-merge": "^2.6.0",
|
33 |
+
"usehooks-ts": "^3.1.1",
|
34 |
"zod": "^3.24.0"
|
35 |
},
|
36 |
"devDependencies": {
|
|
|
24393 |
"version": "4.0.8",
|
24394 |
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
24395 |
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
|
|
24396 |
"license": "MIT"
|
24397 |
},
|
24398 |
"node_modules/lodash.escaperegexp": {
|
|
|
37429 |
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
37430 |
}
|
37431 |
},
|
37432 |
+
"node_modules/usehooks-ts": {
|
37433 |
+
"version": "3.1.1",
|
37434 |
+
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz",
|
37435 |
+
"integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==",
|
37436 |
+
"dependencies": {
|
37437 |
+
"lodash.debounce": "^4.0.8"
|
37438 |
+
},
|
37439 |
+
"engines": {
|
37440 |
+
"node": ">=16.15.0"
|
37441 |
+
},
|
37442 |
+
"peerDependencies": {
|
37443 |
+
"react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
37444 |
+
}
|
37445 |
+
},
|
37446 |
"node_modules/util": {
|
37447 |
"version": "0.12.5",
|
37448 |
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
package.json
CHANGED
@@ -49,6 +49,7 @@
|
|
49 |
"react-dom": "19.0.0",
|
50 |
"react-hook-form": "^7.54.0",
|
51 |
"tailwind-merge": "^2.6.0",
|
|
|
52 |
"zod": "^3.24.0"
|
53 |
},
|
54 |
"devDependencies": {
|
|
|
49 |
"react-dom": "19.0.0",
|
50 |
"react-hook-form": "^7.54.0",
|
51 |
"tailwind-merge": "^2.6.0",
|
52 |
+
"usehooks-ts": "^3.1.1",
|
53 |
"zod": "^3.24.0"
|
54 |
},
|
55 |
"devDependencies": {
|
src/app/dashboard/page.tsx
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import JobCard from '@/components/dashboard/JobCard';
|
2 |
+
import JobFilters from '@/components/dashboard/JobFilters';
|
3 |
+
import SubscribeJob from '@/components/dashboard/SubscribeJob';
|
4 |
+
import { Suspense } from 'react';
|
5 |
+
|
6 |
+
const jobCategories = [
|
7 |
+
'Design',
|
8 |
+
'Full-stack',
|
9 |
+
'Back-end',
|
10 |
+
'Front-end',
|
11 |
+
'QA Engineer',
|
12 |
+
'Data Engineer',
|
13 |
+
'Mobile',
|
14 |
+
'AI Training & Labeling',
|
15 |
+
'DevOps',
|
16 |
+
];
|
17 |
+
|
18 |
+
const jobs = [
|
19 |
+
{
|
20 |
+
id: 1,
|
21 |
+
title: 'Software Engineer - Confidential Computing',
|
22 |
+
company: 'Nethermind',
|
23 |
+
logo: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Screenshot%202025-02-15%20at%2021.58.09-ZsGA1DKAeyYCxqdocTlWoRuaKkhrH1.png',
|
24 |
+
location: 'Anywhere',
|
25 |
+
type: 'Freelancer',
|
26 |
+
postedAt: '2 days ago',
|
27 |
+
},
|
28 |
+
{
|
29 |
+
id: 2,
|
30 |
+
title: 'Marketing Manager - Payments Industry',
|
31 |
+
company: 'HitPay',
|
32 |
+
logo: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Screenshot%202025-02-15%20at%2021.58.09-ZsGA1DKAeyYCxqdocTlWoRuaKkhrH1.png',
|
33 |
+
location: 'Anywhere',
|
34 |
+
type: 'Full-time',
|
35 |
+
postedAt: '2 days ago',
|
36 |
+
},
|
37 |
+
// Add more jobs as needed
|
38 |
+
];
|
39 |
+
|
40 |
+
export default function Page() {
|
41 |
+
return (
|
42 |
+
<main className="min-h-screen bg-background pt-20">
|
43 |
+
<div className="container mx-auto px-4 py-8">
|
44 |
+
{/* Categories */}
|
45 |
+
<div className="mb-8 flex flex-wrap gap-2">
|
46 |
+
{jobCategories.map(category => (
|
47 |
+
<button type="button" key={category} className="rounded-full bg-muted px-4 py-2 transition-colors hover:bg-muted/80">
|
48 |
+
{category}
|
49 |
+
</button>
|
50 |
+
))}
|
51 |
+
</div>
|
52 |
+
|
53 |
+
{/* Filters and Search */}
|
54 |
+
<Suspense fallback={<div>Loading filters...</div>}>
|
55 |
+
<JobFilters />
|
56 |
+
</Suspense>
|
57 |
+
|
58 |
+
{/* Job Listings */}
|
59 |
+
<div className="mt-8 grid grid-cols-1 gap-8 lg:grid-cols-3">
|
60 |
+
<div className="space-y-4 lg:col-span-2">
|
61 |
+
{jobs.map(job => (
|
62 |
+
<JobCard key={job.id} job={job} />
|
63 |
+
))}
|
64 |
+
</div>
|
65 |
+
|
66 |
+
{/* Newsletter Signup */}
|
67 |
+
<div className="lg:col-span-1">
|
68 |
+
<div className="sticky top-4">
|
69 |
+
<SubscribeJob />
|
70 |
+
</div>
|
71 |
+
</div>
|
72 |
+
</div>
|
73 |
+
</div>
|
74 |
+
</main>
|
75 |
+
);
|
76 |
+
}
|
src/app/layout.tsx
CHANGED
@@ -1,5 +1,7 @@
|
|
1 |
import type { Metadata } from 'next';
|
|
|
2 |
import { Navbar } from '@/components/Navbar';
|
|
|
3 |
import { isDevMode } from '@/utils/Helpers';
|
4 |
import { Inter, Outfit } from 'next/font/google';
|
5 |
import '../styles/global.css';
|
@@ -26,10 +28,15 @@ export default function RootLayout({
|
|
26 |
children: React.ReactNode;
|
27 |
}) {
|
28 |
return (
|
29 |
-
<html lang=
|
30 |
<body className={`${inter.variable} ${outfit.variable} font-sans antialiased`} suppressHydrationWarning={isDevMode}>
|
31 |
-
<
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
33 |
</body>
|
34 |
</html>
|
35 |
);
|
|
|
1 |
import type { Metadata } from 'next';
|
2 |
+
import AuthLayout from '@/components/AuthLayout';
|
3 |
import { Navbar } from '@/components/Navbar';
|
4 |
+
import { routing } from '@/libs/i18nNavigation';
|
5 |
import { isDevMode } from '@/utils/Helpers';
|
6 |
import { Inter, Outfit } from 'next/font/google';
|
7 |
import '../styles/global.css';
|
|
|
28 |
children: React.ReactNode;
|
29 |
}) {
|
30 |
return (
|
31 |
+
<html lang={routing.defaultLocale} suppressHydrationWarning>
|
32 |
<body className={`${inter.variable} ${outfit.variable} font-sans antialiased`} suppressHydrationWarning={isDevMode}>
|
33 |
+
<AuthLayout params={{
|
34 |
+
locale: routing.defaultLocale,
|
35 |
+
}}
|
36 |
+
>
|
37 |
+
<Navbar />
|
38 |
+
{children}
|
39 |
+
</AuthLayout>
|
40 |
</body>
|
41 |
</html>
|
42 |
);
|
src/components/AuthLayout/index.tsx
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ClerkProvider } from '@clerk/nextjs';
|
2 |
+
import { setRequestLocale } from 'next-intl/server';
|
3 |
+
|
4 |
+
export default async function AuthLayout(props: {
|
5 |
+
children: React.ReactNode;
|
6 |
+
params: { locale: string };
|
7 |
+
}) {
|
8 |
+
const { locale } = await props.params;
|
9 |
+
setRequestLocale(locale);
|
10 |
+
let signInUrl = '/sign-in';
|
11 |
+
let signUpUrl = '/sign-up';
|
12 |
+
let dashboardUrl = '/dashboard';
|
13 |
+
let afterSignOutUrl = '/';
|
14 |
+
|
15 |
+
signInUrl = `/${signInUrl}`;
|
16 |
+
signUpUrl = `/${signUpUrl}`;
|
17 |
+
dashboardUrl = `/`;
|
18 |
+
afterSignOutUrl = `/${afterSignOutUrl}`;
|
19 |
+
|
20 |
+
return (
|
21 |
+
<ClerkProvider
|
22 |
+
localization={{
|
23 |
+
signIn: {
|
24 |
+
start: {
|
25 |
+
title: 'Sign In',
|
26 |
+
},
|
27 |
+
},
|
28 |
+
signUp: {
|
29 |
+
start: {
|
30 |
+
title: 'Sign Up',
|
31 |
+
},
|
32 |
+
},
|
33 |
+
}}
|
34 |
+
signInUrl={signInUrl}
|
35 |
+
signUpUrl={signUpUrl}
|
36 |
+
signInFallbackRedirectUrl={dashboardUrl}
|
37 |
+
signUpFallbackRedirectUrl={dashboardUrl}
|
38 |
+
afterSignOutUrl={afterSignOutUrl}
|
39 |
+
>
|
40 |
+
<div className="mx-auto flex w-full items-center justify-center pt-32">
|
41 |
+
{props.children}
|
42 |
+
</div>
|
43 |
+
</ClerkProvider>
|
44 |
+
);
|
45 |
+
}
|
src/components/HeroSection.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { Button } from '@/components/
|
2 |
import { Download } from 'lucide-react';
|
3 |
import Image from 'next/image';
|
4 |
|
|
|
1 |
+
import { Button } from '@/components/base';
|
2 |
import { Download } from 'lucide-react';
|
3 |
import Image from 'next/image';
|
4 |
|
src/components/Navbar.tsx
CHANGED
@@ -1,9 +1,44 @@
|
|
1 |
'use client';
|
2 |
-
|
3 |
-
import {
|
|
|
|
|
4 |
import Link from 'next/link';
|
|
|
5 |
|
6 |
export function Navbar() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
return (
|
8 |
<nav className="fixed top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
9 |
<div className="container flex h-14 items-center">
|
@@ -15,11 +50,8 @@ export function Navbar() {
|
|
15 |
</div>
|
16 |
|
17 |
<div className="flex items-center gap-6 text-sm">
|
18 |
-
<Link href="/
|
19 |
-
|
20 |
-
</Link>
|
21 |
-
<Link href="/changelog" className="hover:text-foreground/80">
|
22 |
-
Changelog
|
23 |
</Link>
|
24 |
<Link href="/about" className="hover:text-foreground/80">
|
25 |
About
|
@@ -31,12 +63,11 @@ export function Navbar() {
|
|
31 |
<Search className="absolute left-2 top-2.5 size-4 text-muted-foreground" />
|
32 |
<Input placeholder="Search documentation..." className="pl-8" />
|
33 |
</div>
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
</Button>
|
40 |
</div>
|
41 |
</div>
|
42 |
</nav>
|
|
|
1 |
'use client';
|
2 |
+
|
3 |
+
import { Button, Input } from '@/components/base';
|
4 |
+
import { SignOutButton, useUser } from '@clerk/nextjs';
|
5 |
+
import { Search } from 'lucide-react';
|
6 |
import Link from 'next/link';
|
7 |
+
import { useMemo } from 'react';
|
8 |
|
9 |
export function Navbar() {
|
10 |
+
const { user, isSignedIn } = useUser();
|
11 |
+
|
12 |
+
const authContent = useMemo(() => {
|
13 |
+
if (isSignedIn) {
|
14 |
+
return (
|
15 |
+
<>
|
16 |
+
<p className="font-heading">
|
17 |
+
Hello,
|
18 |
+
{' '}
|
19 |
+
{user?.firstName}
|
20 |
+
</p>
|
21 |
+
<SignOutButton>
|
22 |
+
<Button variant="destructive" size="default">
|
23 |
+
Sign Out
|
24 |
+
</Button>
|
25 |
+
</SignOutButton>
|
26 |
+
</>
|
27 |
+
);
|
28 |
+
}
|
29 |
+
|
30 |
+
return (
|
31 |
+
<>
|
32 |
+
<Button variant="default" size="default">
|
33 |
+
<Link href="/sign-in">Sign In</Link>
|
34 |
+
</Button>
|
35 |
+
<Button variant="secondary" size="default">
|
36 |
+
<Link href="/sign-up">Sign Up</Link>
|
37 |
+
</Button>
|
38 |
+
</>
|
39 |
+
);
|
40 |
+
}, [isSignedIn, user]);
|
41 |
+
|
42 |
return (
|
43 |
<nav className="fixed top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
44 |
<div className="container flex h-14 items-center">
|
|
|
50 |
</div>
|
51 |
|
52 |
<div className="flex items-center gap-6 text-sm">
|
53 |
+
<Link href="/dashboard" className="hover:text-foreground/80">
|
54 |
+
Dashboard
|
|
|
|
|
|
|
55 |
</Link>
|
56 |
<Link href="/about" className="hover:text-foreground/80">
|
57 |
About
|
|
|
63 |
<Search className="absolute left-2 top-2.5 size-4 text-muted-foreground" />
|
64 |
<Input placeholder="Search documentation..." className="pl-8" />
|
65 |
</div>
|
66 |
+
|
67 |
+
<div className="flex items-center gap-4">
|
68 |
+
{authContent}
|
69 |
+
</div>
|
70 |
+
|
|
|
71 |
</div>
|
72 |
</div>
|
73 |
</nav>
|
src/components/{atoms β base}/Badge/index.tsx
RENAMED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import type * as React from 'react';
|
2 |
import { cn } from '@/utils/Helpers';
|
3 |
import { cva, type VariantProps } from 'class-variance-authority';
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
import type * as React from 'react';
|
4 |
import { cn } from '@/utils/Helpers';
|
5 |
import { cva, type VariantProps } from 'class-variance-authority';
|
src/components/{atoms β base}/Button/index.tsx
RENAMED
@@ -1,4 +1,5 @@
|
|
1 |
-
|
|
|
2 |
import { cn } from '@/utils/Helpers';
|
3 |
import { cva, type VariantProps } from 'class-variance-authority';
|
4 |
import { type ButtonHTMLAttributes, type FC, memo, type PropsWithChildren, type Ref } from 'react';
|
@@ -49,4 +50,5 @@ const Button: FC<ButtonProps> = memo(({
|
|
49 |
|
50 |
Button.displayName = 'Button';
|
51 |
|
|
|
52 |
export { Button, buttonVariants };
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
import { cn } from '@/utils/Helpers';
|
4 |
import { cva, type VariantProps } from 'class-variance-authority';
|
5 |
import { type ButtonHTMLAttributes, type FC, memo, type PropsWithChildren, type Ref } from 'react';
|
|
|
50 |
|
51 |
Button.displayName = 'Button';
|
52 |
|
53 |
+
// eslint-disable-next-line react-refresh/only-export-components
|
54 |
export { Button, buttonVariants };
|
src/components/{atoms β base}/Card/index.tsx
RENAMED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import { cn } from '@/utils/Helpers';
|
2 |
import * as React from 'react';
|
3 |
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
import { cn } from '@/utils/Helpers';
|
4 |
import * as React from 'react';
|
5 |
|
src/components/{atoms β base}/Input/index.tsx
RENAMED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import { cn } from '@/utils/Helpers';
|
2 |
import { type FC, type InputHTMLAttributes, memo, type Ref } from 'react';
|
3 |
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
import { cn } from '@/utils/Helpers';
|
4 |
import { type FC, type InputHTMLAttributes, memo, type Ref } from 'react';
|
5 |
|
src/components/base/Label/index.tsx
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { cn } from '@/utils/Helpers';
|
4 |
+
import * as React from 'react';
|
5 |
+
|
6 |
+
export type LabelProps = {} & React.LabelHTMLAttributes<HTMLLabelElement>;
|
7 |
+
|
8 |
+
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(({ className, ...props }, ref) => {
|
9 |
+
return (
|
10 |
+
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
11 |
+
<label
|
12 |
+
ref={ref}
|
13 |
+
className={cn(
|
14 |
+
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
15 |
+
className,
|
16 |
+
)}
|
17 |
+
{...props}
|
18 |
+
/>
|
19 |
+
);
|
20 |
+
});
|
21 |
+
Label.displayName = 'Label';
|
22 |
+
|
23 |
+
export { Label };
|
src/components/base/Select/Select.tsx
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import * as React from 'react';
|
4 |
+
import { useOnClickOutside } from 'usehooks-ts';
|
5 |
+
import { SelectContext } from './SelectContext';
|
6 |
+
|
7 |
+
export default function Select({
|
8 |
+
children,
|
9 |
+
value: controlledValue,
|
10 |
+
onChange,
|
11 |
+
defaultValue,
|
12 |
+
}: {
|
13 |
+
children: React.ReactNode;
|
14 |
+
value?: string;
|
15 |
+
onChange?: (value: string) => void;
|
16 |
+
defaultValue?: string;
|
17 |
+
}) {
|
18 |
+
const [open, setOpen] = React.useState(false);
|
19 |
+
const [internalValue, setInternalValue] = React.useState(defaultValue || '');
|
20 |
+
const ref = React.useRef<HTMLDivElement>(null);
|
21 |
+
|
22 |
+
const handleClickOutside = () => {
|
23 |
+
setOpen(false);
|
24 |
+
};
|
25 |
+
|
26 |
+
useOnClickOutside<HTMLDivElement>(ref as React.RefObject<HTMLDivElement>, handleClickOutside);
|
27 |
+
|
28 |
+
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
29 |
+
const handleChange = onChange || setInternalValue;
|
30 |
+
|
31 |
+
return (
|
32 |
+
// eslint-disable-next-line react/no-unstable-context-value
|
33 |
+
<SelectContext.Provider value={{ open, setOpen, value, onChange: handleChange }}>
|
34 |
+
<div className="relative" ref={ref}>{children}</div>
|
35 |
+
</SelectContext.Provider>
|
36 |
+
);
|
37 |
+
}
|
src/components/base/Select/SelectContent.tsx
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { cn } from '@/utils/Helpers';
|
4 |
+
import * as React from 'react';
|
5 |
+
import { SelectContext } from './SelectContext';
|
6 |
+
|
7 |
+
export default function SelectContent({ children, className }: { children: React.ReactNode; className?: string }) {
|
8 |
+
const context = React.useContext(SelectContext);
|
9 |
+
if (!context) {
|
10 |
+
throw new Error('SelectContent must be used within Select');
|
11 |
+
}
|
12 |
+
|
13 |
+
if (!context.open) {
|
14 |
+
return null;
|
15 |
+
}
|
16 |
+
|
17 |
+
return (
|
18 |
+
<div
|
19 |
+
className={cn(
|
20 |
+
'absolute top-full z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover text-popover-foreground shadow-md',
|
21 |
+
className,
|
22 |
+
)}
|
23 |
+
>
|
24 |
+
<div className="p-1">{children}</div>
|
25 |
+
</div>
|
26 |
+
);
|
27 |
+
}
|
src/components/base/Select/SelectContext.tsx
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import * as React from 'react';
|
4 |
+
|
5 |
+
export const SelectContext = React.createContext<SelectContextType | undefined>(undefined);
|
6 |
+
|
7 |
+
type SelectContextType = {
|
8 |
+
open: boolean;
|
9 |
+
setOpen: (open: boolean) => void;
|
10 |
+
value: string;
|
11 |
+
onChange: (value: string) => void;
|
12 |
+
};
|
src/components/base/Select/SelectItem.tsx
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { cn } from '@/utils/Helpers';
|
4 |
+
import * as React from 'react';
|
5 |
+
import { SelectContext } from './SelectContext';
|
6 |
+
|
7 |
+
export default function SelectItem({
|
8 |
+
children,
|
9 |
+
value,
|
10 |
+
className,
|
11 |
+
}: {
|
12 |
+
children: React.ReactNode;
|
13 |
+
value: string;
|
14 |
+
className?: string;
|
15 |
+
}) {
|
16 |
+
const context = React.useContext(SelectContext);
|
17 |
+
if (!context) {
|
18 |
+
throw new Error('SelectItem must be used within Select');
|
19 |
+
}
|
20 |
+
|
21 |
+
const isSelected = context.value === value;
|
22 |
+
|
23 |
+
return (
|
24 |
+
<button
|
25 |
+
type="button"
|
26 |
+
onClick={() => {
|
27 |
+
context.onChange(value);
|
28 |
+
context.setOpen(false);
|
29 |
+
}}
|
30 |
+
className={cn(
|
31 |
+
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground',
|
32 |
+
isSelected && 'bg-accent text-accent-foreground',
|
33 |
+
className,
|
34 |
+
)}
|
35 |
+
>
|
36 |
+
{children}
|
37 |
+
</button>
|
38 |
+
);
|
39 |
+
}
|
src/components/base/Select/SelectTrigger.tsx
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { cn } from '@/utils/Helpers';
|
4 |
+
import { ChevronDown } from 'lucide-react';
|
5 |
+
import * as React from 'react';
|
6 |
+
import { SelectContext } from './SelectContext';
|
7 |
+
|
8 |
+
export default function SelectTrigger({ children, className }: { children: React.ReactNode; className?: string }) {
|
9 |
+
const context = React.useContext(SelectContext);
|
10 |
+
if (!context) {
|
11 |
+
throw new Error('SelectTrigger must be used within Select');
|
12 |
+
}
|
13 |
+
|
14 |
+
return (
|
15 |
+
<button
|
16 |
+
type="button"
|
17 |
+
onClick={() => context.setOpen(!context.open)}
|
18 |
+
className={cn(
|
19 |
+
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
20 |
+
className,
|
21 |
+
)}
|
22 |
+
>
|
23 |
+
{children}
|
24 |
+
<ChevronDown className="size-4 opacity-50" />
|
25 |
+
</button>
|
26 |
+
);
|
27 |
+
}
|
src/components/base/Select/SelectValue.tsx
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import * as React from 'react';
|
4 |
+
import { SelectContext } from './SelectContext';
|
5 |
+
|
6 |
+
export default function SelectValue({ placeholder }: { placeholder?: string }) {
|
7 |
+
const context = React.useContext(SelectContext);
|
8 |
+
if (!context) {
|
9 |
+
throw new Error('SelectValue must be used within Select');
|
10 |
+
}
|
11 |
+
|
12 |
+
return <span>{context.value || placeholder}</span>;
|
13 |
+
}
|
src/components/base/Select/index.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export { default as Select } from './Select';
|
2 |
+
export { default as SelectContent } from './SelectContent';
|
3 |
+
export { default as SelectItem } from './SelectItem';
|
4 |
+
export { default as SelectTrigger } from './SelectTrigger';
|
5 |
+
export { default as SelectValue } from './SelectValue';
|
src/components/base/Switch/index.tsx
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { cn } from '@/utils/Helpers';
|
4 |
+
import * as React from 'react';
|
5 |
+
|
6 |
+
type SwitchProps = {
|
7 |
+
onCheckedChange?: (checked: boolean) => void;
|
8 |
+
} & React.InputHTMLAttributes<HTMLInputElement>;
|
9 |
+
|
10 |
+
const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(({ className, onCheckedChange, ...props }, ref) => {
|
11 |
+
return (
|
12 |
+
<label className="relative inline-flex cursor-pointer items-center">
|
13 |
+
<input
|
14 |
+
type="checkbox"
|
15 |
+
className="peer sr-only"
|
16 |
+
ref={ref}
|
17 |
+
onChange={e => onCheckedChange?.(e.target.checked)}
|
18 |
+
{...props}
|
19 |
+
/>
|
20 |
+
<div
|
21 |
+
className={cn(
|
22 |
+
'relative h-6 w-11 rounded-full bg-muted transition-colors peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-ring peer-focus:ring-offset-2 peer-checked:bg-primary',
|
23 |
+
className,
|
24 |
+
)}
|
25 |
+
>
|
26 |
+
<div className="absolute left-[2px] top-[2px] size-5 rounded-full bg-white transition-transform peer-checked:translate-x-5" />
|
27 |
+
</div>
|
28 |
+
</label>
|
29 |
+
);
|
30 |
+
});
|
31 |
+
Switch.displayName = 'Switch';
|
32 |
+
|
33 |
+
export { Switch };
|
src/components/{atoms β base}/index.ts
RENAMED
@@ -2,3 +2,6 @@ export { Badge } from './Badge';
|
|
2 |
export { Button } from './Button';
|
3 |
export * from './Card';
|
4 |
export { Input } from './Input';
|
|
|
|
|
|
|
|
2 |
export { Button } from './Button';
|
3 |
export * from './Card';
|
4 |
export { Input } from './Input';
|
5 |
+
export { Label } from './Label';
|
6 |
+
export * from './Select';
|
7 |
+
export { Switch } from './Switch';
|
src/components/dashboard/JobCard.tsx
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button, Card } from '@/components/base';
|
2 |
+
import { Clock, Globe } from 'lucide-react';
|
3 |
+
import Image from 'next/image';
|
4 |
+
|
5 |
+
type JobCardProps = {
|
6 |
+
job: {
|
7 |
+
id: number;
|
8 |
+
title: string;
|
9 |
+
company: string;
|
10 |
+
logo: string;
|
11 |
+
location: string;
|
12 |
+
type: string;
|
13 |
+
postedAt: string;
|
14 |
+
};
|
15 |
+
};
|
16 |
+
|
17 |
+
export default function JobCard({ job }: JobCardProps) {
|
18 |
+
return (
|
19 |
+
<Card className="rounded-lg border bg-card p-6">
|
20 |
+
<div className="flex items-start gap-4">
|
21 |
+
<div className="relative size-12 overflow-hidden rounded-full border">
|
22 |
+
<Image src={job.logo || '/placeholder.svg'} alt={`${job.company} logo`} fill className="object-cover" />
|
23 |
+
</div>
|
24 |
+
<div className="flex-1">
|
25 |
+
<h3 className="text-lg font-semibold">{job.title}</h3>
|
26 |
+
<p className="text-muted-foreground">{job.company}</p>
|
27 |
+
<div className="mt-2 flex items-center gap-4 text-sm text-muted-foreground">
|
28 |
+
<div className="flex items-center gap-1">
|
29 |
+
<Globe className="size-4" />
|
30 |
+
{job.location}
|
31 |
+
</div>
|
32 |
+
<div className="flex items-center gap-1">
|
33 |
+
<Clock className="size-4" />
|
34 |
+
{job.type}
|
35 |
+
</div>
|
36 |
+
</div>
|
37 |
+
</div>
|
38 |
+
<div className="flex flex-col gap-2">
|
39 |
+
<Button variant="outline">View</Button>
|
40 |
+
<Button>Apply</Button>
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
</Card>
|
44 |
+
);
|
45 |
+
}
|
src/components/dashboard/JobFilters.tsx
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch } from '@/components/base';
|
4 |
+
import { Search } from 'lucide-react';
|
5 |
+
|
6 |
+
export default function JobFilters() {
|
7 |
+
return (
|
8 |
+
<div className="rounded-lg bg-card p-4">
|
9 |
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
10 |
+
<Select>
|
11 |
+
<SelectTrigger>
|
12 |
+
<SelectValue placeholder="Location" />
|
13 |
+
</SelectTrigger>
|
14 |
+
<SelectContent>
|
15 |
+
<SelectItem value="anywhere">Anywhere</SelectItem>
|
16 |
+
<SelectItem value="vietnam">Vietnam</SelectItem>
|
17 |
+
<SelectItem value="usa">USA</SelectItem>
|
18 |
+
</SelectContent>
|
19 |
+
</Select>
|
20 |
+
|
21 |
+
<Select>
|
22 |
+
<SelectTrigger>
|
23 |
+
<SelectValue placeholder="Job type" />
|
24 |
+
</SelectTrigger>
|
25 |
+
<SelectContent>
|
26 |
+
<SelectItem value="fulltime">Full-time</SelectItem>
|
27 |
+
<SelectItem value="freelancer">Freelancer</SelectItem>
|
28 |
+
<SelectItem value="contract">Contract</SelectItem>
|
29 |
+
</SelectContent>
|
30 |
+
</Select>
|
31 |
+
|
32 |
+
<Select>
|
33 |
+
<SelectTrigger>
|
34 |
+
<SelectValue placeholder="Experience" />
|
35 |
+
</SelectTrigger>
|
36 |
+
<SelectContent>
|
37 |
+
<SelectItem value="entry">Entry Level</SelectItem>
|
38 |
+
<SelectItem value="mid">Mid Level</SelectItem>
|
39 |
+
<SelectItem value="senior">Senior Level</SelectItem>
|
40 |
+
</SelectContent>
|
41 |
+
</Select>
|
42 |
+
|
43 |
+
<div className="relative">
|
44 |
+
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
45 |
+
<Input className="pl-9" placeholder="E.g. Front-end developer" type="search" />
|
46 |
+
</div>
|
47 |
+
</div>
|
48 |
+
|
49 |
+
<div className="mt-4 flex items-center justify-end space-x-2">
|
50 |
+
<Switch id="closed-jobs" />
|
51 |
+
<Label htmlFor="closed-jobs">Hide closed jobs</Label>
|
52 |
+
</div>
|
53 |
+
</div>
|
54 |
+
);
|
55 |
+
}
|
src/components/dashboard/SubscribeJob.tsx
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { Button, Input } from '@/components/base';
|
4 |
+
import { Send } from 'lucide-react';
|
5 |
+
|
6 |
+
export default function SubscribeJob() {
|
7 |
+
return (
|
8 |
+
<div className="rounded-lg border bg-card p-6">
|
9 |
+
<div className="mb-6">
|
10 |
+
<div className="mb-4 flex size-10 items-center justify-center rounded-full bg-primary/10">
|
11 |
+
<Send className="size-5 text-primary" />
|
12 |
+
</div>
|
13 |
+
<h2 className="mb-2 text-xl font-semibold">New remote jobs in your inbox, every Monday!</h2>
|
14 |
+
<p className="text-muted-foreground">Subscribe to get your 5-minute brief on tech remote jobs every Monday</p>
|
15 |
+
</div>
|
16 |
+
<form className="space-y-4">
|
17 |
+
<Input type="email" placeholder="Enter your email" />
|
18 |
+
<Button className="w-full">Subscribe</Button>
|
19 |
+
</form>
|
20 |
+
</div>
|
21 |
+
);
|
22 |
+
}
|
src/components/homepage/AnnouncementBanner.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { Badge, Button } from '@/components/
|
2 |
import { ChevronDown, Download } from 'lucide-react';
|
3 |
import Image from 'next/image';
|
4 |
|
|
|
1 |
+
import { Badge, Button } from '@/components/base';
|
2 |
import { ChevronDown, Download } from 'lucide-react';
|
3 |
import Image from 'next/image';
|
4 |
|
src/components/homepage/CustomizationSection.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { Badge, Card } from '@/components/
|
2 |
import Image from 'next/image';
|
3 |
|
4 |
export function CustomizationSection() {
|
|
|
1 |
+
import { Badge, Card } from '@/components/base';
|
2 |
import Image from 'next/image';
|
3 |
|
4 |
export function CustomizationSection() {
|
src/components/homepage/FeatureSection.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { Badge } from '@/components/
|
2 |
import { Cloud, FileText, Grid, MessageCircle, Server } from 'lucide-react';
|
3 |
import Image from 'next/image';
|
4 |
|
|
|
1 |
+
import { Badge } from '@/components/base';
|
2 |
import { Cloud, FileText, Grid, MessageCircle, Server } from 'lucide-react';
|
3 |
import Image from 'next/image';
|
4 |
|