Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
517 changes: 396 additions & 121 deletions frontend/src/components/EarthTwin.tsx

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions frontend/src/components/GlobeBookmarks/BookmarkCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import type { Bookmark } from '../../hooks/useBookmarkStorage';
import { MaterialIcon } from '../MaterialIcon';

interface BookmarkCardProps {
bookmark: Bookmark;
onOpen: (bookmark: Bookmark) => void;
onEdit: (bookmark: Bookmark) => void;
onDelete: (bookmark: Bookmark) => void;
onToggleFavorite: (bookmark: Bookmark) => void;
onShare: (bookmark: Bookmark) => void;
}

export function BookmarkCard({
bookmark,
onOpen,
onShare,
onEdit,
onDelete,
onToggleFavorite,
}: BookmarkCardProps) {
const lastOpenedLabel = bookmark.lastOpenedAt
? new Intl.DateTimeFormat('en-IN', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(bookmark.lastOpenedAt))
: 'Not opened yet';
Comment thread
khedkaravani-rgb marked this conversation as resolved.

return (
<article className="border border-border-panel/70 bg-surface-container/35 p-3 transition-ui hover:border-primary-container/70 hover:bg-surface-container/60">
<div className="flex items-start justify-between gap-3">
<button
type="button"
onClick={() => onOpen(bookmark)}
className="min-w-0 flex-1 text-left"
aria-label={`Open bookmark: ${bookmark.name}`}
>
<div className="flex items-center gap-2">
<MaterialIcon
name={bookmark.isFavorite ? 'star' : 'public'}
className={
bookmark.isFavorite
? 'text-status-warning text-sm'
: 'text-primary-container text-sm'
}
/>

<h3 className="truncate font-technical-data text-sm font-bold text-on-surface">
{bookmark.name}
</h3>
</div>

<p className="mt-1 font-label-caps text-[9px] tracking-wider text-primary-container/75">
{bookmark.category}
{bookmark.isDefault ? ' · DEFAULT VIEW' : ''}
</p>

{bookmark.description && (
<p className="mt-2 line-clamp-2 font-technical-data text-xs text-on-surface-variant">
{bookmark.description}
</p>
)}
</button>

<button
type="button"
onClick={() => onToggleFavorite(bookmark)}
aria-label={
bookmark.isFavorite
? `Remove ${bookmark.name} from favorites`
: `Add ${bookmark.name} to favorites`
}
aria-pressed={bookmark.isFavorite}
className="shrink-0 border border-border-panel p-1.5 text-on-surface-variant transition-ui hover:border-status-warning hover:text-status-warning"
>
<MaterialIcon
name={bookmark.isFavorite ? 'star' : 'star_outline'}
className="text-base"
/>
</button>
</div>

<div className="mt-3 flex items-center justify-between gap-3 border-t border-border-panel/50 pt-3">
<span className="font-technical-data text-[9px] text-on-surface-variant/70">
{lastOpenedLabel}
</span>

<div className="flex items-center gap-1">
<button
type="button"
onClick={() => onOpen(bookmark)}
className="border border-primary-container/60 px-2 py-1 font-technical-data text-[10px] font-bold text-primary-container transition-ui hover:bg-primary-container hover:text-bg-deep-space"
aria-label={`Open ${bookmark.name}`}
>
OPEN
</button>

<button
type="button"
onClick={() => onShare(bookmark)}
className="border border-border-panel px-2 py-1 text-on-surface-variant transition-ui hover:border-primary-container hover:text-primary-container"
aria-label={`Copy a share link for ${bookmark.name}`}
>
<MaterialIcon name="share" className="text-sm" />
</button>

{!bookmark.isDefault && (
<>

<button
type="button"
onClick={() => onEdit(bookmark)}
className="border border-border-panel px-2 py-1 text-on-surface-variant transition-ui hover:border-primary-container hover:text-primary-container"
aria-label={`Edit ${bookmark.name}`}
>
<MaterialIcon name="edit" className="text-sm" />
</button>

<button
type="button"
onClick={() => onDelete(bookmark)}
className="border border-border-panel px-2 py-1 text-on-surface-variant transition-ui hover:border-status-emergency hover:text-status-emergency"
aria-label={`Delete ${bookmark.name}`}
>
<MaterialIcon name="delete" className="text-sm" />
</button>
</>
)}
</div>
</div>
</article>
);
}
204 changes: 204 additions & 0 deletions frontend/src/components/GlobeBookmarks/BookmarkModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { useState } from 'react';
import type { Bookmark } from '../../hooks/useBookmarkStorage';

export interface BookmarkFormValues {
name: string;
description: string;
category: string;
}

interface BookmarkModalProps {
initialBookmark?: Bookmark | null;
onClose: () => void;
onSubmit: (values: BookmarkFormValues) => void;
}

function getInitialValues(
bookmark?: Bookmark | null
): BookmarkFormValues {
return {
name: bookmark?.name ?? '',
description: bookmark?.description ?? '',
category: bookmark?.category ?? 'Custom',
};
}

