fix: updated readme and contribution guide
Signed-off-by: Varun Raj <mailme@varunraj.in>
@@ -1,2 +1,2 @@
|
||||
IMMICH_URL=""
|
||||
NEXT_PUBLIC_IMMICH_URL=""
|
||||
IMMICH_API_KEY=""
|
||||
@@ -0,0 +1,31 @@
|
||||
# Contributing to Immich Power Tools
|
||||
|
||||
The code is pretty straight forward and easy to understand. Please just make sure you follow the below guidelines so that its easy for me to review and merge your PR.
|
||||
|
||||
|
||||
### Structure
|
||||
|
||||
- `components` - All the UI components
|
||||
- `config` - All the configuration files and constants
|
||||
- `config/routes.ts` - All the routes for the application
|
||||
- `helpers` - All the helper functions for both data and modules
|
||||
- `lib` - All the utility functions
|
||||
- `pages` - All the pages based on [Next.js](https://nextjs.org/)'s page based routing
|
||||
- `styles` - All the global styles (We don't touch this much as we are using Tailwind CSS)
|
||||
- `types` - All the typescript types and interfaces (Make sure you create `.d.ts` files for each module)
|
||||
|
||||
### API Infrastructure
|
||||
|
||||
Since Immich API needs to send auth token in the headers, I've created a proxy api that matches the Immich's APIs path to forward the request to Immich's API with it's token (Which is stored in the `.env` file). This is done to avoid CORS issues and Security issues.
|
||||
|
||||
You can find the API route here `pages/api/immich-proxy/[...path].ts`
|
||||
|
||||

|
||||
|
||||
|
||||
### Thubmnails
|
||||
|
||||
Since the `thumbnailPath` of Immich API doesnt give us a direct link to the image, I've created a proxy api that fetches the image and sends it back as a response.
|
||||
|
||||
**For Person's Thumbnail**
|
||||
You can find the API route here `pages/api/immich-proxy/thumbnail/[id].ts`
|
||||
@@ -1,17 +1,54 @@
|
||||
# Immich Power Tools
|
||||
#  Immich Power Tools
|
||||
|
||||
A unofficial immich client to provide better tools to organize and manage your immich account.
|
||||
A unofficial immich client to provide better tools to organize and manage your immich account. Building it to speed up your workflows in Immich to organize your people and albums.
|
||||
|
||||
## Getting Started
|
||||
<div style="position: relative; padding-bottom: 56.25%; height: 0;"><iframe src="https://www.loom.com/embed/13aa90d8ab2e4acab0993bdc8703a750?sid=5b6249ac-d318-44e1-a3e2-6ed3c7a1d38a" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe></div>
|
||||
|
||||
First, run the development server:
|
||||
### Back story
|
||||
|
||||
Recently I've migrated my entire Google photos library to Immich, I was able to successfully migrate all my assets along with its albums to immich. But there were few things like people match that was lacking. I loved Immich UI on the whole but for organizing content I felt its quite restricted and I had to do a lot of things in bulk instead of opening each asset and doing it. Hence I built this tool (continuing to itereate) to make my life and any other Immich user's life easier.
|
||||
|
||||
### Getting Started
|
||||
|
||||
> [!NOTE]
|
||||
> Please use `bun` to run the project. If you don't have `bun` installed, checkout [bun.sh](https://bun.sh/) for installation.
|
||||
|
||||
Copy paste the `.env.example` to `.env` and fill in the values.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
NEXT_PUBLIC_IMMICH_URL="" # Immich API URL
|
||||
IMMICH_API_KEY="" # Immich API Key
|
||||
```
|
||||
|
||||
Refer here for obtaining Immich API Key: https://immich.app/docs/features/command-line-interface#obtain-the-api-key
|
||||
|
||||
Run the development server:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
**Immich Related**
|
||||
|
||||
- [x] Manage People
|
||||
- [ ] Manage Albums
|
||||
- [ ] Statistics
|
||||
|
||||
**Tech Related**
|
||||
|
||||
- [ ] Dark Mode
|
||||
- [ ] Dockerize
|
||||
- [ ] Authentication
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- [Next.js](https://nextjs.org/) for infrastructure
|
||||
- [Tailwind CSS](https://tailwindcss.com/) for styling
|
||||
- [Shadcn](https://shadcn.com/) for UI Components
|
||||
- [Axios](https://axios-http.com/) for API calls
|
||||
|
||||
### Contributing
|
||||
|
||||
Feel free to contribute to this project, I'm open to any suggestions and improvements. Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||
@@ -11,12 +11,14 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@tanstack/react-table": "^8.20.1",
|
||||
"@types/qs": "^6.9.15",
|
||||
"axios": "^1.7.4",
|
||||
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 460 B |
|
After Width: | Height: | Size: 714 B |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
Before Width: | Height: | Size: 629 B |
|
After Width: | Height: | Size: 830 KiB |
|
After Width: | Height: | Size: 432 KiB |
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import Header from "../shared/Header";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode[] | React.ReactNode;
|
||||
}
|
||||
export default function PageLayout({ children }: IProps) {
|
||||
const header = Array.isArray(children)
|
||||
? children.find((child) => React.isValidElement(child) && child.type === Header)
|
||||
: null;
|
||||
|
||||
const childrenWithoutHeader = Array.isArray(children)
|
||||
? children.filter((child) => {
|
||||
return React.isValidElement(child) && child.type !== Header;
|
||||
})
|
||||
: children;
|
||||
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
<main className={
|
||||
cn("flex flex-1 max-h-[calc(100vh-60px)] flex-col gap-4 p-4 lg:gap-6 lg:p-6 overflow-y-auto", {
|
||||
'mb-14': !!header,
|
||||
})
|
||||
}>
|
||||
{childrenWithoutHeader}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import Sidebar from "../shared/Sidebar"
|
||||
|
||||
|
||||
type RootLayoutProps = {
|
||||
@@ -8,10 +9,11 @@ type RootLayoutProps = {
|
||||
|
||||
export default function RootLayout({ children }: RootLayoutProps) {
|
||||
return (
|
||||
<div className={cn(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
)}>
|
||||
{children}
|
||||
<div className="grid max-h-screen min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
|
||||
<Sidebar />
|
||||
<div className="flex flex-col">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export default function PeopleList() {
|
||||
const router = useRouter()
|
||||
const { page = 1 } = router.query as { page: string }
|
||||
const [people, setPeople] = useState<IPerson[]>([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
@@ -62,6 +63,7 @@ export default function PeopleList() {
|
||||
})
|
||||
.then((response) => {
|
||||
setPeople(response.people);
|
||||
setCount(response.total);
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorMessage(error.message);
|
||||
@@ -76,25 +78,15 @@ export default function PeopleList() {
|
||||
fetchData();
|
||||
}, [page]);
|
||||
|
||||
const renderContent = () => {
|
||||
|
||||
|
||||
if (loading) return <Loader />;
|
||||
if (errorMessage) return <div>{errorMessage}</div>;
|
||||
return (
|
||||
<div className="grid grid-cols-8 gap-4 p-2">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4 p-2">
|
||||
{people.map((person) => (
|
||||
<PersonItem person={person} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{renderContent()}
|
||||
<div className="fixed bottom-0 w-full bg-gray-200 py-2 flex justify-center">
|
||||
<PeoplePagination />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import PersonHideCell from "./PersonHideCell";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { ENV } from "@/config/environment";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
interface IProps {
|
||||
person: IPerson;
|
||||
@@ -31,37 +32,62 @@ export default function PersonItem({ person }: IProps) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleHide = (hidden: boolean) => {
|
||||
setLoading(true);
|
||||
return updatePerson(person.id, {
|
||||
isHidden: hidden,
|
||||
})
|
||||
.then(() => {
|
||||
setFormData((person) => ({
|
||||
...person,
|
||||
isHidden: hidden,
|
||||
}));
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("flex flex-col rounded-lg pb-2 border border-2 border-transparent items-center gap-2", {
|
||||
"opacity-50": formData.isHidden,
|
||||
"border border-blue-500": formData.name,
|
||||
})}
|
||||
className={clsx(
|
||||
"flex flex-col rounded-lg pb-2 border border-2 border-transparent items-center gap-2",
|
||||
{
|
||||
"opacity-50": formData.isHidden,
|
||||
"border border-blue-500": formData.name,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full h-auto">
|
||||
<Avatar
|
||||
className="w-full h-auto rounded-lg"
|
||||
src={person.thumbnailPath}
|
||||
alt={person.name}
|
||||
/>
|
||||
<div className="absolute top-2 left-2 ">
|
||||
<Link className="bg-green-300 rounded-lg px-2 py-1 text-sm" href={`${ENV.IMMICH_URL}/people/${person.id}`} target="_blank">
|
||||
View
|
||||
</Link>
|
||||
</div>
|
||||
<div className="absolute top-2 right-2 ">
|
||||
<PersonHideCell
|
||||
person={person}
|
||||
onUpdate={(newData) => {
|
||||
setFormData(newData);
|
||||
}}
|
||||
<div className="relative w-full h-auto group">
|
||||
<div
|
||||
onClick={() => {
|
||||
handleHide(!formData.isHidden);
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
className="w-full h-auto rounded-lg"
|
||||
src={person.thumbnailPath}
|
||||
alt={person.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-2 left-2 group-hover:block hidden">
|
||||
<Link
|
||||
className="bg-green-300 block rounded-lg px-2 py-1 text-sm"
|
||||
href={`${ENV.IMMICH_URL}/people/${person.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<ArrowUpRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="absolute top-2 right-2 ">
|
||||
<PersonMergeDropdown person={person} />
|
||||
</div>
|
||||
</div>
|
||||
{!editMode ? (
|
||||
<h2
|
||||
className="text-lg text-center font-semibold"
|
||||
className="text-lg text-center font-semibold hover:bg-gray-300 w-full px-2 py-1 rounded-lg"
|
||||
onClick={() => {
|
||||
setEditMode((prev) => !prev);
|
||||
}}
|
||||
@@ -75,8 +101,9 @@ export default function PersonItem({ person }: IProps) {
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="text-lg font-semibold text-center"
|
||||
className="text-lg font-semibold text-center w-full px-2 py-1 rounded-lg"
|
||||
defaultValue={formData.name}
|
||||
placeholder="Enter name"
|
||||
autoFocus
|
||||
onChange={(e) => {
|
||||
setFormData({ ...formData, name: e.target.value });
|
||||
@@ -90,8 +117,9 @@ export default function PersonItem({ person }: IProps) {
|
||||
onBlur={handleEdit}
|
||||
/>
|
||||
)}
|
||||
<PersonMergeDropdown person={person} />
|
||||
<PersonBirthdayCell person={person} />
|
||||
<div className="flex flex-col gap-2">
|
||||
<PersonBirthdayCell person={person} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,12 +95,11 @@ export function PersonMergeDropdown(
|
||||
<div className="flex items-center space-x-4">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="justify-start flex gap-2">
|
||||
<Merge size={14} />
|
||||
<span className="text-xs">Merge</span>
|
||||
<Button variant="outline" className="!py-0.5 !px-2 text-xs h-7">
|
||||
Merge
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" side="right" align="start">
|
||||
<PopoverContent className="p-0" side="bottom" align="center">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Change status..."
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Home,
|
||||
LineChart,
|
||||
Menu,
|
||||
Package,
|
||||
Package2,
|
||||
ShoppingCart,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import Head from "next/head";
|
||||
|
||||
interface IProps {
|
||||
leftComponent?: React.ReactNode | string;
|
||||
rightComponent?: React.ReactNode | string;
|
||||
}
|
||||
export default function Header({ leftComponent, rightComponent }: IProps) {
|
||||
const renderLeftComponent = () => {
|
||||
if (typeof leftComponent === "string") {
|
||||
return <p className="font-semibold">{leftComponent}</p>;
|
||||
}
|
||||
return leftComponent;
|
||||
};
|
||||
|
||||
const renderRightComponent = () => {
|
||||
if (typeof rightComponent === "string") {
|
||||
return <p>{rightComponent}</p>;
|
||||
}
|
||||
return rightComponent;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{typeof leftComponent === "string" && (
|
||||
<Head>
|
||||
<title>{leftComponent} - Immich Power Tools</title>
|
||||
</Head>
|
||||
)}
|
||||
<header key="header" className="sticky z-10 top-0 w-full flex h-14 items-center gap-4 border-b bg-muted px-4 lg:h-[60px] lg:px-6">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="shrink-0 md:hidden">
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="flex flex-col">
|
||||
<nav className="grid gap-2 text-lg font-medium">
|
||||
<Link
|
||||
href="#"
|
||||
className="flex items-center gap-2 text-lg font-semibold"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl bg-muted px-3 py-2 text-foreground hover:text-foreground"
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
Orders
|
||||
<Badge className="ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded-full">
|
||||
6
|
||||
</Badge>
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Package className="h-5 w-5" />
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Users className="h-5 w-5" />
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<LineChart className="h-5 w-5" />
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="mt-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Upgrade to Pro</CardTitle>
|
||||
<CardDescription>
|
||||
Unlock all features and get unlimited access to our support
|
||||
team.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button size="sm" className="w-full">
|
||||
Upgrade
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="w-full flex-1">
|
||||
{renderLeftComponent()}
|
||||
</div>
|
||||
{renderRightComponent()}
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import Link from "next/link";
|
||||
import { Bell, Camera, Package2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { sidebarNavs } from "@/config/constants/sidebarNavs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useRouter } from "next/router";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { ENV } from "@/config/environment";
|
||||
|
||||
export default function Sidebar() {
|
||||
const router = useRouter();
|
||||
const { pathname } = router;
|
||||
|
||||
return (
|
||||
<div className="hidden border-r bg-muted/40 md:block max-h-screen min-h-screen">
|
||||
<div className="flex h-full max-h-screen flex-col gap-2">
|
||||
<div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
|
||||
<Link href="/" className="flex items-center gap-2 font-semibold">
|
||||
<img
|
||||
src="/favicon.png"
|
||||
alt="Immich Power Tools"
|
||||
className="w-8 h-8"
|
||||
/>
|
||||
<span className="">Immich Power Tools</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<nav className="grid items-start px-2 text-sm font-medium lg:px-4">
|
||||
{sidebarNavs.map((nav) => (
|
||||
<Link
|
||||
key={nav.title}
|
||||
href={nav.link}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary",
|
||||
{
|
||||
"text-primary": pathname === nav.link,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{nav.icon}
|
||||
{nav.title}
|
||||
{nav.badge && (
|
||||
<Badge variant="outline" className="rounded-full text-nowrap">
|
||||
{nav.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-col gap-1 text-xs text-center py-2">
|
||||
<p className="text-gray-800 font-mono">Connected to</p>
|
||||
<Link href={ENV.IMMICH_URL} target="_blank" className="font-mono">
|
||||
{ENV.IMMICH_URL}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="border text-gray-700 text-xs text-center py-2">
|
||||
|
||||
<p>
|
||||
Made with <span className="text-red-500">♥</span> by <Link target="_blank" href="https://x.com/zathvarun" className="text-primary">@zathvarun</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
@@ -0,0 +1,203 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
DotFilledIcon,
|
||||
} from "@radix-ui/react-icons"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<DotFilledIcon className="h-4 w-4 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,138 @@
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const TooltipRoot = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
|
||||
const Tooltip: React.FC<React.ComponentProps<typeof TooltipRoot>> = ({
|
||||
children,
|
||||
...props
|
||||
}) => (
|
||||
<TooltipProvider>
|
||||
<TooltipRoot {...props}>
|
||||
{children}
|
||||
<TooltipContent />
|
||||
</TooltipRoot>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
export { Tooltip, TooltipRoot, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Home, User } from "lucide-react";
|
||||
|
||||
|
||||
export const sidebarNavs = [
|
||||
{
|
||||
title: "Manage People",
|
||||
link: "/",
|
||||
icon: <User className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Statistics",
|
||||
link: "/statistics",
|
||||
icon: <Home className="h-4 w-4" />,
|
||||
badge: "Comming Soon"
|
||||
},
|
||||
{
|
||||
title: "Albums",
|
||||
link: "/albums",
|
||||
icon: <Home className="h-4 w-4" />,
|
||||
badge: "Comming Soon"
|
||||
},
|
||||
];
|
||||
@@ -1,10 +1,21 @@
|
||||
import { Inter } from "next/font/google";
|
||||
import PeopleList from "@/components/people/PeopleList";
|
||||
import PageLayout from "@/components/layouts/PageLayout";
|
||||
import Header from "@/components/shared/Header";
|
||||
import { PeoplePagination } from "@/components/people/PeoplePagination";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<PeopleList />
|
||||
<PageLayout>
|
||||
<Header
|
||||
leftComponent="Manage People"
|
||||
rightComponent={
|
||||
<PeoplePagination />
|
||||
}
|
||||
/>
|
||||
<PeopleList />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@ export interface IPerson {
|
||||
|
||||
interface IPeopleListResponse extends IListData{
|
||||
people: IPerson[]
|
||||
total: number
|
||||
}
|
||||
@@ -12,7 +12,7 @@ const config = {
|
||||
prefix: "",
|
||||
theme: {
|
||||
fontFamily: {
|
||||
|
||||
mono: [ ...fontFamily.mono ],
|
||||
},
|
||||
container: {
|
||||
center: true,
|
||||
|
||||