Spaces:
Running
Running
Nguyen Thanh Hoang
Hoang Nguyen
commited on
feat(dashoard): implement page dashboard (#4)
Browse filesCo-authored-by: Hoang Nguyen <hoangnt@inspirelab.vn>
- package-lock.json +15 -1
- package.json +1 -0
- src/app/dashboard/page.tsx +76 -0
- src/app/page.tsx +0 -46
- src/components/HeroSection.tsx +1 -1
- src/components/Navbar.tsx +13 -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
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/page.tsx
CHANGED
@@ -6,52 +6,6 @@ export default function Home() {
|
|
6 |
return (
|
7 |
<main className="flex min-h-screen flex-col">
|
8 |
<div className="flex-1 space-y-16 pb-8 pt-20 md:pb-12 md:pt-24 lg:py-32">
|
9 |
-
<div className="container flex flex-col items-center gap-4 text-center">
|
10 |
-
<Badge className="rounded-lg" variant="secondary">
|
11 |
-
NEW
|
12 |
-
{' '}
|
13 |
-
<span className="mx-1">+</span>
|
14 |
-
{' '}
|
15 |
-
v0.5.14 is now live on GitHub. Check it out!
|
16 |
-
</Badge>
|
17 |
-
|
18 |
-
<span className="text-4xl">❝</span>
|
19 |
-
|
20 |
-
<h1 className="font-heading text-3xl sm:text-5xl md:text-6xl lg:text-7xl">
|
21 |
-
Chat with AI
|
22 |
-
<br />
|
23 |
-
without privacy concerns
|
24 |
-
</h1>
|
25 |
-
|
26 |
-
<p className="max-w-2xl leading-normal text-muted-foreground sm:text-xl sm:leading-8">
|
27 |
-
Jan is an open source ChatGPT-alternative that runs 100% offline.
|
28 |
-
</p>
|
29 |
-
|
30 |
-
<div className="space-y-4">
|
31 |
-
<Button className="h-11 px-8" size="lg">
|
32 |
-
<Download className="mr-2 size-4" />
|
33 |
-
Download for Windows
|
34 |
-
<ChevronDown className="ml-2 size-4" />
|
35 |
-
</Button>
|
36 |
-
<p className="text-xs text-muted-foreground">
|
37 |
-
<span className="font-semibold text-yellow-500">2.5M+</span>
|
38 |
-
{' '}
|
39 |
-
downloads | Free & Open Source
|
40 |
-
</p>
|
41 |
-
</div>
|
42 |
-
</div>
|
43 |
-
|
44 |
-
<div className="container">
|
45 |
-
<div className="relative mx-auto aspect-video max-w-5xl overflow-hidden rounded-xl border bg-background shadow-xl">
|
46 |
-
<Image
|
47 |
-
src="https://sjc.microlink.io/-ax0tIqUfnMYpO1Y6sFNuRcGN_Oe6cQwpzrnQR5q5pzkpfA29UGKZ228lDnpeQCpNANORBcNBmQgFoOtLn18vw.jpeg"
|
48 |
-
alt="Jan AI Interface"
|
49 |
-
fill
|
50 |
-
className="object-cover"
|
51 |
-
priority
|
52 |
-
/>
|
53 |
-
</div>
|
54 |
-
</div>
|
55 |
<AnnouncementBanner />
|
56 |
<FeaturesSection />
|
57 |
<CustomizationSection />
|
|
|
6 |
return (
|
7 |
<main className="flex min-h-screen flex-col">
|
8 |
<div className="flex-1 space-y-16 pb-8 pt-20 md:pb-12 md:pt-24 lg:py-32">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
<AnnouncementBanner />
|
10 |
<FeaturesSection />
|
11 |
<CustomizationSection />
|
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,6 +1,6 @@
|
|
1 |
'use client';
|
2 |
-
import { Button, Input } from '@/components/
|
3 |
-
import { Search
|
4 |
import Link from 'next/link';
|
5 |
|
6 |
export function Navbar() {
|
@@ -15,11 +15,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 +28,15 @@ 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 |
-
|
|
|
|
|
|
|
40 |
</div>
|
41 |
</div>
|
42 |
</nav>
|
|
|
1 |
'use client';
|
2 |
+
import { Button, Input } from '@/components/base';
|
3 |
+
import { Search } from 'lucide-react';
|
4 |
import Link from 'next/link';
|
5 |
|
6 |
export function Navbar() {
|
|
|
15 |
</div>
|
16 |
|
17 |
<div className="flex items-center gap-6 text-sm">
|
18 |
+
<Link href="/dashboard" className="hover:text-foreground/80">
|
19 |
+
Dashboard
|
|
|
|
|
|
|
20 |
</Link>
|
21 |
<Link href="/about" className="hover:text-foreground/80">
|
22 |
About
|
|
|
28 |
<Search className="absolute left-2 top-2.5 size-4 text-muted-foreground" />
|
29 |
<Input placeholder="Search documentation..." className="pl-8" />
|
30 |
</div>
|
31 |
+
<div className="flex gap-4">
|
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 |
+
</div>
|
39 |
+
|
40 |
</div>
|
41 |
</div>
|
42 |
</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 |
|