forked from Goutam/lynkeduppro-crm
feat: add core domain types for Photo Gallery SDK including media items, albums, and annotations
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { useViewMedia } from '../../hooks/useViewMedia';
|
||||
import { groupByTime } from '../../lib/grouping';
|
||||
import { Icon, type IconName } from '../../icons';
|
||||
import { useGallery, useGalleryStoreApi } from '../../store/context';
|
||||
import { liveMedia } from '../../store/selectors';
|
||||
import { MediaGrid } from '../MediaGrid';
|
||||
|
||||
function Welcome({ onImport }: { onImport: () => void }) {
|
||||
// Host-neutral copy: the SDK is embedded in other products (see `title`), so the
|
||||
// empty state must not name a specific photo app or reference its settings.
|
||||
const hints: Array<{ icon: IconName; text: string }> = [
|
||||
{ icon: 'download', text: 'Click the + button to import.' },
|
||||
{ icon: 'duplicates', text: 'Drag photos and videos straight in.' },
|
||||
{ icon: 'camera', text: 'Capture a shot with your camera.' },
|
||||
{ icon: 'image', text: 'Everything you add is searchable.' },
|
||||
];
|
||||
const title = useGallery((s) => s.config.title);
|
||||
return (
|
||||
<div className="apg-empty" onClick={onImport} role="button" tabIndex={0}>
|
||||
<div className="apg-empty__title">Welcome to {title}</div>
|
||||
<div className="apg-empty__subtitle">To get started, do any of the following:</div>
|
||||
<div className="apg-empty__hints">
|
||||
{hints.map((h, i) => (
|
||||
<div className="apg-empty__hint" key={i}>
|
||||
<span style={{ color: 'var(--apg-text-tertiary)' }}>
|
||||
<Icon name={h.icon} size={40} />
|
||||
</span>
|
||||
<span>{h.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LibraryView() {
|
||||
const api = useGalleryStoreApi();
|
||||
const items = useViewMedia();
|
||||
const scale = useGallery((s) => s.libraryScale);
|
||||
const searchQuery = useGallery((s) => s.searchQuery);
|
||||
const objectFocus = useGallery((s) => s.objectFocus);
|
||||
const tagFocus = useGallery((s) => s.tagFocus);
|
||||
const personFocus = useGallery((s) => s.personFocus);
|
||||
const personName = useGallery((s) => {
|
||||
if (!s.personFocus) return null;
|
||||
const p = s.people.find((x) => x.id === s.personFocus);
|
||||
return p?.name ?? 'Unnamed person';
|
||||
});
|
||||
const totalLive = useGallery((s) => liveMedia(s.media).length);
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const importFiles = async (files: FileList | null) => {
|
||||
if (!files?.length) return;
|
||||
await api.getState().importFiles(files);
|
||||
if (fileRef.current) fileRef.current.value = '';
|
||||
};
|
||||
|
||||
const onDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
void importFiles(e.dataTransfer.files);
|
||||
};
|
||||
|
||||
let content: React.ReactNode;
|
||||
if (items.length === 0) {
|
||||
if (totalLive === 0) {
|
||||
content = <Welcome onImport={() => fileRef.current?.click()} />;
|
||||
} else {
|
||||
content = (
|
||||
<div className="apg-empty">
|
||||
<div className="apg-empty__card">
|
||||
<div className="apg-empty__title" style={{ fontSize: 24 }}>
|
||||
No Results
|
||||
</div>
|
||||
<div className="apg-empty__subtitle">
|
||||
{objectFocus
|
||||
? `No photos containing “${objectFocus}”.`
|
||||
: searchQuery
|
||||
? 'Try a different search term.'
|
||||
: 'No items match this filter.'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else if (scale === 'all') {
|
||||
content = <MediaGrid items={items} />;
|
||||
} else {
|
||||
const granularity = scale === 'years' ? 'year' : scale === 'months' ? 'month' : 'day';
|
||||
const sections = groupByTime(items, granularity);
|
||||
content = (
|
||||
<>
|
||||
{sections.map((s) => (
|
||||
<MediaGrid key={s.key} items={s.items} title={s.title} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="apg-scroll"
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setDragging(true);
|
||||
}}
|
||||
onDragLeave={() => setDragging(false)}
|
||||
onDrop={onDrop}
|
||||
style={dragging ? { outline: '3px dashed var(--apg-accent)', outlineOffset: -8 } : undefined}
|
||||
>
|
||||
{objectFocus || tagFocus || personFocus ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '12px 12px 0',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
<Icon name={objectFocus ? 'search' : personFocus ? 'person-circle' : 'tag'} size={16} />
|
||||
{objectFocus
|
||||
? `Object: ${objectFocus}`
|
||||
: personFocus
|
||||
? `Person: ${personName}`
|
||||
: `Tag: ${tagFocus}`}
|
||||
<button
|
||||
type="button"
|
||||
className="apg-btn"
|
||||
style={{ marginLeft: 8, padding: '3px 10px', textTransform: 'none' }}
|
||||
onClick={() =>
|
||||
objectFocus
|
||||
? api.getState().setObjectFocus(null)
|
||||
: personFocus
|
||||
? api.getState().setPersonFocus(null)
|
||||
: api.getState().setTagFocus(null)
|
||||
}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{content}
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
multiple
|
||||
hidden
|
||||
onChange={(e) => void importFiles(e.target.files)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user