Add Photos UI
This commit is contained in:
@@ -68,9 +68,9 @@ Just push to the `main` branch.
|
|||||||
- [x] WebDav Server
|
- [x] WebDav Server
|
||||||
- [x] [Desktop app for selective file sync](https://github.com/bewcloud/bewcloud-desktop/releases) (`rclone` via WebDav)
|
- [x] [Desktop app for selective file sync](https://github.com/bewcloud/bewcloud-desktop/releases) (`rclone` via WebDav)
|
||||||
- [x] [Mobile app for offline file view](https://github.com/bewcloud/bewcloud-mobile/releases) (API + WebDav client)
|
- [x] [Mobile app for offline file view](https://github.com/bewcloud/bewcloud-mobile/releases) (API + WebDav client)
|
||||||
- [x] Add photo auto-uplod support for mobile client
|
- [x] Add photo auto-upload support for mobile client
|
||||||
- [x] Notes UI
|
- [x] Notes UI
|
||||||
- [ ] Photos UI
|
- [x] Photos UI
|
||||||
- [ ] Add notes view support for mobile app
|
- [ ] Add notes view support for mobile app
|
||||||
- [ ] Add notes edit support for mobile app
|
- [ ] Add notes edit support for mobile app
|
||||||
- [ ] Respect `CONFIG_ENABLED_APPS` in `.env` for enabling apps
|
- [ ] Respect `CONFIG_ENABLED_APPS` in `.env` for enabling apps
|
||||||
|
|||||||
@@ -1,37 +1,44 @@
|
|||||||
interface FilesBreadcrumbProps {
|
interface FilesBreadcrumbProps {
|
||||||
path: string;
|
path: string;
|
||||||
isShowingNotes?: boolean;
|
isShowingNotes?: boolean;
|
||||||
|
isShowingPhotos?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FilesBreadcrumb({ path, isShowingNotes }: FilesBreadcrumbProps) {
|
export default function FilesBreadcrumb({ path, isShowingNotes, isShowingPhotos }: FilesBreadcrumbProps) {
|
||||||
const routePath = isShowingNotes ? 'notes' : 'files';
|
let routePath = 'files';
|
||||||
|
let rootPath = '/';
|
||||||
|
|
||||||
if (!isShowingNotes && path === '/') {
|
if (isShowingNotes) {
|
||||||
return (
|
routePath = 'notes';
|
||||||
<h3 class='text-base font-semibold text-white whitespace-nowrap mr-2'>
|
rootPath = '/Notes/';
|
||||||
All files
|
} else if (isShowingPhotos) {
|
||||||
</h3>
|
routePath = 'photos';
|
||||||
);
|
rootPath = '/Photos/';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isShowingNotes && path === '/Notes/') {
|
const itemPluralLabel = routePath;
|
||||||
|
|
||||||
|
if (path === rootPath) {
|
||||||
return (
|
return (
|
||||||
<h3 class='text-base font-semibold text-white whitespace-nowrap mr-2'>
|
<h3 class='text-base font-semibold text-white whitespace-nowrap mr-2'>
|
||||||
All notes
|
All {itemPluralLabel}
|
||||||
</h3>
|
</h3>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathParts = path.slice(1, -1).split('/');
|
const pathParts = path.slice(1, -1).split('/');
|
||||||
|
|
||||||
if (isShowingNotes) {
|
|
||||||
pathParts.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h3 class='text-base font-semibold text-white whitespace-nowrap mr-2'>
|
<h3 class='text-base font-semibold text-white whitespace-nowrap mr-2'>
|
||||||
{isShowingNotes ? <a href={`/notes?path=/Notes/`}>All notes</a> : <a href={`/files?path=/`}>All files</a>}
|
{!isShowingNotes && !isShowingPhotos ? <a href={`/files?path=/`}>All files</a> : null}
|
||||||
|
{isShowingNotes ? <a href={`/notes?path=/Notes/`}>All notes</a> : null}
|
||||||
|
{isShowingPhotos ? <a href={`/photos?path=/Photos/`}>All photos</a> : null}
|
||||||
{pathParts.map((part, index) => {
|
{pathParts.map((part, index) => {
|
||||||
|
// Ignore the first directory in special ones
|
||||||
|
if (index === 0 && (isShowingNotes || isShowingPhotos)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (index === pathParts.length - 1) {
|
if (index === pathParts.length - 1) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ interface ListFilesProps {
|
|||||||
onClickOpenRenameFile?: (parentPath: string, name: string) => void;
|
onClickOpenRenameFile?: (parentPath: string, name: string) => void;
|
||||||
onClickOpenMoveDirectory?: (parentPath: string, name: string) => void;
|
onClickOpenMoveDirectory?: (parentPath: string, name: string) => void;
|
||||||
onClickOpenMoveFile?: (parentPath: string, name: string) => void;
|
onClickOpenMoveFile?: (parentPath: string, name: string) => void;
|
||||||
onClickDeleteDirectory: (parentPath: string, name: string) => Promise<void>;
|
onClickDeleteDirectory?: (parentPath: string, name: string) => Promise<void>;
|
||||||
onClickDeleteFile: (parentPath: string, name: string) => Promise<void>;
|
onClickDeleteFile?: (parentPath: string, name: string) => Promise<void>;
|
||||||
isShowingNotes?: boolean;
|
isShowingNotes?: boolean;
|
||||||
|
isShowingPhotos?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ListFiles(
|
export default function ListFiles(
|
||||||
@@ -24,6 +25,7 @@ export default function ListFiles(
|
|||||||
onClickDeleteDirectory,
|
onClickDeleteDirectory,
|
||||||
onClickDeleteFile,
|
onClickDeleteFile,
|
||||||
isShowingNotes,
|
isShowingNotes,
|
||||||
|
isShowingPhotos,
|
||||||
}: ListFilesProps,
|
}: ListFilesProps,
|
||||||
) {
|
) {
|
||||||
const dateFormat = new Intl.DateTimeFormat('en-GB', {
|
const dateFormat = new Intl.DateTimeFormat('en-GB', {
|
||||||
@@ -35,9 +37,23 @@ export default function ListFiles(
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
});
|
});
|
||||||
|
|
||||||
const routePath = isShowingNotes ? 'notes' : 'files';
|
let routePath = 'files';
|
||||||
const itemSingleLabel = isShowingNotes ? 'note' : 'file';
|
let itemSingleLabel = 'file';
|
||||||
const itemPluralLabel = routePath;
|
let itemPluralLabel = 'files';
|
||||||
|
|
||||||
|
if (isShowingNotes) {
|
||||||
|
routePath = 'notes';
|
||||||
|
itemSingleLabel = 'note';
|
||||||
|
itemPluralLabel = 'notes';
|
||||||
|
} else if (isShowingPhotos) {
|
||||||
|
routePath = 'photos';
|
||||||
|
itemSingleLabel = 'photo';
|
||||||
|
itemPluralLabel = 'photos';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isShowingPhotos && directories.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section class='mx-auto max-w-7xl my-8'>
|
<section class='mx-auto max-w-7xl my-8'>
|
||||||
@@ -46,8 +62,10 @@ export default function ListFiles(
|
|||||||
<tr class='border-b border-slate-600'>
|
<tr class='border-b border-slate-600'>
|
||||||
<th scope='col' class='px-6 py-4 font-medium text-white'>Name</th>
|
<th scope='col' class='px-6 py-4 font-medium text-white'>Name</th>
|
||||||
<th scope='col' class='px-6 py-4 font-medium text-white w-56'>Last update</th>
|
<th scope='col' class='px-6 py-4 font-medium text-white w-56'>Last update</th>
|
||||||
{isShowingNotes ? null : <th scope='col' class='px-6 py-4 font-medium text-white w-32'>Size</th>}
|
{isShowingNotes || isShowingPhotos
|
||||||
<th scope='col' class='px-6 py-4 font-medium text-white w-20'></th>
|
? null
|
||||||
|
: <th scope='col' class='px-6 py-4 font-medium text-white w-32'>Size</th>}
|
||||||
|
{isShowingPhotos ? null : <th scope='col' class='px-6 py-4 font-medium text-white w-20'></th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class='divide-y divide-slate-600 border-t border-slate-600'>
|
<tbody class='divide-y divide-slate-600 border-t border-slate-600'>
|
||||||
@@ -75,59 +93,63 @@ export default function ListFiles(
|
|||||||
<td class='px-6 py-4 text-slate-200'>
|
<td class='px-6 py-4 text-slate-200'>
|
||||||
{dateFormat.format(new Date(directory.updated_at))}
|
{dateFormat.format(new Date(directory.updated_at))}
|
||||||
</td>
|
</td>
|
||||||
{isShowingNotes ? null : (
|
{isShowingNotes || isShowingPhotos ? null : (
|
||||||
<td class='px-6 py-4 text-slate-200'>
|
<td class='px-6 py-4 text-slate-200'>
|
||||||
-
|
-
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td class='px-6 py-4'>
|
{isShowingPhotos ? null : (
|
||||||
{(fullPath === TRASH_PATH || typeof onClickOpenRenameDirectory === 'undefined' ||
|
<td class='px-6 py-4'>
|
||||||
typeof onClickOpenMoveDirectory === 'undefined')
|
{(fullPath === TRASH_PATH || typeof onClickOpenRenameDirectory === 'undefined' ||
|
||||||
? null
|
typeof onClickOpenMoveDirectory === 'undefined')
|
||||||
: (
|
? null
|
||||||
<section class='flex items-center justify-end w-20'>
|
: (
|
||||||
<span
|
<section class='flex items-center justify-end w-20'>
|
||||||
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
<span
|
||||||
onClick={() => onClickOpenRenameDirectory(directory.parent_path, directory.directory_name)}
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
||||||
>
|
onClick={() => onClickOpenRenameDirectory(directory.parent_path, directory.directory_name)}
|
||||||
<img
|
>
|
||||||
src='/images/rename.svg'
|
<img
|
||||||
class='white drop-shadow-md'
|
src='/images/rename.svg'
|
||||||
width={18}
|
class='white drop-shadow-md'
|
||||||
height={18}
|
width={18}
|
||||||
alt='Rename directory'
|
height={18}
|
||||||
title='Rename directory'
|
alt='Rename directory'
|
||||||
/>
|
title='Rename directory'
|
||||||
</span>
|
/>
|
||||||
<span
|
</span>
|
||||||
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
<span
|
||||||
onClick={() => onClickOpenMoveDirectory(directory.parent_path, directory.directory_name)}
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
||||||
>
|
onClick={() => onClickOpenMoveDirectory(directory.parent_path, directory.directory_name)}
|
||||||
<img
|
>
|
||||||
src='/images/move.svg'
|
<img
|
||||||
class='white drop-shadow-md'
|
src='/images/move.svg'
|
||||||
width={18}
|
class='white drop-shadow-md'
|
||||||
height={18}
|
width={18}
|
||||||
alt='Move directory'
|
height={18}
|
||||||
title='Move directory'
|
alt='Move directory'
|
||||||
/>
|
title='Move directory'
|
||||||
</span>
|
/>
|
||||||
<span
|
</span>
|
||||||
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100'
|
{typeof onClickDeleteDirectory === 'undefined' ? null : (
|
||||||
onClick={() => onClickDeleteDirectory(directory.parent_path, directory.directory_name)}
|
<span
|
||||||
>
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100'
|
||||||
<img
|
onClick={() => onClickDeleteDirectory(directory.parent_path, directory.directory_name)}
|
||||||
src='/images/delete.svg'
|
>
|
||||||
class='red drop-shadow-md'
|
<img
|
||||||
width={20}
|
src='/images/delete.svg'
|
||||||
height={20}
|
class='red drop-shadow-md'
|
||||||
alt='Delete directory'
|
width={20}
|
||||||
title='Delete directory'
|
height={20}
|
||||||
/>
|
alt='Delete directory'
|
||||||
</span>
|
title='Delete directory'
|
||||||
</section>
|
/>
|
||||||
)}
|
</span>
|
||||||
</td>
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -159,53 +181,57 @@ export default function ListFiles(
|
|||||||
{humanFileSize(file.size_in_bytes)}
|
{humanFileSize(file.size_in_bytes)}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td class='px-6 py-4'>
|
{isShowingPhotos ? null : (
|
||||||
<section class='flex items-center justify-end w-20'>
|
<td class='px-6 py-4'>
|
||||||
{typeof onClickOpenRenameFile === 'undefined' ? null : (
|
<section class='flex items-center justify-end w-20'>
|
||||||
<span
|
{typeof onClickOpenRenameFile === 'undefined' ? null : (
|
||||||
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
<span
|
||||||
onClick={() => onClickOpenRenameFile(file.parent_path, file.file_name)}
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
||||||
>
|
onClick={() => onClickOpenRenameFile(file.parent_path, file.file_name)}
|
||||||
<img
|
>
|
||||||
src='/images/rename.svg'
|
<img
|
||||||
class='white drop-shadow-md'
|
src='/images/rename.svg'
|
||||||
width={18}
|
class='white drop-shadow-md'
|
||||||
height={18}
|
width={18}
|
||||||
alt={`Rename ${itemSingleLabel}`}
|
height={18}
|
||||||
title={`Rename ${itemSingleLabel}`}
|
alt={`Rename ${itemSingleLabel}`}
|
||||||
/>
|
title={`Rename ${itemSingleLabel}`}
|
||||||
</span>
|
/>
|
||||||
)}
|
</span>
|
||||||
{typeof onClickOpenMoveFile === 'undefined' ? null : (
|
)}
|
||||||
<span
|
{typeof onClickOpenMoveFile === 'undefined' ? null : (
|
||||||
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
<span
|
||||||
onClick={() => onClickOpenMoveFile(file.parent_path, file.file_name)}
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
||||||
>
|
onClick={() => onClickOpenMoveFile(file.parent_path, file.file_name)}
|
||||||
<img
|
>
|
||||||
src='/images/move.svg'
|
<img
|
||||||
class='white drop-shadow-md'
|
src='/images/move.svg'
|
||||||
width={18}
|
class='white drop-shadow-md'
|
||||||
height={18}
|
width={18}
|
||||||
alt={`Move ${itemSingleLabel}`}
|
height={18}
|
||||||
title={`Move ${itemSingleLabel}`}
|
alt={`Move ${itemSingleLabel}`}
|
||||||
/>
|
title={`Move ${itemSingleLabel}`}
|
||||||
</span>
|
/>
|
||||||
)}
|
</span>
|
||||||
<span
|
)}
|
||||||
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100'
|
{typeof onClickDeleteFile === 'undefined' ? null : (
|
||||||
onClick={() => onClickDeleteFile(file.parent_path, file.file_name)}
|
<span
|
||||||
>
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100'
|
||||||
<img
|
onClick={() => onClickDeleteFile(file.parent_path, file.file_name)}
|
||||||
src='/images/delete.svg'
|
>
|
||||||
class='red drop-shadow-md'
|
<img
|
||||||
width={20}
|
src='/images/delete.svg'
|
||||||
height={20}
|
class='red drop-shadow-md'
|
||||||
alt={`Delete ${itemSingleLabel}`}
|
width={20}
|
||||||
title={`Delete ${itemSingleLabel}`}
|
height={20}
|
||||||
/>
|
alt={`Delete ${itemSingleLabel}`}
|
||||||
</span>
|
title={`Delete ${itemSingleLabel}`}
|
||||||
</section>
|
/>
|
||||||
</td>
|
</span>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{directories.length === 0 && files.length === 0
|
{directories.length === 0 && files.length === 0
|
||||||
|
|||||||
226
components/files/ListPhotos.tsx
Normal file
226
components/files/ListPhotos.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { Directory, DirectoryFile } from '/lib/types.ts';
|
||||||
|
import { humanFileSize, TRASH_PATH } from '/lib/utils/files.ts';
|
||||||
|
|
||||||
|
interface ListFilesProps {
|
||||||
|
directories: Directory[];
|
||||||
|
files: DirectoryFile[];
|
||||||
|
onClickOpenRenameDirectory?: (parentPath: string, name: string) => void;
|
||||||
|
onClickOpenRenameFile?: (parentPath: string, name: string) => void;
|
||||||
|
onClickOpenMoveDirectory?: (parentPath: string, name: string) => void;
|
||||||
|
onClickOpenMoveFile?: (parentPath: string, name: string) => void;
|
||||||
|
onClickDeleteDirectory: (parentPath: string, name: string) => Promise<void>;
|
||||||
|
onClickDeleteFile: (parentPath: string, name: string) => Promise<void>;
|
||||||
|
isShowingNotes?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ListFiles(
|
||||||
|
{
|
||||||
|
directories,
|
||||||
|
files,
|
||||||
|
onClickOpenRenameDirectory,
|
||||||
|
onClickOpenRenameFile,
|
||||||
|
onClickOpenMoveDirectory,
|
||||||
|
onClickOpenMoveFile,
|
||||||
|
onClickDeleteDirectory,
|
||||||
|
onClickDeleteFile,
|
||||||
|
isShowingNotes,
|
||||||
|
}: ListFilesProps,
|
||||||
|
) {
|
||||||
|
const dateFormat = new Intl.DateTimeFormat('en-GB', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
|
||||||
|
const routePath = isShowingNotes ? 'notes' : 'files';
|
||||||
|
const itemSingleLabel = isShowingNotes ? 'note' : 'file';
|
||||||
|
const itemPluralLabel = routePath;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section class='mx-auto max-w-7xl my-8'>
|
||||||
|
<table class='w-full border-collapse bg-gray-900 text-left text-sm text-slate-500 shadow-sm rounded-md'>
|
||||||
|
<thead>
|
||||||
|
<tr class='border-b border-slate-600'>
|
||||||
|
<th scope='col' class='px-6 py-4 font-medium text-white'>Name</th>
|
||||||
|
<th scope='col' class='px-6 py-4 font-medium text-white w-56'>Last update</th>
|
||||||
|
{isShowingNotes ? null : <th scope='col' class='px-6 py-4 font-medium text-white w-32'>Size</th>}
|
||||||
|
<th scope='col' class='px-6 py-4 font-medium text-white w-20'></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class='divide-y divide-slate-600 border-t border-slate-600'>
|
||||||
|
{directories.map((directory) => {
|
||||||
|
const fullPath = `${directory.parent_path}${directory.directory_name}/`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr class='bg-slate-700 hover:bg-slate-600 group'>
|
||||||
|
<td class='flex gap-3 px-6 py-4'>
|
||||||
|
<a
|
||||||
|
href={`/${routePath}?path=${fullPath}`}
|
||||||
|
class='flex items-center font-normal text-white'
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`/images/${fullPath === TRASH_PATH ? 'trash.svg' : 'directory.svg'}`}
|
||||||
|
class='white drop-shadow-md mr-2'
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
alt='Directory'
|
||||||
|
title='Directory'
|
||||||
|
/>
|
||||||
|
{directory.directory_name}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class='px-6 py-4 text-slate-200'>
|
||||||
|
{dateFormat.format(new Date(directory.updated_at))}
|
||||||
|
</td>
|
||||||
|
{isShowingNotes ? null : (
|
||||||
|
<td class='px-6 py-4 text-slate-200'>
|
||||||
|
-
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td class='px-6 py-4'>
|
||||||
|
{(fullPath === TRASH_PATH || typeof onClickOpenRenameDirectory === 'undefined' ||
|
||||||
|
typeof onClickOpenMoveDirectory === 'undefined')
|
||||||
|
? null
|
||||||
|
: (
|
||||||
|
<section class='flex items-center justify-end w-20'>
|
||||||
|
<span
|
||||||
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
||||||
|
onClick={() => onClickOpenRenameDirectory(directory.parent_path, directory.directory_name)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src='/images/rename.svg'
|
||||||
|
class='white drop-shadow-md'
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
alt='Rename directory'
|
||||||
|
title='Rename directory'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
||||||
|
onClick={() => onClickOpenMoveDirectory(directory.parent_path, directory.directory_name)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src='/images/move.svg'
|
||||||
|
class='white drop-shadow-md'
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
alt='Move directory'
|
||||||
|
title='Move directory'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100'
|
||||||
|
onClick={() => onClickDeleteDirectory(directory.parent_path, directory.directory_name)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src='/images/delete.svg'
|
||||||
|
class='red drop-shadow-md'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
alt='Delete directory'
|
||||||
|
title='Delete directory'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{files.map((file) => (
|
||||||
|
<tr class='bg-slate-700 hover:bg-slate-600 group'>
|
||||||
|
<td class='flex gap-3 px-6 py-4'>
|
||||||
|
<a
|
||||||
|
href={`/${routePath}/open/${file.file_name}?path=${file.parent_path}`}
|
||||||
|
class='flex items-center font-normal text-white'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src='/images/file.svg'
|
||||||
|
class='white drop-shadow-md mr-2'
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
alt='File'
|
||||||
|
title='File'
|
||||||
|
/>
|
||||||
|
{file.file_name}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class='px-6 py-4 text-slate-200'>
|
||||||
|
{dateFormat.format(new Date(file.updated_at))}
|
||||||
|
</td>
|
||||||
|
{isShowingNotes ? null : (
|
||||||
|
<td class='px-6 py-4 text-slate-200'>
|
||||||
|
{humanFileSize(file.size_in_bytes)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td class='px-6 py-4'>
|
||||||
|
<section class='flex items-center justify-end w-20'>
|
||||||
|
{typeof onClickOpenRenameFile === 'undefined' ? null : (
|
||||||
|
<span
|
||||||
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
||||||
|
onClick={() => onClickOpenRenameFile(file.parent_path, file.file_name)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src='/images/rename.svg'
|
||||||
|
class='white drop-shadow-md'
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
alt={`Rename ${itemSingleLabel}`}
|
||||||
|
title={`Rename ${itemSingleLabel}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{typeof onClickOpenMoveFile === 'undefined' ? null : (
|
||||||
|
<span
|
||||||
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100 mr-2'
|
||||||
|
onClick={() => onClickOpenMoveFile(file.parent_path, file.file_name)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src='/images/move.svg'
|
||||||
|
class='white drop-shadow-md'
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
alt={`Move ${itemSingleLabel}`}
|
||||||
|
title={`Move ${itemSingleLabel}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100'
|
||||||
|
onClick={() => onClickDeleteFile(file.parent_path, file.file_name)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src='/images/delete.svg'
|
||||||
|
class='red drop-shadow-md'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
alt={`Delete ${itemSingleLabel}`}
|
||||||
|
title={`Delete ${itemSingleLabel}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{directories.length === 0 && files.length === 0
|
||||||
|
? (
|
||||||
|
<tr>
|
||||||
|
<td class='flex gap-3 px-6 py-4 font-normal' colspan={4}>
|
||||||
|
<div class='text-md'>
|
||||||
|
<div class='font-medium text-slate-400'>No {itemPluralLabel} to show</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
components/photos/ListPhotos.tsx
Normal file
66
components/photos/ListPhotos.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { DirectoryFile } from '/lib/types.ts';
|
||||||
|
import { PHOTO_IMAGE_EXTENSIONS, PHOTO_VIDEO_EXTENSIONS } from '/lib/utils/photos.ts';
|
||||||
|
|
||||||
|
interface ListPhotosProps {
|
||||||
|
files: DirectoryFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ListPhotos(
|
||||||
|
{
|
||||||
|
files,
|
||||||
|
}: ListPhotosProps,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<section class='mx-auto max-w-7xl my-8'>
|
||||||
|
{files.length === 0
|
||||||
|
? (
|
||||||
|
<article class='px-6 py-4 font-normal text-center w-full'>
|
||||||
|
<div class='font-medium text-slate-400 text-md'>No photos to show</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<section class='w-full grid grid-cols-2 md:grid-cols-3 gap-4'>
|
||||||
|
{files.map((file) => {
|
||||||
|
const lowercaseFileName = file.file_name.toLowerCase();
|
||||||
|
const extensionName = lowercaseFileName.split('.').pop() || '';
|
||||||
|
|
||||||
|
const isImage = PHOTO_IMAGE_EXTENSIONS.some((extension) => extension === extensionName);
|
||||||
|
const isVideo = PHOTO_VIDEO_EXTENSIONS.some((extension) => extension === extensionName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article class='hover:opacity-70'>
|
||||||
|
<a
|
||||||
|
href={`/files/open/${file.file_name}?path=${file.parent_path}`}
|
||||||
|
class='flex items-center'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
{isVideo
|
||||||
|
? (
|
||||||
|
<video class='h-auto max-w-full rounded-md' title={file.file_name}>
|
||||||
|
<source
|
||||||
|
src={`/files/open/${file.file_name}?path=${file.parent_path}`}
|
||||||
|
type={`video/${extensionName}`}
|
||||||
|
/>
|
||||||
|
</video>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
{isImage
|
||||||
|
? (
|
||||||
|
<img
|
||||||
|
src={`/files/open/${file.file_name}?path=${file.parent_path}`}
|
||||||
|
class='h-auto max-w-full rounded-md'
|
||||||
|
alt={file.file_name}
|
||||||
|
title={file.file_name}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
224
components/photos/MainPhotos.tsx
Normal file
224
components/photos/MainPhotos.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { useSignal } from '@preact/signals';
|
||||||
|
|
||||||
|
import { Directory, DirectoryFile } from '/lib/types.ts';
|
||||||
|
import { ResponseBody as UploadResponseBody } from '/routes/api/files/upload.tsx';
|
||||||
|
import {
|
||||||
|
RequestBody as CreateDirectoryRequestBody,
|
||||||
|
ResponseBody as CreateDirectoryResponseBody,
|
||||||
|
} from '/routes/api/files/create-directory.tsx';
|
||||||
|
import CreateDirectoryModal from '/components/files/CreateDirectoryModal.tsx';
|
||||||
|
import ListFiles from '/components/files/ListFiles.tsx';
|
||||||
|
import FilesBreadcrumb from '/components/files/FilesBreadcrumb.tsx';
|
||||||
|
import ListPhotos from '/components/photos/ListPhotos.tsx';
|
||||||
|
|
||||||
|
interface MainPhotosProps {
|
||||||
|
initialDirectories: Directory[];
|
||||||
|
initialFiles: DirectoryFile[];
|
||||||
|
initialPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MainPhotos({ initialDirectories, initialFiles, initialPath }: MainPhotosProps) {
|
||||||
|
const isAdding = useSignal<boolean>(false);
|
||||||
|
const isUploading = useSignal<boolean>(false);
|
||||||
|
const directories = useSignal<Directory[]>(initialDirectories);
|
||||||
|
const files = useSignal<DirectoryFile[]>(initialFiles);
|
||||||
|
const path = useSignal<string>(initialPath);
|
||||||
|
const areNewOptionsOption = useSignal<boolean>(false);
|
||||||
|
const isNewDirectoryModalOpen = useSignal<boolean>(false);
|
||||||
|
|
||||||
|
function onClickUploadFile() {
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.multiple = true;
|
||||||
|
fileInput.accept = 'image/*,video/*';
|
||||||
|
fileInput.click();
|
||||||
|
|
||||||
|
fileInput.onchange = async (event) => {
|
||||||
|
const chosenFilesList = (event.target as HTMLInputElement)?.files!;
|
||||||
|
|
||||||
|
const chosenFiles = Array.from(chosenFilesList);
|
||||||
|
|
||||||
|
isUploading.value = true;
|
||||||
|
|
||||||
|
for (const chosenFile of chosenFiles) {
|
||||||
|
if (!chosenFile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
areNewOptionsOption.value = false;
|
||||||
|
|
||||||
|
const requestBody = new FormData();
|
||||||
|
requestBody.set('parent_path', path.value);
|
||||||
|
requestBody.set('name', chosenFile.name);
|
||||||
|
requestBody.set('contents', chosenFile);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/files/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: requestBody,
|
||||||
|
});
|
||||||
|
const result = await response.json() as UploadResponseBody;
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error('Failed to upload photo!');
|
||||||
|
}
|
||||||
|
|
||||||
|
files.value = [...result.newFiles];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isUploading.value = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickCreateDirectory() {
|
||||||
|
if (isNewDirectoryModalOpen.value) {
|
||||||
|
isNewDirectoryModalOpen.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNewDirectoryModalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onClickSaveDirectory(newDirectoryName: string) {
|
||||||
|
if (isAdding.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newDirectoryName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
areNewOptionsOption.value = false;
|
||||||
|
isAdding.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestBody: CreateDirectoryRequestBody = {
|
||||||
|
parentPath: path.value,
|
||||||
|
name: newDirectoryName,
|
||||||
|
};
|
||||||
|
const response = await fetch(`/api/files/create-directory`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
});
|
||||||
|
const result = await response.json() as CreateDirectoryResponseBody;
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error('Failed to create directory!');
|
||||||
|
}
|
||||||
|
|
||||||
|
directories.value = [...result.newDirectories];
|
||||||
|
|
||||||
|
isNewDirectoryModalOpen.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdding.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCloseCreateDirectory() {
|
||||||
|
isNewDirectoryModalOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNewOptionsDropdown() {
|
||||||
|
areNewOptionsOption.value = !areNewOptionsOption.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section class='flex flex-row items-center justify-between mb-4'>
|
||||||
|
<section class='flex items-center justify-end w-full'>
|
||||||
|
<FilesBreadcrumb path={path.value} isShowingPhotos={true} />
|
||||||
|
|
||||||
|
<section class='relative inline-block text-left ml-2'>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class='inline-block justify-center gap-x-1.5 rounded-md bg-[#51A4FB] px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-400 ml-2'
|
||||||
|
type='button'
|
||||||
|
title='Add new file or directory'
|
||||||
|
id='new-button'
|
||||||
|
aria-expanded='true'
|
||||||
|
aria-haspopup='true'
|
||||||
|
onClick={() => toggleNewOptionsDropdown()}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src='/images/add.svg'
|
||||||
|
alt='Add new file or directory'
|
||||||
|
class={`white ${isAdding.value || isUploading.value ? 'animate-spin' : ''}`}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={`absolute right-0 z-10 mt-2 w-44 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none ${
|
||||||
|
!areNewOptionsOption.value ? 'hidden' : ''
|
||||||
|
}`}
|
||||||
|
role='menu'
|
||||||
|
aria-orientation='vertical'
|
||||||
|
aria-labelledby='new-button'
|
||||||
|
tabindex={-1}
|
||||||
|
>
|
||||||
|
<div class='py-1'>
|
||||||
|
<button
|
||||||
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
|
onClick={() => onClickUploadFile()}
|
||||||
|
>
|
||||||
|
Upload Photo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
|
onClick={() => onClickCreateDirectory()}
|
||||||
|
>
|
||||||
|
New Directory
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class='mx-auto max-w-7xl my-8'>
|
||||||
|
<ListFiles
|
||||||
|
directories={directories.value}
|
||||||
|
files={[]}
|
||||||
|
isShowingPhotos={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListPhotos
|
||||||
|
files={files.value}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class={`flex justify-end items-center text-sm mt-1 mx-2 text-slate-100`}
|
||||||
|
>
|
||||||
|
{isAdding.value
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
<img src='/images/loading.svg' class='white mr-2' width={18} height={18} />Creating...
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
{isUploading.value
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
<img src='/images/loading.svg' class='white mr-2' width={18} height={18} />Uploading...
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
{!isAdding.value && !isUploading.value ? <> </> : null}
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<CreateDirectoryModal
|
||||||
|
isOpen={isNewDirectoryModalOpen.value}
|
||||||
|
onClickSave={onClickSaveDirectory}
|
||||||
|
onClose={onCloseCreateDirectory}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ import * as $news from './routes/news.tsx';
|
|||||||
import * as $news_feeds from './routes/news/feeds.tsx';
|
import * as $news_feeds from './routes/news/feeds.tsx';
|
||||||
import * as $notes from './routes/notes.tsx';
|
import * as $notes from './routes/notes.tsx';
|
||||||
import * as $notes_open_fileName_ from './routes/notes/open/[fileName].tsx';
|
import * as $notes_open_fileName_ from './routes/notes/open/[fileName].tsx';
|
||||||
|
import * as $photos from './routes/photos.tsx';
|
||||||
import * as $settings from './routes/settings.tsx';
|
import * as $settings from './routes/settings.tsx';
|
||||||
import * as $signup from './routes/signup.tsx';
|
import * as $signup from './routes/signup.tsx';
|
||||||
import * as $Settings from './islands/Settings.tsx';
|
import * as $Settings from './islands/Settings.tsx';
|
||||||
@@ -45,6 +46,7 @@ import * as $news_Articles from './islands/news/Articles.tsx';
|
|||||||
import * as $news_Feeds from './islands/news/Feeds.tsx';
|
import * as $news_Feeds from './islands/news/Feeds.tsx';
|
||||||
import * as $notes_Note from './islands/notes/Note.tsx';
|
import * as $notes_Note from './islands/notes/Note.tsx';
|
||||||
import * as $notes_NotesWrapper from './islands/notes/NotesWrapper.tsx';
|
import * as $notes_NotesWrapper from './islands/notes/NotesWrapper.tsx';
|
||||||
|
import * as $photos_PhotosWrapper from './islands/photos/PhotosWrapper.tsx';
|
||||||
import { type Manifest } from '$fresh/server.ts';
|
import { type Manifest } from '$fresh/server.ts';
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
@@ -82,6 +84,7 @@ const manifest = {
|
|||||||
'./routes/news/feeds.tsx': $news_feeds,
|
'./routes/news/feeds.tsx': $news_feeds,
|
||||||
'./routes/notes.tsx': $notes,
|
'./routes/notes.tsx': $notes,
|
||||||
'./routes/notes/open/[fileName].tsx': $notes_open_fileName_,
|
'./routes/notes/open/[fileName].tsx': $notes_open_fileName_,
|
||||||
|
'./routes/photos.tsx': $photos,
|
||||||
'./routes/settings.tsx': $settings,
|
'./routes/settings.tsx': $settings,
|
||||||
'./routes/signup.tsx': $signup,
|
'./routes/signup.tsx': $signup,
|
||||||
},
|
},
|
||||||
@@ -94,6 +97,7 @@ const manifest = {
|
|||||||
'./islands/news/Feeds.tsx': $news_Feeds,
|
'./islands/news/Feeds.tsx': $news_Feeds,
|
||||||
'./islands/notes/Note.tsx': $notes_Note,
|
'./islands/notes/Note.tsx': $notes_Note,
|
||||||
'./islands/notes/NotesWrapper.tsx': $notes_NotesWrapper,
|
'./islands/notes/NotesWrapper.tsx': $notes_NotesWrapper,
|
||||||
|
'./islands/photos/PhotosWrapper.tsx': $photos_PhotosWrapper,
|
||||||
},
|
},
|
||||||
baseUrl: import.meta.url,
|
baseUrl: import.meta.url,
|
||||||
} satisfies Manifest;
|
} satisfies Manifest;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default function Note({ fileName, currentPath, contents }: NoteProps) {
|
|||||||
</textarea>
|
</textarea>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class={`flex justify-end items-center text-sm mt-1 mx-2 ${
|
class={`flex justify-end items-center text-sm mt-1 mx-auto max-w-7xl ${
|
||||||
hasSaved.value ? 'text-green-600' : 'text-slate-100'
|
hasSaved.value ? 'text-green-600' : 'text-slate-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
21
islands/photos/PhotosWrapper.tsx
Normal file
21
islands/photos/PhotosWrapper.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Directory, DirectoryFile } from '/lib/types.ts';
|
||||||
|
import MainPhotos from '/components/photos/MainPhotos.tsx';
|
||||||
|
|
||||||
|
interface PhotosWrapperProps {
|
||||||
|
initialDirectories: Directory[];
|
||||||
|
initialFiles: DirectoryFile[];
|
||||||
|
initialPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This wrapper is necessary because islands need to be the first frontend component, but they don't support functions as props, so the more complex logic needs to live in the component itself
|
||||||
|
export default function PhotosWrapper(
|
||||||
|
{ initialDirectories, initialFiles, initialPath }: PhotosWrapperProps,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<MainPhotos
|
||||||
|
initialDirectories={initialDirectories}
|
||||||
|
initialFiles={initialFiles}
|
||||||
|
initialPath={initialPath}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
lib/utils/photos.ts
Normal file
3
lib/utils/photos.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const PHOTO_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'svg', 'bmp', 'gif', 'tiff', 'webp', 'heic'] as const;
|
||||||
|
export const PHOTO_VIDEO_EXTENSIONS = ['heif', 'webm', 'mp4', 'mov'] as const;
|
||||||
|
export const PHOTO_EXTENSIONS = [...PHOTO_IMAGE_EXTENSIONS, ...PHOTO_VIDEO_EXTENSIONS] as const;
|
||||||
@@ -40,7 +40,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FilesPage({ data }: PageProps<Data, FreshContextState>) {
|
export default function NotesPage({ data }: PageProps<Data, FreshContextState>) {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<NotesWrapper
|
<NotesWrapper
|
||||||
|
|||||||
58
routes/photos.tsx
Normal file
58
routes/photos.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Handlers, PageProps } from 'fresh/server.ts';
|
||||||
|
|
||||||
|
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
|
import { getDirectories, getFiles } from '/lib/data/files.ts';
|
||||||
|
import { PHOTO_EXTENSIONS } from '/lib/utils/photos.ts';
|
||||||
|
import PhotosWrapper from '/islands/photos/PhotosWrapper.tsx';
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
userDirectories: Directory[];
|
||||||
|
userPhotos: DirectoryFile[];
|
||||||
|
currentPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler: Handlers<Data, FreshContextState> = {
|
||||||
|
async GET(request, context) {
|
||||||
|
if (!context.state.user) {
|
||||||
|
return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = new URL(request.url).searchParams;
|
||||||
|
|
||||||
|
let currentPath = searchParams.get('path') || '/Photos/';
|
||||||
|
|
||||||
|
// Send invalid paths back to Photos root
|
||||||
|
if (!currentPath.startsWith('/Photos/') || currentPath.includes('../')) {
|
||||||
|
currentPath = '/Photos/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always append a trailing slash
|
||||||
|
if (!currentPath.endsWith('/')) {
|
||||||
|
currentPath = `${currentPath}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDirectories = await getDirectories(context.state.user.id, currentPath);
|
||||||
|
|
||||||
|
const userFiles = await getFiles(context.state.user.id, currentPath);
|
||||||
|
|
||||||
|
const userPhotos = userFiles.filter((file) => {
|
||||||
|
const lowercaseFileName = file.file_name.toLowerCase();
|
||||||
|
|
||||||
|
return PHOTO_EXTENSIONS.some((extension) => lowercaseFileName.endsWith(extension));
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.render({ userDirectories, userPhotos, currentPath });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PhotosPage({ data }: PageProps<Data, FreshContextState>) {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<PhotosWrapper
|
||||||
|
initialDirectories={data.userDirectories}
|
||||||
|
initialFiles={data.userPhotos}
|
||||||
|
initialPath={data.currentPath}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user