Skip to content

Commit

Permalink
Feat/detail view (#8)
Browse files Browse the repository at this point in the history
implement a first version of the detail view, which displays all data of
the work + implement an sheet overlay for places with a map display.
  • Loading branch information
oliviareichl authored Sep 25, 2024
1 parent 88de27e commit 1a98913
Show file tree
Hide file tree
Showing 28 changed files with 1,657 additions and 33 deletions.
49 changes: 49 additions & 0 deletions components/map-sidebar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { EyeIcon, MapPinIcon } from "lucide-vue-next";
const router = useRouter();
const props = defineProps<{
place: {
id: number | undefined;
name: string | undefined;
longitude: number | null | undefined;
latitude: number | null | undefined;
description: string | undefined;
};
relation: string;
}>();
function setPlaceQuery(id: number | undefined) {
if (id != null) {
void router.push({
query: { place: id },
});
}
}
</script>

<template>
<Sheet>
<SheetTrigger as-child @click="setPlaceQuery(props.place.id)">
<EyeIcon :size="16" class="text-frisch-orange" />
</SheetTrigger>
<SheetContent>
<div class="flex items-center gap-1 text-sm">
<MapPinIcon :size="16" />
{{ props.relation }}
</div>
<SheetTitle class="pb-2">{{ props.place.name }}</SheetTitle>
<SheetDescription>
<div v-if="props.place.longitude != null && props.place.latitude != null">
<Map :longitude="props.place.longitude" :latitude="props.place.latitude" />
</div>
<div class="py-2 text-base font-semibold text-black">Beschreibung</div>
<div v-if="props.place.description !== ''">
{{ props.place.description }}
</div>
<div v-else>Keine Beschreibung vorhanden.</div>
</SheetDescription>
</SheetContent>
</Sheet>
</template>
107 changes: 107 additions & 0 deletions components/map.client.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script lang="ts" setup>
import "maplibre-gl/dist/maplibre-gl.css";
import { assert } from "@acdh-oeaw/lib";
import { FullscreenControl, Map as GeoMap, NavigationControl, ScaleControl } from "maplibre-gl";
import { type GeoMapContext, geoMapContextKey } from "@/components/map.context";
const props = defineProps<{
longitude: number;
latitude: number;
}>();
const elementRef = ref<HTMLElement | null>(null);
const context: GeoMapContext = {
map: null,
};
onMounted(create);
onScopeDispose(dispose);
async function create() {
await nextTick();
assert(elementRef.value != null);
const map = new GeoMap({
center: [props.longitude, props.latitude],
container: elementRef.value,
maxZoom: 24,
minZoom: 1,
pitch: initialViewState.pitch,
style: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json", // style URL,
zoom: initialViewState.zoom,
});
context.map = map;
map.on("load", init);
}
function init() {
assert(context.map != null);
const map = context.map;
//
const nav = new NavigationControl({});
map.addControl(nav, "top-left");
//
const fullscreen = new FullscreenControl({});
map.addControl(fullscreen, "top-right");
//
const scale = new ScaleControl({});
map.addControl(scale, "bottom-left");
//
const placeId = "place-data";
map.addSource("place-data", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {},
geometry: {
type: "Point",
coordinates: [props.longitude, props.latitude],
},
},
],
},
});
//
map.addLayer({
id: "points",
type: "circle",
source: placeId,
filter: ["==", "$type", "Point"],
paint: {
"circle-color": "rgb(236 81 0)",
"circle-radius": 6,
},
});
}
function dispose() {
context.map?.remove();
}
defineExpose(context);
provide(geoMapContextKey, context);
</script>

<template>
<div class="relative grid h-80 text-black">
<div ref="elementRef" data-geo-map="true" />
<slot />
</div>
</template>
7 changes: 7 additions & 0 deletions components/map.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Map as GeoMap } from "maplibre-gl";

export interface GeoMapContext {
map: GeoMap | null;
}

