diff --git a/src/components/File/FileModal.tsx b/src/components/File/FileModal.tsx
index bf4c5ad6..debb4f11 100644
--- a/src/components/File/FileModal.tsx
+++ b/src/components/File/FileModal.tsx
@@ -47,6 +47,7 @@ import { FileMeta } from '.';
import Type from '../Type';
import Tag from 'components/File/tag/Tag';
import Item from 'components/File/tag/Item';
+import { useDeleteFileTags, useFileTags, useTags, useUpdateFileTags } from 'lib/queries/tags';
export default function FileModal({
open,
@@ -70,15 +71,14 @@ export default function FileModal({
const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite();
const folders = useFolders();
-
- const [overrideRender, setOverrideRender] = useState(false);
+ const tags = useFileTags(file.id);
+ const updateTags = useUpdateFileTags(file.id);
+ const removeTags = useDeleteFileTags(file.id);
const clipboard = useClipboard();
- const [tags, setTags] = useState<{ label: string; value: string; color: string }[]>([
- { label: 'Tag 1', value: 'tag-1', color: '#ff0000' },
- { label: 'Tag 2', value: 'tag-2', color: '#00ff00' },
- { label: 'Tag 3', value: 'tag-3', color: '#0000ff' },
- ]);
+ const allTags = useTags();
+
+ const [overrideRender, setOverrideRender] = useState(false);
const handleDelete = async () => {
deleteFile.mutate(file.id, {
@@ -227,6 +227,40 @@ export default function FileModal({
console.log('should save');
};
+ const handleAddTags = (t: string[]) => {
+ // filter out existing tags from t
+ t = t.filter((tag) => !tags.data.find((t) => t.id === tag));
+
+ const fullTag = allTags.data.find((tag) => tag.id === t[0]);
+
+ if (!fullTag) return;
+
+ updateTags.mutate([...tags.data, fullTag], {
+ onSuccess: () => {
+ showNotification({
+ title: 'Added tag',
+ message: fullTag.name,
+ color: 'green',
+ icon: ,
+ });
+ },
+ });
+ };
+
+ const handleRemoveTags = (t: string[]) => {
+ const fullTag = allTags.data.find((tag) => tag.id === t[0]);
+
+ removeTags.mutate(t, {
+ onSuccess: () =>
+ showNotification({
+ title: 'Removed tag',
+ message: fullTag.name,
+ color: 'green',
+ icon: ,
+ }),
+ });
+ };
+
return (
}>Tags
t.id) ?? []}
+ data={allTags.data?.map((t) => ({ value: t.id, label: t.name, color: t.color })) ?? []}
+ placeholder={allTags.data?.length ? 'Add tags' : 'Add tags (optional)'}
icon={}
valueComponent={Tag}
itemComponent={Item}
@@ -316,9 +351,11 @@ export default function FileModal({
)}
+ // onChange={(t) => (t.length === 1 ? handleRemoveTags(t) : handleAddTags(t))}
+ onChange={(t) => console.log(t)}
onCreate={(t) => {
const item = { value: t, label: t, color: colorHash(t) };
- setTags([...tags, item]);
+ // setLabelTags([...labelTags, item]);
return item;
}}
onBlur={handleTagsSave}
diff --git a/src/components/pages/Files/TagsModal.tsx b/src/components/pages/Files/TagsModal.tsx
new file mode 100644
index 00000000..58745865
--- /dev/null
+++ b/src/components/pages/Files/TagsModal.tsx
@@ -0,0 +1,197 @@
+import {
+ ActionIcon,
+ Button,
+ ColorInput,
+ Group,
+ Modal,
+ Paper,
+ Stack,
+ Text,
+ TextInput,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useDeleteTags, useTags } from 'lib/queries/tags';
+import { showNotification } from '@mantine/notifications';
+import { IconRefresh, IconTag, IconTags, IconTagsOff } from '@tabler/icons-react';
+import { useState } from 'react';
+import { colorHash } from 'utils/client';
+import useFetch from 'hooks/useFetch';
+import { useModals } from '@mantine/modals';
+import MutedText from 'components/MutedText';
+
+export function TagCard({ tags, tag }) {
+ const deleteTags = useDeleteTags();
+ const modals = useModals();
+
+ const deleteTag = () => {
+ modals.openConfirmModal({
+ zIndex: 1000,
+ size: 'auto',
+ title: (
+
+ Delete tag {tag.name}?
+
+ ),
+ children: `This will remove the tag from ${tag.files.length} file${tag.files.length === 1 ? '' : 's'}`,
+ labels: {
+ confirm: 'Delete',
+ cancel: 'Cancel',
+ },
+ onCancel() {
+ modals.closeAll();
+ },
+ onConfirm() {
+ deleteTags.mutate([tag.id], {
+ onSuccess: () => {
+ showNotification({
+ title: 'Tag deleted',
+ message: `Tag ${tag.name} was deleted`,
+ color: 'green',
+ icon: ,
+ });
+ modals.closeAll();
+ tags.refetch();
+ },
+ });
+ },
+ });
+ };
+
+ return (
+ ({
+ backgroundColor: tag.color,
+ '&:hover': {
+ backgroundColor: t.fn.darken(tag.color, 0.1),
+ },
+ cursor: 'pointer',
+ })}
+ px='xs'
+ onClick={deleteTag}
+ >
+
+
+ {tag.name} ({tag.files.length})
+
+
+
+ );
+}
+
+export function CreateTagModal({ tags, open, onClose }) {
+ const [color, setColor] = useState('');
+ const [name, setName] = useState('');
+
+ const [colorError, setColorError] = useState('');
+ const [nameError, setNameError] = useState('');
+
+ const onSubmit = async (e) => {
+ e.preventDefault();
+ setNameError('');
+ setColorError('');
+
+ const n = name.trim();
+ const c = color.trim();
+
+ if (n.length === 0 && c.length === 0) {
+ setNameError('Name is required');
+ setColorError('Color is required');
+ return;
+ } else if (n.length === 0) {
+ setNameError('Name is required');
+ setColorError('');
+ return;
+ } else if (c.length === 0) {
+ setNameError('');
+ setColorError('Color is required');
+ return;
+ }
+
+ const data = await useFetch('/api/user/tags', 'POST', {
+ tags: [
+ {
+ name: n,
+ color: c,
+ },
+ ],
+ });
+
+ if (!data.error) {
+ showNotification({
+ title: 'Tag created',
+ message: (
+ <>
+ Tag {name} was created
+ >
+ ),
+ color: 'green',
+ icon: ,
+ });
+ tags.refetch();
+ onClose();
+ } else {
+ showNotification({
+ title: 'Error creating tag',
+ message: data.error,
+ color: 'red',
+ icon: ,
+ });
+ }
+ };
+
+ return (
+ Create Tag} size='xs' opened={open} onClose={onClose} zIndex={300}>
+
+
+ );
+}
+
+export default function TagsModal({ open, onClose }) {
+ const tags = useTags();
+
+ const [createOpen, setCreateOpen] = useState(false);
+
+ return (
+ <>
+ setCreateOpen(false)} />
+ Tags} size='auto' opened={open} onClose={onClose}>
+ Click on a tag to delete it.
+
+ {tags.isSuccess && tags.data.map((tag) => )}
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/pages/Files/index.tsx b/src/components/pages/Files/index.tsx
index 9a03bbe3..f1d3411f 100644
--- a/src/components/pages/Files/index.tsx
+++ b/src/components/pages/Files/index.tsx
@@ -1,5 +1,5 @@
import { Accordion, ActionIcon, Box, Group, Pagination, SimpleGrid, Title, Tooltip } from '@mantine/core';
-import { IconFileUpload, IconPhotoUp } from '@tabler/icons-react';
+import { IconFileUpload, IconPhotoUp, IconTags } from '@tabler/icons-react';
import File from 'components/File';
import useFetch from 'hooks/useFetch';
import { usePaginatedFiles } from 'lib/queries/files';
@@ -7,13 +7,15 @@ import Link from 'next/link';
import { useEffect, useState } from 'react';
import FilePagation from './FilePagation';
import PendingFilesModal from './PendingFilesModal';
+import TagsModal from 'components/pages/Files/TagsModal';
export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) {
const [favoritePage, setFavoritePage] = useState(1);
const [favoriteNumPages, setFavoriteNumPages] = useState(0);
const favoritePages = usePaginatedFiles(favoritePage, 'media', true);
- const [open, setOpen] = useState(false);
+ const [pendingOpen, setPendingOpen] = useState(false);
+ const [tagsOpen, setTagsOpen] = useState(false);
useEffect(() => {
(async () => {
@@ -24,7 +26,8 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
return (
<>
- setOpen(false)} />
+ setPendingOpen(false)} />
+ setTagsOpen(false)} />
Files
@@ -33,10 +36,15 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
- setOpen(true)} variant='filled' color='primary'>
+ setPendingOpen(true)} variant='filled' color='primary'>
+
+ setTagsOpen(true)} variant='filled' color='primary'>
+
+
+
{favoritePages.isSuccess && favoritePages.data.length ? (
{
+ return useQuery(['tags'], async () => {
+ return fetch('/api/user/tags')
+ .then((res) => res.json() as Promise)
+ .then((data) => data);
+ });
+};
+
+export const useFileTags = (id: string) => {
+ return useQuery(['tags', id], async () => {
+ return fetch(`/api/user/file/${id}/tags`)
+ .then((res) => res.json() as Promise)
+ .then((data) => data);
+ });
+};
+
+export const useUpdateFileTags = (id: string) => {
+ return useMutation(
+ (tags: TagsRequest[]) =>
+ fetch(`/api/user/file/${id}/tags`, {
+ method: 'POST',
+ body: JSON.stringify({ tags }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }).then((res) => res.json()),
+ {
+ onSuccess: () => {
+ queryClient.refetchQueries(['tags', id]);
+ queryClient.refetchQueries(['files']);
+ },
+ }
+ );
+};
+
+export const useDeleteFileTags = (id: string) => {
+ return useMutation(
+ (tags: string[]) =>
+ fetch(`/api/user/file/${id}/tags`, {
+ method: 'DELETE',
+ body: JSON.stringify({ tags }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }).then((res) => res.json()),
+ {
+ onSuccess: () => {
+ queryClient.refetchQueries(['tags', id]);
+ },
+ }
+ );
+};
+
+export const useDeleteTags = () => {
+ return useMutation(
+ (tags: string[]) =>
+ fetch('/api/user/tags', {
+ method: 'DELETE',
+ body: JSON.stringify({ tags }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }).then((res) => res.json()),
+ {
+ onSuccess: () => {
+ queryClient.refetchQueries(['tags']);
+ queryClient.refetchQueries(['files']);
+ },
+ }
+ );
+};
+
+// export const usePaginatedFiles = (page?: number, filter = 'media', favorite = null) => {
+// const queryBuilder = new URLSearchParams({
+// page: Number(page || '1').toString(),
+// filter,
+// ...(favorite !== null && { favorite: favorite.toString() }),
+// });
+// const queryString = queryBuilder.toString();
+//
+// return useQuery(['files', queryString], async () => {
+// return fetch('/api/user/paged?' + queryString)
+// .then((res) => res.json() as Promise)
+// .then((data) =>
+// data.map((x) => ({
+// ...x,
+// createdAt: new Date(x.createdAt),
+// expiresAt: x.expiresAt ? new Date(x.expiresAt) : null,
+// }))
+// );
+// });
+// };
+//
+// export const useRecent = (filter?: string) => {
+// return useQuery(['recent', filter], async () => {
+// return fetch(`/api/user/recent?filter=${encodeURIComponent(filter)}`)
+// .then((res) => res.json())
+// .then((data) =>
+// data.map((x) => ({
+// ...x,
+// createdAt: new Date(x.createdAt),
+// expiresAt: x.expiresAt ? new Date(x.expiresAt) : null,
+// }))
+// );
+// });
+// };
+//
+// export function useFileDelete() {
+// // '/api/user/files', 'DELETE', { id: image.id }
+// return useMutation(
+// async (id: string) => {
+// return fetch('/api/user/files', {
+// method: 'DELETE',
+// body: JSON.stringify({ id }),
+// headers: {
+// 'content-type': 'application/json',
+// },
+// }).then((res) => res.json());
+// },
+// {
+// onSuccess: () => {
+// queryClient.refetchQueries(['files']);
+// },
+// }
+// );
+// }
+//
+// export function useFileFavorite() {
+// // /api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite }
+// return useMutation(
+// async (data: { id: string; favorite: boolean }) => {
+// return fetch('/api/user/files', {
+// method: 'PATCH',
+// body: JSON.stringify(data),
+// headers: {
+// 'content-type': 'application/json',
+// },
+// }).then((res) => res.json());
+// },
+// {
+// onSuccess: () => {
+// queryClient.refetchQueries(['files']);
+// },
+// }
+// );
+// }
+//
+// export function invalidateFiles() {
+// return queryClient.invalidateQueries(['files', 'recent', 'stats']);
+// }
diff --git a/src/pages/api/user/file/[id]/tags.ts b/src/pages/api/user/file/[id]/tags.ts
index 62c497b9..4b6f37bc 100644
--- a/src/pages/api/user/file/[id]/tags.ts
+++ b/src/pages/api/user/file/[id]/tags.ts
@@ -72,7 +72,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
if (!tags) return res.badRequest('no tags');
if (!tags.length) return res.badRequest('no tags');
- // if the tag has an id, it means it already exists so we just connect it
+ // if the tag has an id, it means it already exists, so we just connect it
// if it doesn't have an id, we create it and then connect it
const nFile = await prisma.file.update({
where: { id },