fix: updated readme and contribution guide

Signed-off-by: Varun Raj <mailme@varunraj.in>
This commit is contained in:
Varun Raj
2024-08-18 11:08:15 +05:30
parent 9a42894341
commit d79f41475f
34 changed files with 936 additions and 61 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
IMMICH_URL=""
NEXT_PUBLIC_IMMICH_URL=""
IMMICH_API_KEY=""
+31
View File
@@ -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`
![Immich Proxy API](./screenshots/screenshot-infra.jpg)
### 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`
+48 -11
View File
@@ -1,17 +1,54 @@
# Immich Power Tools
# ![Logo](./public/favicon-32x32.png) 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.
BIN
View File
Binary file not shown.
+2
View File
@@ -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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

-1
View File
@@ -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

+1
View File
@@ -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
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

+31
View File
@@ -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>
</>
);
}
+6 -4
View File
@@ -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>
)
}
+4 -12
View File
@@ -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>
);
}
+53 -25
View File
@@ -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..."
+131
View File
@@ -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>
</>
);
}
+70
View File
@@ -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">&hearts;</span> by <Link target="_blank" href="https://x.com/zathvarun" className="text-primary">@zathvarun</Link>
</p>
</div>
</div>
</div>
</div>
);
}
+36
View File
@@ -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 }
+76
View File
@@ -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 }
+203
View File
@@ -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,
}
+25
View File
@@ -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 }
+138
View File
@@ -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,
}
+41
View File
@@ -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 }
+22
View File
@@ -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"
},
];
+12 -1
View File
@@ -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>
);
}
+1
View File
@@ -10,4 +10,5 @@ export interface IPerson {
interface IPeopleListResponse extends IListData{
people: IPerson[]
total: number
}
+1 -1
View File
@@ -12,7 +12,7 @@ const config = {
prefix: "",
theme: {
fontFamily: {
mono: [ ...fontFamily.mono ],
},
container: {
center: true,