export const geoMapContextKey = Symbol("geo-map-context");
99 changes: 85 additions & 14 deletions components/search-data-table/data-table.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<script lang="ts" setup>
import { type ColumnDef, FlexRender, getCoreRowModel, useVueTable } from "@tanstack/vue-table";
import {
type ColumnDef as TanStackColumnDef,
FlexRender,
getCoreRowModel,
useVueTable,
} from "@tanstack/vue-table";
import NavLink from "@/components/nav-link.vue";
import {
Table,
TableBody,
Expand All @@ -9,37 +15,74 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import type { SearchResults } from "@/types/api.ts";
const props = defineProps<{
data: SearchResults["results"];
resultsTotal: number;
}>();
const columns: Array<ColumnDef<SearchResults["results"][number]>> = [
type CustomColumnDef<T> = TanStackColumnDef<T> & {
maxWidth?: boolean;
};
const columns: Array<CustomColumnDef<SearchResults["results"][number]>> = [
{
accessorKey: "work_type",
header: () => h("div", "Typ"),
cell: ({ row }) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
const workType = row.getValue("work_type") as Array<any> | undefined;
return h("div", {}, workType?.map((type) => type.name).join(", "));
const workType = row.getValue("work_type");
if (!Array.isArray(workType) || workType.length === 0) {
return h("span", {}, "");
}
const firstWorkTypeName = workType[0]?.name;
const IconComponent = firstWorkTypeName ? getWorkIcon(firstWorkTypeName) : null;
const tooltipWrapper = h(TooltipProvider, {}, () => [
h(Tooltip, {}, () => [
h(TooltipTrigger, { class: "cursor-default" }, () => [
IconComponent
? h(IconComponent, { class: "size-4 shrink-0" })
: h("span", {}, workType.map((type) => type.name).join(", ")),
]),
h(TooltipContent, {}, () => workType.map((type) => type.name).join(", ")),
]),
]);
return h("span", {}, [
tooltipWrapper,
h("div", { class: "sr-only" }, workType.map((type) => type.name).join(", ")),
]);
},
},
{
accessorKey: "title",
header: () => h("div", "Titel"),
cell: ({ row }) => {
return h("div", row.getValue("title"));
return h(
NavLink,
{
class:
"underline decoration-dotted transition hover:no-underline focus-visible:no-underline",
href: {
path: row.original.id ? `/work/${row.original.id as unknown as string}` : "",
},
},
row.getValue("title"),
);
},
maxWidth: true, // Custom property
},
{
accessorKey: "expression_data",
header: () => h("div", "Edition"),
cell: ({ row }) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
const edition = row.getValue("expression_data") as Array<any> | undefined;
return h("div", {}, edition?.map((type) => type.edition).join(", "));
return h("div", {}, edition?.map((type) => type.edition_type).join(", "));
},
},
{
Expand All @@ -64,9 +107,19 @@ const columns: Array<ColumnDef<SearchResults["results"][number]>> = [
accessorKey: "expression_data",
header: () => h("div", "Veröffentlichungsdatum"),
cell: ({ row }) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-explicit-any
const publicationDate = row.getValue("expression_data") as Array<any> | undefined;
return h("div", {}, publicationDate?.map((type) => type.publication_date).join(", "));
const publicationData = row.getValue("expression_data");
const publicationDates = Array.isArray(publicationData)
? publicationData
.map((type) => {
const date = type.publication_date || "";
const year = date.split("-")[0] || "";
return year;
})
.join(", ")
: "";
return h("div", {}, publicationDates);
},
},
];
Expand All @@ -80,18 +133,31 @@ const table = useVueTable({
},
getCoreRowModel: getCoreRowModel(),
});
function getHeaderClass<T>(columnDef?: CustomColumnDef<T>): string {
return columnDef?.maxWidth ? "max-w-xl" : "";
}
function getCellClass<T>(columnDef?: CustomColumnDef<T>): string {
return columnDef?.maxWidth ? "max-w-xl" : "";
}
</script>

<template>
<div>
<div class="w-full rounded-md border">
<div class="w-full border">
<Table>
<TableHeader>
<TableHeader class="table-fixed">
<TableCaption class="sr-only">
<span>Suchergebnisse</span>
</TableCaption>
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<TableHead v-for="header in headerGroup.headers" :key="header.id">
<TableHead
v-for="header in headerGroup.headers"
:key="header.id"
:class="getHeaderClass(header.column.columnDef)"
class="truncate p-2 text-left"
>
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.header"
Expand All @@ -107,7 +173,12 @@ const table = useVueTable({
:key="row.id"
:data-state="row.getIsSelected() ? 'selected' : undefined"
>
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
<TableCell
v-for="cell in row.getVisibleCells()"
:key="cell.id"
class="truncate p-2"
:class="getCellClass(cell.column.columnDef)"
>
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
Expand Down
2 changes: 1 addition & 1 deletion components/ui/accordion/AccordionContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const delegatedProps = computed(() => {
v-bind="delegatedProps"
class="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
>
<div :class="cn('pt-0', props.class)">
<div :class="cn('pb-4 pt-0', props.class)">
<slot />
</div>
</AccordionContent>
Expand Down
4 changes: 2 additions & 2 deletions components/ui/accordion/AccordionTrigger.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ const delegatedProps = computed(() => {
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center pb-2 justify-between text-sm font-medium transition-all [&[data-state=open]>svg]:rotate-180',
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class,
)
"
>
<slot />
<slot name="icon">
<ChevronDownIcon
class="size-4 shrink-0 text-frisch-orange transition-transform duration-200"
class="size-4 shrink-0 text-muted-foreground transition-transform duration-200"
/>
</slot>
</AccordionTrigger>
Expand Down
19 changes: 19 additions & 0 deletions components/ui/sheet/Sheet.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
DialogRoot,
type DialogRootEmits,
type DialogRootProps,
useForwardPropsEmits,
} from "radix-vue";
const props = defineProps<DialogRootProps>();
const emits = defineEmits<DialogRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>

<template>
<DialogRoot v-bind="forwarded">
<slot />
</DialogRoot>
</template>
11 changes: 11 additions & 0 deletions components/ui/sheet/SheetClose.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogClose, type DialogCloseProps } from "radix-vue";
const props = defineProps<DialogCloseProps>();
</script>

<template>
<DialogClose v-bind="props">
<slot />
</DialogClose>
</template>
Loading

0 comments on commit 1a98913

Please sign in to comment.