Browse Source

refine calendar and maps

imwald
Silberengel 1 month ago
parent
commit
19c2275086
  1. 44
      src/components/CalendarEventNip52StructuredMeta.tsx
  2. 2
      src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx
  3. 3
      src/i18n/locales/cs.ts
  4. 3
      src/i18n/locales/de.ts
  5. 3
      src/i18n/locales/en.ts
  6. 3
      src/i18n/locales/es.ts
  7. 3
      src/i18n/locales/fr.ts
  8. 3
      src/i18n/locales/nl.ts
  9. 3
      src/i18n/locales/pl.ts
  10. 3
      src/i18n/locales/ru.ts
  11. 3
      src/i18n/locales/tr.ts
  12. 3
      src/i18n/locales/zh.ts
  13. 88
      src/pages/primary/NoteListPage/index.tsx

44
src/components/CalendarEventNip52StructuredMeta.tsx

@ -1,6 +1,5 @@
import { getNip52CalendarEventTagExtras, type CalendarEventMeta } from '@/lib/calendar-event' import { getNip52CalendarEventTagExtras, type CalendarEventMeta } from '@/lib/calendar-event'
import { useSmartNoteNavigation } from '@/PageManager' import { useSmartNoteNavigation } from '@/PageManager'
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -9,6 +8,19 @@ import { Calendar, ExternalLink, Link2, MapPin } from 'lucide-react'
type Placement = 'beforeDescription' | 'afterDescription' type Placement = 'beforeDescription' | 'afterDescription'
/** [Geohash Explorer](https://geohash.softeng.co/) style URL for a geohash string. */
function nip52GeohashSoftengUrl(geohash: string): string {
const h = geohash.trim()
return `https://geohash.softeng.co/${encodeURIComponent(h)}`
}
/** Google Maps “place” style URL from a free-text address or place name. */
function googleMapsPlaceUrl(placeQuery: string): string {
const q = placeQuery.trim()
if (!q) return '#'
return `https://www.google.com/maps/place/${encodeURIComponent(q).replace(/%20/g, '+')}`
}
export function CalendarEventNip52StructuredMeta({ export function CalendarEventNip52StructuredMeta({
placement, placement,
event, event,
@ -38,16 +50,34 @@ export function CalendarEventNip52StructuredMeta({
{meta.locations.length > 1 ? t('calendarNip52Locations') : t('calendarNip52Location')} {meta.locations.length > 1 ? t('calendarNip52Locations') : t('calendarNip52Location')}
</div> </div>
{meta.locations.length === 1 ? ( {meta.locations.length === 1 ? (
<p className="flex gap-2 text-sm leading-snug text-foreground"> <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<p className="flex min-w-0 flex-1 gap-2 text-sm leading-snug text-foreground">
<MapPin className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden /> <MapPin className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden />
<span className="min-w-0">{meta.locations[0]}</span> <span className="min-w-0">{meta.locations[0]}</span>
</p> </p>
<Button variant="outline" size="sm" className="h-8 w-full shrink-0 sm:w-auto" asChild>
<a href={googleMapsPlaceUrl(meta.locations[0])} target="_blank" rel="noopener noreferrer">
<ExternalLink className="size-3.5" />
{t('calendarNip52GoogleMaps')}
</a>
</Button>
</div>
) : ( ) : (
<ul className="min-w-0 space-y-2"> <ul className="min-w-0 space-y-3">
{meta.locations.map((loc, i) => ( {meta.locations.map((loc, i) => (
<li key={`${i}-${loc.slice(0, 24)}`} className="flex gap-2 text-sm leading-snug text-foreground"> <li key={`${i}-${loc.slice(0, 24)}`}>
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<p className="flex min-w-0 flex-1 gap-2 text-sm leading-snug text-foreground">
<MapPin className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden /> <MapPin className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden />
<span className="min-w-0">{loc}</span> <span className="min-w-0">{loc}</span>
</p>
<Button variant="outline" size="sm" className="h-8 w-full shrink-0 sm:w-auto" asChild>
<a href={googleMapsPlaceUrl(loc)} target="_blank" rel="noopener noreferrer">
<ExternalLink className="size-3.5" />
{t('calendarNip52GoogleMaps')}
</a>
</Button>
</div>
</li> </li>
))} ))}
</ul> </ul>
@ -72,11 +102,7 @@ export function CalendarEventNip52StructuredMeta({
<p className="flex flex-wrap items-center gap-2 text-sm font-mono text-foreground"> <p className="flex flex-wrap items-center gap-2 text-sm font-mono text-foreground">
<span className="break-all">{meta.geo.trim()}</span> <span className="break-all">{meta.geo.trim()}</span>
<Button variant="outline" size="sm" className="h-8 shrink-0" asChild> <Button variant="outline" size="sm" className="h-8 shrink-0" asChild>
<a <a href={nip52GeohashSoftengUrl(meta.geo)} target="_blank" rel="noopener noreferrer">
href={`https://geohash.org/${encodeURIComponent(meta.geo.trim())}`}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="size-3.5" /> <ExternalLink className="size-3.5" />
{t('calendarNip52ViewGeohash')} {t('calendarNip52ViewGeohash')}
</a> </a>

2
src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx

@ -50,7 +50,7 @@ export function ActiveRelaysTitlebarButton() {
<Button <Button
variant="ghost" variant="ghost"
size="titlebar-icon" size="titlebar-icon"
className="shrink-0 gap-0.5 text-muted-foreground hover:text-primary disabled:opacity-40 max-sm:mr-3 max-sm:pr-1" className="shrink-0 gap-0.5 text-muted-foreground hover:text-primary disabled:opacity-40"
title={t('Active relays')} title={t('Active relays')}
aria-label={t('Active relays')} aria-label={t('Active relays')}
disabled={rows.length === 0} disabled={rows.length === 0}

3
src/i18n/locales/cs.ts

@ -236,7 +236,8 @@ export default {
calendarNip52Locations: "Locations", calendarNip52Locations: "Locations",
calendarNip52Summary: "Summary", calendarNip52Summary: "Summary",
calendarNip52Geohash: "Geohash", calendarNip52Geohash: "Geohash",
calendarNip52ViewGeohash: "View on map", calendarNip52ViewGeohash: "Mapa geohash",
calendarNip52GoogleMaps: "Google Maps",
calendarNip52DayIndices: "Day indices (NIP-52)", calendarNip52DayIndices: "Day indices (NIP-52)",
calendarNip52CalendarInclusion: "Calendar inclusion", calendarNip52CalendarInclusion: "Calendar inclusion",
calendarNip52CalendarInclusionHint: calendarNip52CalendarInclusionHint:

3
src/i18n/locales/de.ts

@ -236,7 +236,8 @@ export default {
calendarNip52Locations: "Orte", calendarNip52Locations: "Orte",
calendarNip52Summary: "Kurzfassung", calendarNip52Summary: "Kurzfassung",
calendarNip52Geohash: "Geohash", calendarNip52Geohash: "Geohash",
calendarNip52ViewGeohash: "Auf Karte anzeigen", calendarNip52ViewGeohash: "Geohash-Karte",
calendarNip52GoogleMaps: "Google Maps",
calendarNip52DayIndices: "Tages-Indizes (NIP-52)", calendarNip52DayIndices: "Tages-Indizes (NIP-52)",
calendarNip52CalendarInclusion: "Kalender-Einbindung", calendarNip52CalendarInclusion: "Kalender-Einbindung",
calendarNip52CalendarInclusionHint: calendarNip52CalendarInclusionHint:

3
src/i18n/locales/en.ts

@ -240,7 +240,8 @@ export default {
calendarNip52Locations: "Locations", calendarNip52Locations: "Locations",
calendarNip52Summary: "Summary", calendarNip52Summary: "Summary",
calendarNip52Geohash: "Geohash", calendarNip52Geohash: "Geohash",
calendarNip52ViewGeohash: "View on map", calendarNip52ViewGeohash: "Geohash map",
calendarNip52GoogleMaps: "Google Maps",
calendarNip52DayIndices: "Day indices (NIP-52)", calendarNip52DayIndices: "Day indices (NIP-52)",
calendarNip52CalendarInclusion: "Calendar inclusion", calendarNip52CalendarInclusion: "Calendar inclusion",
calendarNip52CalendarInclusionHint: calendarNip52CalendarInclusionHint:

3
src/i18n/locales/es.ts

@ -236,7 +236,8 @@ export default {
calendarNip52Locations: "Locations", calendarNip52Locations: "Locations",
calendarNip52Summary: "Summary", calendarNip52Summary: "Summary",
calendarNip52Geohash: "Geohash", calendarNip52Geohash: "Geohash",
calendarNip52ViewGeohash: "View on map", calendarNip52ViewGeohash: "Mapa Geohash",
calendarNip52GoogleMaps: "Google Maps",
calendarNip52DayIndices: "Day indices (NIP-52)", calendarNip52DayIndices: "Day indices (NIP-52)",
calendarNip52CalendarInclusion: "Calendar inclusion", calendarNip52CalendarInclusion: "Calendar inclusion",
calendarNip52CalendarInclusionHint: calendarNip52CalendarInclusionHint:

3
src/i18n/locales/fr.ts

@ -236,7 +236,8 @@ export default {
calendarNip52Locations: "Locations", calendarNip52Locations: "Locations",
calendarNip52Summary: "Summary", calendarNip52Summary: "Summary",
calendarNip52Geohash: "Geohash", calendarNip52Geohash: "Geohash",
calendarNip52ViewGeohash: "View on map", calendarNip52ViewGeohash: "Carte Geohash",
calendarNip52GoogleMaps: "Google Maps",
calendarNip52DayIndices: "Day indices (NIP-52)", calendarNip52DayIndices: "Day indices (NIP-52)",
calendarNip52CalendarInclusion: "Calendar inclusion", calendarNip52CalendarInclusion: "Calendar inclusion",
calendarNip52CalendarInclusionHint: calendarNip52CalendarInclusionHint:

3
src/i18n/locales/nl.ts

@ -236,7 +236,8 @@ export default {
calendarNip52Locations: "Locations", calendarNip52Locations: "Locations",
calendarNip52Summary: "Summary", calendarNip52Summary: "Summary",
calendarNip52Geohash: "Geohash", calendarNip52Geohash: "Geohash",
calendarNip52ViewGeohash: "View on map", calendarNip52ViewGeohash: "Geohash-kaart",
calendarNip52GoogleMaps: "Google Maps",
calendarNip52DayIndices: "Day indices (NIP-52)", calendarNip52DayIndices: "Day indices (NIP-52)",
calendarNip52CalendarInclusion: "Calendar inclusion", calendarNip52CalendarInclusion: "Calendar inclusion",
calendarNip52CalendarInclusionHint: calendarNip52CalendarInclusionHint:

3
src/i18n/locales/pl.ts

@ -236,7 +236,8 @@ export default {
calendarNip52Locations: "Locations", calendarNip52Locations: "Locations",
calendarNip52Summary: "Summary", calendarNip52Summary: "Summary",
calendarNip52Geohash: "Geohash", calendarNip52Geohash: "Geohash",
calendarNip52ViewGeohash: "View on map", calendarNip52ViewGeohash: "Mapa geohash",
calendarNip52GoogleMaps: "Google Maps",
calendarNip52DayIndices: "Day indices (NIP-52)", calendarNip52DayIndices: "Day indices (NIP-52)",
calendarNip52CalendarInclusion: "Calendar inclusion", calendarNip52CalendarInclusion: "Calendar inclusion",
calendarNip52CalendarInclusionHint: calendarNip52CalendarInclusionHint:

3
src/i18n/locales/ru.ts

@ -236,7 +236,8 @@ export default {
calendarNip52Locations: "Locations", calendarNip52Locations: "Locations",
calendarNip52Summary: "Summary", calendarNip52Summary: "Summary",
calendarNip52Geohash: "Geohash", calendarNip52Geohash: "Geohash",
calendarNip52ViewGeohash: "View on map", calendarNip52ViewGeohash: "Карта геохэша",
calendarNip52GoogleMaps: "Google Maps",
calendarNip52DayIndices: "Day indices (NIP-52)", calendarNip52DayIndices: "Day indices (NIP-52)",
calendarNip52CalendarInclusion: "Calendar inclusion", calendarNip52CalendarInclusion: "Calendar inclusion",
calendarNip52CalendarInclusionHint: calendarNip52CalendarInclusionHint:

3
src/i18n/locales/tr.ts

@ -236,7 +236,8 @@ export default {
calendarNip52Locations: "Locations", calendarNip52Locations: "Locations",
calendarNip52Summary: "Summary", calendarNip52Summary: "Summary",
calendarNip52Geohash: "Geohash", calendarNip52Geohash: "Geohash",
calendarNip52ViewGeohash: "View on map", calendarNip52ViewGeohash: "Geohash haritası",
calendarNip52GoogleMaps: "Google Maps",
calendarNip52DayIndices: "Day indices (NIP-52)", calendarNip52DayIndices: "Day indices (NIP-52)",
calendarNip52CalendarInclusion: "Calendar inclusion", calendarNip52CalendarInclusion: "Calendar inclusion",
calendarNip52CalendarInclusionHint: calendarNip52CalendarInclusionHint:

3
src/i18n/locales/zh.ts

@ -236,7 +236,8 @@ export default {
calendarNip52Locations: "Locations", calendarNip52Locations: "Locations",
calendarNip52Summary: "Summary", calendarNip52Summary: "Summary",
calendarNip52Geohash: "Geohash", calendarNip52Geohash: "Geohash",
calendarNip52ViewGeohash: "View on map", calendarNip52ViewGeohash: "Geohash 地图",
calendarNip52GoogleMaps: "Google Maps",
calendarNip52DayIndices: "Day indices (NIP-52)", calendarNip52DayIndices: "Day indices (NIP-52)",
calendarNip52CalendarInclusion: "Calendar inclusion", calendarNip52CalendarInclusion: "Calendar inclusion",
calendarNip52CalendarInclusionHint: calendarNip52CalendarInclusionHint:

88
src/pages/primary/NoteListPage/index.tsx

@ -1,4 +1,3 @@
import RelayInfo from '@/components/RelayInfo'
import { RefreshButton } from '@/components/RefreshButton' import { RefreshButton } from '@/components/RefreshButton'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
@ -9,11 +8,9 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
import type { TNoteListRef } from '@/components/NoteList' import type { TNoteListRef } from '@/components/NoteList'
import { NoteCardLoadingSkeleton } from '@/components/NoteCard' import { NoteCardLoadingSkeleton } from '@/components/NoteCard'
import { TPageRef } from '@/types' import { TPageRef } from '@/types'
import { Compass, Info, Star, UsersRound } from 'lucide-react' import { Calendar, Compass, Star, UsersRound } from 'lucide-react'
import React, { import React, {
Dispatch,
forwardRef, forwardRef,
SetStateAction,
useCallback, useCallback,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
@ -37,7 +34,6 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => {
const feedRef = useRef<TNoteListRef>(null) const feedRef = useRef<TNoteListRef>(null)
const { feedInfo, relayUrls, isReady } = useFeed() const { feedInfo, relayUrls, isReady } = useFeed()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const [showRelayDetails, setShowRelayDetails] = useState(false)
const [homeSubHeader, setHomeSubHeader] = useState<React.ReactNode>(null) const [homeSubHeader, setHomeSubHeader] = useState<React.ReactNode>(null)
const usesSubHeader = const usesSubHeader =
@ -100,9 +96,6 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => {
} else { } else {
content = ( content = (
<> <>
{showRelayDetails && feedInfo.feedType === 'relay' && !!feedInfo.id && (
<RelayInfo url={feedInfo.id!} className="mb-2 pt-3" />
)}
<RelaysFeed <RelaysFeed
ref={feedRef} ref={feedRef}
setSubHeader={setHomeSubHeaderStable} setSubHeader={setHomeSubHeaderStable}
@ -150,15 +143,7 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => {
suppressMobileDefaultActiveRelaysButton suppressMobileDefaultActiveRelaysButton
titlebar={ titlebar={
showNoteListTitlebar ? ( showNoteListTitlebar ? (
<NoteListPageTitlebar <NoteListPageTitlebar onFeedRefresh={runFeedRefresh} showTitlebarRefresh={!usesSubHeader} />
layoutRef={layoutRef}
onFeedRefresh={runFeedRefresh}
showTitlebarRefresh={!usesSubHeader}
showRelayDetails={showRelayDetails}
setShowRelayDetails={
feedInfo.feedType === 'relay' && !!feedInfo.id ? setShowRelayDetails : undefined
}
/>
) : null ) : null
} }
subHeader={subHeader} subHeader={subHeader}
@ -174,17 +159,11 @@ NoteListPage.displayName = 'NoteListPage'
export default NoteListPage export default NoteListPage
function NoteListPageTitlebar({ function NoteListPageTitlebar({
layoutRef,
onFeedRefresh, onFeedRefresh,
showTitlebarRefresh, showTitlebarRefresh
showRelayDetails,
setShowRelayDetails
}: { }: {
layoutRef?: React.RefObject<TPageRef>
onFeedRefresh: () => void onFeedRefresh: () => void
showTitlebarRefresh: boolean showTitlebarRefresh: boolean
showRelayDetails?: boolean
setShowRelayDetails?: Dispatch<SetStateAction<boolean>>
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
@ -196,18 +175,29 @@ function NoteListPageTitlebar({
const followsLatestActive = display && current === 'follows-latest' && primaryViewType === null const followsLatestActive = display && current === 'follows-latest' && primaryViewType === null
const favoritesActive = const favoritesActive =
display && current === 'spells' && spell === 'favorites' && primaryViewType === null display && current === 'spells' && spell === 'favorites' && primaryViewType === null
const calendarActive = display && current === 'calendar' && primaryViewType === null
if (!isSmallScreen) {
return ( return (
<div className="relative flex gap-1 items-center h-full justify-between"> <div className="flex h-full w-full min-w-0 items-center justify-end gap-1 pr-1">
<div className="flex min-w-0 flex-1 items-center gap-1 h-full pl-1 sm:pl-3"> {showTitlebarRefresh ? <RefreshButton onClick={onFeedRefresh} /> : null}
{isSmallScreen && ( </div>
<> )
}
/**
* Mobile: avoid absolutely centered logo (overlaps side controls on narrow widths). Three columns
* left/right hug content; center flexes so the banner shrinks. Overflow columns scroll if needed.
*/
return (
<div className="grid h-full w-full min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-0.5 sm:gap-x-1">
<div className="flex min-h-0 min-w-0 items-center justify-start gap-0.5 overflow-x-auto overflow-y-hidden scrollbar-hide sm:gap-1">
<Button <Button
variant="ghost" variant="ghost"
size="titlebar-icon" size="titlebar-icon"
title={t('Explore')} title={t('Explore')}
aria-label={t('Explore')} aria-label={t('Explore')}
className={exploreActive ? 'bg-accent/50' : ''} className={`shrink-0 ${exploreActive ? 'bg-accent/50' : ''}`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (primaryViewType !== null) { if (primaryViewType !== null) {
@ -226,7 +216,7 @@ function NoteListPageTitlebar({
size="titlebar-icon" size="titlebar-icon"
title={t('Follows latest nav label')} title={t('Follows latest nav label')}
aria-label={t('Follows latest nav label')} aria-label={t('Follows latest nav label')}
className={followsLatestActive ? 'bg-accent/50' : ''} className={`shrink-0 ${followsLatestActive ? 'bg-accent/50' : ''}`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (primaryViewType !== null) { if (primaryViewType !== null) {
@ -242,7 +232,7 @@ function NoteListPageTitlebar({
size="titlebar-icon" size="titlebar-icon"
title={t('Favorites')} title={t('Favorites')}
aria-label={t('Favorites')} aria-label={t('Favorites')}
className={favoritesActive ? 'bg-accent/50' : ''} className={`shrink-0 ${favoritesActive ? 'bg-accent/50' : ''}`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (primaryViewType !== null) { if (primaryViewType !== null) {
@ -255,14 +245,11 @@ function NoteListPageTitlebar({
</Button> </Button>
</> </>
) : null} ) : null}
</>
)}
</div> </div>
{isSmallScreen && ( <div className="flex min-h-0 min-w-0 items-center justify-center gap-0.5 px-0.5">
<div className="absolute left-1/2 z-10 -translate-x-1/2 transform">
<button <button
type="button" type="button"
className="flex max-h-10 max-w-[min(72vw,14rem)] cursor-pointer items-center justify-center overflow-hidden rounded-xl bg-card px-1.5 ring-1 ring-border/50" className="flex min-h-8 min-w-0 max-w-full flex-1 cursor-pointer items-center justify-center overflow-hidden rounded-xl bg-card px-1 py-0.5 ring-1 ring-border/50 sm:px-1.5"
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
@ -270,35 +257,30 @@ function NoteListPageTitlebar({
}} }}
aria-label="Imwald" aria-label="Imwald"
> >
<Logo className="max-h-8 w-full object-contain object-center" /> <Logo className="max-h-7 w-full min-w-0 object-contain object-center sm:max-h-8" />
</button> </button>
</div>
)}
<div className="shrink-0 flex gap-1 items-center">
{showTitlebarRefresh && <RefreshButton onClick={onFeedRefresh} />}
{setShowRelayDetails && (
<Button <Button
variant="ghost" variant="ghost"
size="titlebar-icon" size="titlebar-icon"
className={`shrink-0 ${calendarActive ? 'bg-accent/50' : ''}`}
title={t('Calendar')}
aria-label={t('Calendar')}
onClick={(e) => { onClick={(e) => {
e.preventDefault()
e.stopPropagation() e.stopPropagation()
setShowRelayDetails((show) => !show) if (primaryViewType !== null) {
setPrimaryNoteView(null)
if (!showRelayDetails) {
layoutRef?.current?.scrollToTop('smooth')
} }
navigate('calendar')
}} }}
className={showRelayDetails ? 'bg-accent/50' : ''}
> >
<Info /> <Calendar />
</Button> </Button>
)} </div>
{isSmallScreen ? ( <div className="flex min-h-0 min-w-0 items-center justify-end gap-0.5 overflow-x-auto overflow-y-hidden scrollbar-hide sm:gap-1">
<> {showTitlebarRefresh ? <RefreshButton onClick={onFeedRefresh} /> : null}
<ActiveRelaysTitlebarButton /> <ActiveRelaysTitlebarButton />
<HelpAndAccountMenu variant="titlebar" /> <HelpAndAccountMenu variant="titlebar" />
</>
) : null}
</div> </div>
</div> </div>
) )

Loading…
Cancel
Save