bradduy commited on
Commit
43b111c
Β·
2 Parent(s): 06c4f9e 6efad45

Merge remote-tracking branch 'upstream/main'

Browse files
CHANGELOG.md CHANGED
@@ -1,16 +1,17 @@
1
- # [1.1.0](https://github.com/bradduy/edtech/compare/v1.0.0...v1.1.0) (2025-02-12)
2
 
3
 
4
  ### Features
5
 
6
- * **login:** init form signin + signup ([a9ff6a3](https://github.com/bradduy/edtech/commit/a9ff6a3b06ce5f3417617cb5805287b0839ecfc9))
7
 
8
- # 1.0.0 (2025-01-31)
9
 
10
 
11
  ### Features
12
 
13
- * **homepage:** implement banner section ([d04e372](https://github.com/bradduy/edtech/commit/d04e372a875355ae2b794b26318788cec1c065f3))
 
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="en" suppressHydrationWarning>
30
  <body className={`${inter.variable} ${outfit.variable} font-sans antialiased`} suppressHydrationWarning={isDevMode}>
31
- <Navbar />
32
- {children}
 
 
 
 
 
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/atoms';
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
- import { Button, Input } from '@/components/atoms';
3
- import { Search, Settings } from 'lucide-react';
 
 
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="/documentation" className="hover:text-foreground/80">
19
- Documentation
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
- <Link href="/blog" className="hover:text-foreground/80">
35
- Blog
36
- </Link>
37
- <Button variant="ghost" size="icon">
38
- <Settings className="size-5" />
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
- /* eslint-disable react-refresh/only-export-components */
 
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/atoms';
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/atoms';
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/atoms';
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