export function BookmarkModal({
initialBookmark,
onClose,
onSubmit,
}: BookmarkModalProps) {
const [values, setValues] = useState<BookmarkFormValues>(() =>
getInitialValues(initialBookmark)
);

const isEditing = Boolean(initialBookmark);

const handleSubmit = (
event: React.FormEvent<HTMLFormElement>
) => {
event.preventDefault();

const name = values.name.trim();

if (!name) {
return;
}

onSubmit({
name,
description: values.description.trim(),
category: values.category.trim() || 'Custom',
});
};

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm"
role="presentation"
onMouseDown={(event) => {
if (event.target === event.currentTarget) {
onClose();
}
}}
>
<section
role="dialog"
aria-modal="true"
aria-labelledby="bookmark-modal-title"
className="w-full max-w-md border border-primary-container/40 bg-bg-deep-space/95 p-5 shadow-[0_0_40px_rgba(0,229,255,0.15)] backdrop-blur-xl"
onKeyDown={(event) => {
if (event.key === 'Escape') {
onClose();
}
}}
>
<div className="mb-5 flex items-start justify-between gap-4">
<div>
<p className="font-label-caps text-[10px] tracking-widest text-primary-container/80">
GLOBE BOOKMARK
</p>

<h2
id="bookmark-modal-title"
className="mt-1 font-display-lg text-lg font-bold text-on-surface"
>
{isEditing
? 'Edit Saved View'
: 'Save Current View'}
</h2>
</div>

<button
type="button"
onClick={onClose}
aria-label="Close bookmark dialog"
className="border border-border-panel px-2 py-1 text-on-surface-variant transition-ui hover:border-primary-container hover:text-primary-container"
>
×
</button>
</div>

<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label
htmlFor="bookmark-name"
className="mb-1.5 block font-technical-data text-xs text-on-surface"
>
Name <span className="text-status-emergency">*</span>
</label>

<input
id="bookmark-name"
autoFocus
required
maxLength={80}
value={values.name}
onChange={(event) =>
setValues((current) => ({
...current,
name: event.target.value,
}))
}
placeholder="Example: India monitoring view"
className="w-full border border-border-panel bg-surface-container/40 px-3 py-2 font-technical-data text-sm text-on-surface outline-none transition-ui placeholder:text-on-surface-variant/50 focus:border-primary-container"
/>
</div>

<div>
<label
htmlFor="bookmark-description"
className="mb-1.5 block font-technical-data text-xs text-on-surface"
>
Description <span className="text-on-surface-variant/60">(optional)</span>
</label>

<textarea
id="bookmark-description"
rows={3}
maxLength={240}
value={values.description}
onChange={(event) =>
setValues((current) => ({
...current,
description: event.target.value,
}))
}
placeholder="Add context for this saved view."
className="w-full resize-none border border-border-panel bg-surface-container/40 px-3 py-2 font-technical-data text-sm text-on-surface outline-none transition-ui placeholder:text-on-surface-variant/50 focus:border-primary-container"
/>
</div>

<div>
<label
htmlFor="bookmark-category"
className="mb-1.5 block font-technical-data text-xs text-on-surface"
>
Category
</label>

<input
id="bookmark-category"
list="bookmark-category-options"
maxLength={40}
value={values.category}
onChange={(event) =>
setValues((current) => ({
...current,
category: event.target.value,
}))
}
placeholder="Custom"
className="w-full border border-border-panel bg-surface-container/40 px-3 py-2 font-technical-data text-sm text-on-surface outline-none transition-ui placeholder:text-on-surface-variant/50 focus:border-primary-container"
/>

<datalist id="bookmark-category-options">
<option value="Custom" />
<option value="Region" />
<option value="Satellite" />
<option value="Orbit" />
<option value="Mission" />
</datalist>
</div>

<div className="flex justify-end gap-2 border-t border-border-panel/60 pt-5">
<button
type="button"
onClick={onClose}
className="border border-border-panel px-4 py-2 font-technical-data text-xs font-bold text-on-surface-variant transition-ui hover:border-on-surface hover:text-on-surface"
>
CANCEL
</button>

<button
type="submit"
className="border border-primary-container bg-primary-container px-4 py-2 font-technical-data text-xs font-bold text-bg-deep-space transition-ui hover:brightness-110"
>
{isEditing ? 'SAVE CHANGES' : 'SAVE BOOKMARK'}
</button>
</div>
</form>
</section>
</div>
);
}
29 changes: 29 additions & 0 deletions frontend/src/components/GlobeBookmarks/BookmarkSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { MaterialIcon } from '../MaterialIcon';

interface BookmarkSearchProps {
value: string;
onChange: (value: string) => void;
}

export function BookmarkSearch({
value,
onChange,
}: BookmarkSearchProps) {
return (
<div className="relative">
<MaterialIcon
name="search"
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-on-surface-variant/60"
/>

<input
type="search"
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder="Search saved views..."
aria-label="Search bookmarks by name, description, or category"
className="w-full border border-border-panel bg-surface-container/40 py-2 pl-9 pr-3 font-technical-data text-sm text-on-surface outline-none placeholder:text-on-surface-variant/50 focus:border-primary-container"
/>
</div>
);
}
Loading
Loading