convert frontend to workspace
This commit is contained in:
parent
7f03e9ef13
commit
c223f2f7af
42 changed files with 108 additions and 64 deletions
245
projects/frontend/src/views/gallery/gallery-list.vue
Normal file
245
projects/frontend/src/views/gallery/gallery-list.vue
Normal file
|
@ -0,0 +1,245 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { type RouteRecordRaw, useRouter } from 'vue-router'
|
||||
|
||||
import type { GalleryDisplayedEntries, GalleryList } from './gallery'
|
||||
import { type GalleryListDefinition } from 'content/routes.js'
|
||||
import { amendVariantsWithDefaults } from './gallery-utilities'
|
||||
import { fetchAndParseYaml, storage } from 'src/utilities/fetch'
|
||||
import { getCurrentRoute } from 'src/utilities/vuetils'
|
||||
import { useRouteStore } from 'src/routes'
|
||||
|
||||
import GalleryTile from './gallery-tile.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
variants: string[]
|
||||
}>()
|
||||
|
||||
/**
|
||||
* Wraps around the path of variant ids to ensure there are not any issues
|
||||
* @param variants the array of variant ids
|
||||
*/
|
||||
const validateVariantPath = (variants?: string[]) => {
|
||||
return !!variants && variants[0] !== '' ? variants : null
|
||||
}
|
||||
|
||||
const currentRoute = getCurrentRoute()
|
||||
const routeStore = useRouteStore()
|
||||
const routeConfig = routeStore._routes[currentRoute.path] as GalleryListDefinition & RouteRecordRaw
|
||||
const globalConfig = routeStore._globals
|
||||
const storageId = `${globalConfig.id}`
|
||||
const router = useRouter()
|
||||
let config: GalleryList = null!
|
||||
let tagsToggled: string[] = []
|
||||
|
||||
const ready = ref(false)
|
||||
const galleryReady = ref(false)
|
||||
const entries = ref({} as GalleryDisplayedEntries)
|
||||
const variants = ref(validateVariantPath(props.variants))
|
||||
const hasWarnings = ref(false)
|
||||
const hideWarnings = defineModel('showWarnings', { type: Boolean })
|
||||
const tagsByCategory = ref({} as { [category: string]: Record<string, string> })
|
||||
|
||||
/**
|
||||
* Handles updating the displayed tags in the list
|
||||
*/
|
||||
const onLoadTags = () => {
|
||||
if (!config.tags) {
|
||||
tagsByCategory.value = null as any
|
||||
} else {
|
||||
tagsByCategory.value = { 'NoCategory': {}}
|
||||
Object.keys(config.tags).forEach(id => {
|
||||
const tag = config.tags[id]
|
||||
if (!!tag.category) {
|
||||
if (!tagsByCategory.value[tag.category]) {
|
||||
tagsByCategory.value[tag.category] = {}
|
||||
}
|
||||
tagsByCategory.value[tag.category][id] = tag.displayName || id
|
||||
} else {
|
||||
tagsByCategory.value['NoCategory'][id] = tag.displayName || id
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles updating the displayed entries in the list
|
||||
*/
|
||||
const onDisplayEntries = () => {
|
||||
resetTags()
|
||||
galleryReady.value = false
|
||||
let currentEntries = config.entries
|
||||
if (!!variants.value) {
|
||||
variants.value.forEach((variant) => {
|
||||
currentEntries = amendVariantsWithDefaults(currentEntries[variant])!
|
||||
})
|
||||
}
|
||||
entries.value = currentEntries
|
||||
hasWarnings.value = !!Object.values(entries.value).find(other => !!other.warning)
|
||||
setTimeout(() => galleryReady.value = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the tile clicked event;
|
||||
* handles navigating to the gallery-view route to display the entry selected
|
||||
* @param clickEvent.event the event context which invoked this handler
|
||||
* @param clickEvent.id the id of the tile that was clicked
|
||||
*/
|
||||
const onTileClicked = (clickEvent: { event: Event, id: string }) => {
|
||||
const { event, id } = clickEvent
|
||||
const entry = entries.value[id]
|
||||
|
||||
event.preventDefault()
|
||||
if (!!entry.variants) {
|
||||
const newPath = !!variants.value
|
||||
? `${(variants.value || []).join(';')};${id}`
|
||||
: id
|
||||
router.push({ name: routeConfig.name, query: { v: newPath }})
|
||||
variants.value = newPath.split(';')
|
||||
onDisplayEntries()
|
||||
} else if (!!variants.value) {
|
||||
router.push({ name: `${routeConfig.name?.toString()}: View Entry`, query: { v: `${(variants.value || []).join(';')};${id}` }})
|
||||
} else {
|
||||
router.push({ name: `${routeConfig.name?.toString()}: View Entry`, query: { v: `${id}` }})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the back button which appears when navigated into entry variants;
|
||||
* navigates upward one level from the currently displayed variants
|
||||
*/
|
||||
const onNavigateBack = (event: Event) => {
|
||||
event.preventDefault()
|
||||
let newPath: string | null = variants.value!.slice(0, variants.value!.length - 1).join(';')
|
||||
if (newPath === '') {
|
||||
router.push({ name: routeConfig.name})
|
||||
variants.value = null
|
||||
} else {
|
||||
router.push({ name: routeConfig.name, query: { v: newPath }})
|
||||
variants.value = validateVariantPath(newPath?.split(';'))
|
||||
}
|
||||
onDisplayEntries()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the toggle for hiding/showing warnings;
|
||||
* updates localstorage with the state of the checkbox
|
||||
* so that it is saved between page loads
|
||||
* @param event the event context which invoked this handler
|
||||
*/
|
||||
const onHideWarningsToggled = (event: Event) => {
|
||||
storage.write(`${storageId}::hideWarnings`, (event.target as HTMLInputElement).checked)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for a tag being selected;
|
||||
* updates the visibility state of the current entries
|
||||
* @param event the event context which invoked this handler
|
||||
* @param tagId: the id of the tag
|
||||
*/
|
||||
const onToggleTag = (event: Event, tagId: string) => {
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
tagsToggled.push(tagId)
|
||||
} else {
|
||||
const index = tagsToggled.indexOf(tagId)
|
||||
if (index > -1) {
|
||||
tagsToggled.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (tagsToggled.length < 1) {
|
||||
Object.keys(entries.value).forEach(entryId => {
|
||||
entries.value[entryId].hidden = false
|
||||
})
|
||||
} else {
|
||||
Object.keys(entries.value).forEach(entryId => {
|
||||
entries.value[entryId].hidden = !entries.value[entryId].tags?.some(own => tagsToggled.includes(own))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the `hidden` state of entries
|
||||
* and the `tagsToggled` array
|
||||
*/
|
||||
const resetTags = () => {
|
||||
Object.keys(entries.value).forEach(entryId => {
|
||||
entries.value[entryId].hidden = false
|
||||
})
|
||||
tagsToggled = []
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
ready.value = false
|
||||
config = await fetchAndParseYaml<GalleryList>(routeConfig.config)
|
||||
document.title = routeConfig.title
|
||||
hideWarnings.value = storage.read(`${storageId}::hideWarnings`) || false
|
||||
onLoadTags()
|
||||
onDisplayEntries()
|
||||
ready.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.template.gallery-list
|
||||
.navigation(
|
||||
v-if='ready'
|
||||
)
|
||||
button.input(
|
||||
v-if='variants?.length > 0'
|
||||
@click='onNavigateBack($event)'
|
||||
)
|
||||
span Back
|
||||
.input.labeled-checkbox(
|
||||
v-if='hasWarnings'
|
||||
)
|
||||
label(
|
||||
for='warning-toggle-checkbox'
|
||||
) Hide Warnings
|
||||
input(
|
||||
type='checkbox'
|
||||
name='warning-toggle-checkbox'
|
||||
id='warning-toggle-checkbox'
|
||||
v-model='hideWarnings'
|
||||
@input='onHideWarningsToggled($event)'
|
||||
)
|
||||
Transition
|
||||
.filters(
|
||||
v-if='galleryReady && !!tagsByCategory'
|
||||
)
|
||||
h2 Filters
|
||||
.category(
|
||||
v-for='(tags, category) in tagsByCategory'
|
||||
:id='category'
|
||||
)
|
||||
h3(
|
||||
v-if='category !== "NoCategory"'
|
||||
) {{ category }}
|
||||
.input.labeled-checkbox(
|
||||
v-for='(tagDisplayName, tagId) in tags'
|
||||
:id='tagId'
|
||||
)
|
||||
label(
|
||||
:for='`${tagId}-toggle`'
|
||||
) {{ tagDisplayName }}
|
||||
input(
|
||||
type='checkbox'
|
||||
:name='`${tagId}-toggle`'
|
||||
:id='`${tagId}-toggle`'
|
||||
@input='onToggleTag($event, tagId)'
|
||||
)
|
||||
Transition
|
||||
.gallery(
|
||||
v-if='galleryReady'
|
||||
)
|
||||
Transition(
|
||||
v-for='(entry, id) in entries'
|
||||
)
|
||||
GalleryTile(
|
||||
v-if='!entry.hidden'
|
||||
:entry='entry'
|
||||
:id='id'
|
||||
:hideWarnings='hideWarnings'
|
||||
@click='onTileClicked($event)'
|
||||
)
|
||||
</template>
|
52
projects/frontend/src/views/gallery/gallery-tile.vue
Normal file
52
projects/frontend/src/views/gallery/gallery-tile.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { GalleryEntry } from './gallery'
|
||||
import { getTitleFromEntryOrId } from './gallery-utilities'
|
||||
|
||||
const props = defineProps<{
|
||||
entry: GalleryEntry,
|
||||
id: string,
|
||||
hideWarnings: boolean,
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'click', value: { event: Event, id: string }): void
|
||||
}>()
|
||||
|
||||
const title = computed(() => getTitleFromEntryOrId(props.entry, props.id))
|
||||
const styles = computed(() => {
|
||||
const stylesReturn: Record<string, string> = {}
|
||||
if (!!props.entry.thumbnailBackground) {
|
||||
stylesReturn.background = props.entry.thumbnailBackground
|
||||
}
|
||||
stylesReturn['object-position'] = props.entry.thumbnailPosition || 'center center'
|
||||
return stylesReturn
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.gallery-embed(
|
||||
@click='$emit("click", { event: $event, id })'
|
||||
)
|
||||
.image-wrapper(
|
||||
:class='{ warning: !!entry.warning && !hideWarnings }'
|
||||
)
|
||||
img(
|
||||
:src='entry.thumbnailUrl || entry.url'
|
||||
:alt='entry.description || id'
|
||||
:style='styles'
|
||||
)
|
||||
.caption-wrapper
|
||||
p.variants(
|
||||
v-if='!!entry.variants'
|
||||
) has variants
|
||||
p {{ title }}
|
||||
p.warning(
|
||||
v-if='!!entry.warning'
|
||||
) {{ entry.warning }}
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
49
projects/frontend/src/views/gallery/gallery-utilities.ts
Normal file
49
projects/frontend/src/views/gallery/gallery-utilities.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { deepCopy } from 'src/utilities/dom'
|
||||
|
||||
import type { GalleryEntry, GalleryEntryInheritedProperties } from './gallery'
|
||||
|
||||
export const getTitleFromEntryOrId = (entry: GalleryEntry, id: string) => (
|
||||
entry.title !== undefined
|
||||
? entry.title === '' || entry.title === null
|
||||
? 'untitled'
|
||||
: entry.title
|
||||
: id
|
||||
)
|
||||
|
||||
export const amendVariantsWithDefaults = (entry: GalleryEntry) => {
|
||||
const variants = deepCopy(entry.variants)
|
||||
if (!!variants) {
|
||||
Object.keys(variants).forEach(id => _amendVariantWithDefaults(entry, variants[id]))
|
||||
}
|
||||
|
||||
return variants
|
||||
}
|
||||
|
||||
export const _amendVariantWithDefaults = (parent: GalleryEntryInheritedProperties, variant: GalleryEntryInheritedProperties) => {
|
||||
if (variant.title === undefined && (!!parent.title || parent.title === null || parent.title === '')) {
|
||||
variant.title = parent.title
|
||||
}
|
||||
if (!variant.description && !!parent.description) {
|
||||
variant.description = parent.description
|
||||
}
|
||||
if (variant.warning === undefined && !!parent.warning) {
|
||||
variant.warning = parent.warning
|
||||
}
|
||||
if (!variant.fields && !!parent.fields) {
|
||||
variant.fields = parent.fields
|
||||
}
|
||||
if (!variant.tags && !!parent.tags) {
|
||||
variant.tags = parent.tags
|
||||
}
|
||||
if (!variant.thumbnailPosition && !!parent.thumbnailPosition) {
|
||||
variant.thumbnailPosition = parent.thumbnailPosition
|
||||
}
|
||||
if (!variant.thumbnailBackground && !!parent.thumbnailBackground) {
|
||||
variant.thumbnailBackground = parent.thumbnailBackground
|
||||
}
|
||||
if (!variant.background && !!parent.background) {
|
||||
variant.background = parent.background
|
||||
}
|
||||
|
||||
return variant
|
||||
}
|
79
projects/frontend/src/views/gallery/gallery-view.vue
Normal file
79
projects/frontend/src/views/gallery/gallery-view.vue
Normal file
|
@ -0,0 +1,79 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import type { GalleryEntry, GalleryList } from './gallery'
|
||||
import { type GalleryListDefinition } from 'content/routes.js'
|
||||
import { amendVariantsWithDefaults, getTitleFromEntryOrId } from './gallery-utilities'
|
||||
import { fetchAndParseYaml } from 'src/utilities/fetch'
|
||||
import { getCurrentRoute } from 'src/utilities/vuetils'
|
||||
import { useRouteStore } from 'src/routes'
|
||||
|
||||
import EmbedableContent from 'src/components/shared/embedable-content.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
variants: string[]
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'loaded'): void
|
||||
}>()
|
||||
|
||||
const ready = ref(false)
|
||||
const config = ref(null! as GalleryList)
|
||||
const entry = ref(null! as GalleryEntry)
|
||||
const id = ref('')
|
||||
const currentRoute = getCurrentRoute()
|
||||
const routeStore = useRouteStore()
|
||||
const routeConfig = routeStore._routes[currentRoute.path.substring(0, currentRoute.path.length - 5)] as GalleryListDefinition
|
||||
const routeSubConfig = routeStore._routes[currentRoute.path]
|
||||
const title = ref('')
|
||||
const styles = computed(() => {
|
||||
const stylesReturn: Record<string, string> = {}
|
||||
if (!!entry.value.background) {
|
||||
stylesReturn.background = entry.value.background
|
||||
}
|
||||
return stylesReturn
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
config.value = await fetchAndParseYaml<GalleryList>(routeConfig.config)
|
||||
let currentEntries = config.value.entries
|
||||
for (let i = 0; i < props.variants.length - 1; ++i) {
|
||||
currentEntries = amendVariantsWithDefaults(currentEntries[props.variants[i]])!
|
||||
}
|
||||
entry.value = currentEntries[props.variants[props.variants.length - 1]]
|
||||
id.value = props.variants[props.variants.length - 1]
|
||||
title.value = getTitleFromEntryOrId(entry.value, id.value)
|
||||
document.title = routeSubConfig.title.replace('$ENTRY', title.value)
|
||||
ready.value = true
|
||||
emits('loaded')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.template.gallery-view
|
||||
.view-wrapper(
|
||||
v-if='ready'
|
||||
)
|
||||
.view-outlet
|
||||
img(
|
||||
:src='entry.url || entry.thumbnailUrl'
|
||||
:style='styles'
|
||||
)
|
||||
.view-content
|
||||
p {{ title }}
|
||||
dl.info
|
||||
.info-entry(
|
||||
v-for='(field, key) in entry.fields'
|
||||
:class='key'
|
||||
)
|
||||
dt {{ key }}
|
||||
dd {{ field }}
|
||||
EmbedableContent(
|
||||
:content='entry.description'
|
||||
)
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
137
projects/frontend/src/views/gallery/gallery.d.ts
vendored
Normal file
137
projects/frontend/src/views/gallery/gallery.d.ts
vendored
Normal file
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
* A partial definition of a {@link GalleryEntry}
|
||||
* which defines the properties of an entry
|
||||
* that can be passed down from a top-level entry down to its variants
|
||||
*/
|
||||
export type GalleryEntryInheritedProperties = {
|
||||
/**
|
||||
* css background applied to the image (useful for transparent images)
|
||||
*/
|
||||
background?: string
|
||||
/**
|
||||
* [SUPPORTS MARKDOWN] a place for the siteowner to describe the entry
|
||||
*/
|
||||
description?: string
|
||||
/**
|
||||
* a key-value pair set of general-purpose fields to additionally describe the entry
|
||||
* @example entry.fields = {
|
||||
* 'date': '1960/01/01',
|
||||
* 'rating': 'general',
|
||||
* }
|
||||
*/
|
||||
fields?: Record<string, string>
|
||||
/**
|
||||
* array of tag-ids that apply to the art;
|
||||
* this is used to allow a visitor to filter out entries of a specific tag
|
||||
* @see {@link GalleryList.tags}
|
||||
*/
|
||||
tags: string[]
|
||||
/**
|
||||
* css background applied to the thumbnail (useful for transparent images)
|
||||
*/
|
||||
thumbnailBackground?: string
|
||||
/**
|
||||
* the position of the thumbnail;
|
||||
* this will be applied as css `object-position`
|
||||
*/
|
||||
thumbnailPosition?: string
|
||||
/**
|
||||
* the title of the entry
|
||||
*/
|
||||
title: string | null | undefined
|
||||
/**
|
||||
* any content warnings that apply to the entry,
|
||||
* which will be used to apply the `.warning` classname
|
||||
* to the DOM of a gallery tile
|
||||
* and will be displayed with the tile
|
||||
*/
|
||||
warning?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines an entry in a gallery that can be displayed in a tiled manner
|
||||
* and can be clicked by a visitor to display its variants or the entry itself if there are none
|
||||
*/
|
||||
export type GalleryEntry = GalleryEntryInheritedProperties & {
|
||||
/**
|
||||
* the url to the thumbnail to show for the entry in the gallery tile
|
||||
*/
|
||||
thumbnailUrl?: string
|
||||
/**
|
||||
* the url to the entry itself
|
||||
*/
|
||||
url?: string
|
||||
/**
|
||||
* optional variants for the entry;
|
||||
* this is a recursive definition of {@link GalleryEntry entries}
|
||||
* which can be navigated deeper into in a gallery
|
||||
* in a manner like a folder in a file-based operating system
|
||||
*/
|
||||
variants?: GalleryEntries
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the list of entries in a gallery,
|
||||
* a key-value object where the value is the entry,
|
||||
* and the key represents the id of a piece;
|
||||
* it is important for this to be uniquely defined at minimum within
|
||||
* the scope of the entry in relation to surrounding variants
|
||||
* in order for the app to properly navigate through variants
|
||||
* and ultimately the view page displaying the entry
|
||||
*/
|
||||
export type GalleryEntries = { [idOrTitle: string]: GalleryEntry }
|
||||
|
||||
/**
|
||||
* A wrapper around {@link GalleryEntries} for the app's use only which adds additional fields
|
||||
* in order for the app to effectively display the entries.
|
||||
*/
|
||||
export type GalleryDisplayedEntries = { [idOrTitle: string]: GalleryEntry & {
|
||||
/**
|
||||
* specifies whether the entry is hidden by the tags selected by a visitor
|
||||
*/
|
||||
hidden?: boolean
|
||||
}}
|
||||
|
||||
/**
|
||||
* Defines a tag used by entries in a gallery
|
||||
* used for filtering entries from view
|
||||
*/
|
||||
export type GalleryTag = {
|
||||
/**
|
||||
* specifies a category that the tag belongs to
|
||||
* in order to optionally organize them in the view;
|
||||
* if category is not specified, tags will be assigned
|
||||
* "NoCategory" and organized into a section without a heading
|
||||
* placed before any other sections formed by categories (if any)
|
||||
*/
|
||||
category?: string
|
||||
/**
|
||||
* specifies the name that the tag will appear as in the DOM;
|
||||
* if not specified, the id of the tag will be used in its place
|
||||
*/
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the list of tags in a gallery,
|
||||
* a key-value object where the value is the entry,
|
||||
* and the key represents the id of a tag;
|
||||
* the id of a tag must be unique,
|
||||
* and the ids specified in a gallery entry must match
|
||||
* the ids specified in `GalleryTags` in order for them to work effectively
|
||||
*/
|
||||
export type GalleryTags = { [id: string]: GalleryTag }
|
||||
|
||||
/**
|
||||
* Defines the model of the `GalleryList` template
|
||||
*/
|
||||
export type GalleryList = {
|
||||
/**
|
||||
* the entries to display in a gallery-list template
|
||||
*/
|
||||
entries: GalleryEntries
|
||||
/**
|
||||
* the tags to use for filtering entries
|
||||
*/
|
||||
tags: GalleryTags
|
||||
}
|
37
projects/frontend/src/views/markdown/markdown.vue
Normal file
37
projects/frontend/src/views/markdown/markdown.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import EmbedableContent from 'src/components/shared/embedable-content.vue'
|
||||
|
||||
import { type MarkdownDefinition } from 'content/routes.js'
|
||||
import { fetchAndParseMarkdown } from 'src/utilities/fetch'
|
||||
import { getCurrentRoute } from 'src/utilities/vuetils'
|
||||
import { useRouteStore } from 'src/routes'
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'loaded'): void
|
||||
}>()
|
||||
|
||||
const content = ref('')
|
||||
const currentRoute = getCurrentRoute()
|
||||
const routeStore = useRouteStore()
|
||||
const routeConfig = routeStore._routes[currentRoute.path] as MarkdownDefinition
|
||||
|
||||
onMounted(async () => {
|
||||
content.value = await fetchAndParseMarkdown(routeConfig.content)
|
||||
document.title = routeConfig.title
|
||||
emits('loaded')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.template.markdown
|
||||
EmbedableContent(
|
||||
v-if='content'
|
||||
:content='content'
|
||||
)
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
45
projects/frontend/src/views/project/project-list.vue
Normal file
45
projects/frontend/src/views/project/project-list.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import type { ProjectList, ProjectListingInfo } from './project'
|
||||
import { type ProjectListDefinition } from 'content/routes.js'
|
||||
import { fetchAndParseYaml } from 'src/utilities/fetch'
|
||||
import { getCurrentRoute } from 'src/utilities/vuetils'
|
||||
import { useRouteStore } from 'src/routes'
|
||||
|
||||
import ProjectTile from './project-tile.vue'
|
||||
|
||||
const projectIds = ref([] as string[])
|
||||
const projects = ref({} as { [key: string]: ProjectListingInfo })
|
||||
const ready = ref(false)
|
||||
const currentRoute = getCurrentRoute()
|
||||
const routeStore = useRouteStore()
|
||||
const routeConfig = routeStore._routes[currentRoute.path] as ProjectListDefinition
|
||||
|
||||
onMounted(async () => {
|
||||
const config = await fetchAndParseYaml<ProjectList>(routeConfig.config)
|
||||
projectIds.value = Object.keys(config.projects)
|
||||
for (let i = 0; i < projectIds.value.length; ++i) {
|
||||
const id = projectIds.value[i]
|
||||
projects.value[id] = await fetchAndParseYaml(config.projects[id].config)
|
||||
}
|
||||
document.title = routeConfig.title
|
||||
ready.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.template.project-list
|
||||
#projects(
|
||||
v-if='ready'
|
||||
)
|
||||
ProjectTile(
|
||||
v-for='id in projectIds'
|
||||
:id='id'
|
||||
:info='projects[id]'
|
||||
)
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
51
projects/frontend/src/views/project/project-tile.vue
Normal file
51
projects/frontend/src/views/project/project-tile.vue
Normal file
|
@ -0,0 +1,51 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
|
||||
import { getFormattedPeriod } from 'src/utilities/dom'
|
||||
import type { ProjectListingInfo } from './project'
|
||||
import { getCurrentRoute } from 'src/utilities/vuetils'
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
info: ProjectListingInfo
|
||||
}>()
|
||||
|
||||
const currentRoute = getCurrentRoute()
|
||||
|
||||
const { thumbnailBackground, thumbnailBackgroundSize } = props.info
|
||||
const period = computed(() => (!!props.info.period
|
||||
? getFormattedPeriod(props.info.period)
|
||||
: undefined
|
||||
))
|
||||
const caption = computed(() => marked.parse(props.info.caption || ''))
|
||||
const summary = computed(() => marked.parse(props.info.summary || ''))
|
||||
const title = computed(() => marked.parse(props.info.title || ''))
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.project-embed
|
||||
router-link.link(
|
||||
:to='{ name: `${currentRoute.name}: View Project`, query: { id: id } }'
|
||||
:href='`./view?id=${id}`'
|
||||
:style='{ background: thumbnailBackground, backgroundSize: thumbnailBackgroundSize }'
|
||||
)
|
||||
.text
|
||||
.title(
|
||||
v-html='title'
|
||||
)
|
||||
p.period(
|
||||
v-if='period'
|
||||
) {{ period }}
|
||||
.caption(
|
||||
v-html='caption'
|
||||
)
|
||||
.summary(
|
||||
v-html='summary'
|
||||
)
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
|
||||
</style>
|
||||
|
53
projects/frontend/src/views/project/project-view.vue
Normal file
53
projects/frontend/src/views/project/project-view.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import type { ProjectList, ProjectListingInfo } from './project'
|
||||
import { type ProjectListDefinition } from 'content/routes.js'
|
||||
import { fetchAndParseMarkdown, fetchAndParseYaml } from 'src/utilities/fetch'
|
||||
import { getCurrentRoute } from 'src/utilities/vuetils'
|
||||
import { useRouteStore } from 'src/routes'
|
||||
|
||||
import EmbedableContent from 'src/components/shared/embedable-content.vue'
|
||||
import { getFormattedPeriod } from 'src/utilities/dom'
|
||||
|
||||
const props = defineProps<{
|
||||
id: string,
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'loaded'): void
|
||||
}>()
|
||||
|
||||
const ready = ref(false)
|
||||
const info = ref(null! as ProjectListingInfo)
|
||||
const content = ref('')
|
||||
const currentRoute = getCurrentRoute()
|
||||
const routeStore = useRouteStore()
|
||||
const routeConfig = routeStore._routes[currentRoute.path.substring(0, currentRoute.path.length - 5)] as ProjectListDefinition
|
||||
const routeSubConfig = routeStore._routes[currentRoute.path]
|
||||
|
||||
onMounted(async () => {
|
||||
const config = await fetchAndParseYaml<ProjectList>(routeConfig.config)
|
||||
info.value = await fetchAndParseYaml<ProjectListingInfo>(config.projects[props.id].config)
|
||||
const md = await fetchAndParseMarkdown(config.projects[props.id].content)
|
||||
content.value = md.replace('$PERIOD', getFormattedPeriod(info.value.period!))
|
||||
document.title = routeSubConfig.title.replace('$PROJECT', info.value.title)
|
||||
|
||||
ready.value = true
|
||||
emits('loaded')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.project
|
||||
article(
|
||||
v-if='ready'
|
||||
)
|
||||
EmbedableContent(
|
||||
:content='content'
|
||||
)
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
</style>
|
116
projects/frontend/src/views/project/project.d.ts
vendored
Normal file
116
projects/frontend/src/views/project/project.d.ts
vendored
Normal file
|
@ -0,0 +1,116 @@
|
|||
import { DateRange } from 'src/types/shared/dateRange'
|
||||
|
||||
/**
|
||||
* This describes aditional information about a project.
|
||||
* to display when listing the project on the portfolio page.
|
||||
* At minimum, the title should be specified.
|
||||
*/
|
||||
export type ProjectListingInfo = {
|
||||
/**[Supports Markdown]
|
||||
* Caption displayed below the title of the project on the projects page
|
||||
*/
|
||||
caption?: string
|
||||
/**
|
||||
* When defined, the period will be displayed as {from} - {to}
|
||||
*/
|
||||
period?: DateRange
|
||||
/**[Supports Markdown]
|
||||
* Information to summarize a project
|
||||
*/
|
||||
summary?: string
|
||||
/**
|
||||
* Tags that correspond to project filters on the portfolio page if defined
|
||||
*/
|
||||
tags?: string[]
|
||||
/**[Supports Markdown]
|
||||
* The title of the project
|
||||
*/
|
||||
title: string
|
||||
/**[CSS:background]
|
||||
* Background image, repeat, attachment, and position for the project
|
||||
*/
|
||||
thumbnailBackground?: string
|
||||
/**[CSS:background-size]
|
||||
* Background image size
|
||||
*/
|
||||
thumbnailBackgroundSize?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* This concatenates project files for entry within the store.
|
||||
*/
|
||||
export type ProjectStoreEntry = {
|
||||
/**
|
||||
* Content pulled from the projects' markdown (.md) file
|
||||
*/
|
||||
content?: string
|
||||
|
||||
/**
|
||||
* Listing information pulled from the projects' yaml (.yml) file
|
||||
*/
|
||||
listing?: ProjectListingInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a filter category in the filters panel
|
||||
*/
|
||||
export type ProjectFilterCategory = {
|
||||
/**
|
||||
* The heading of the category
|
||||
*/
|
||||
heading: string
|
||||
/**
|
||||
* The filters associated with the category, or more categories
|
||||
*/
|
||||
filters: FilterDefinition[] | ProjectFilterCategory[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a filter for a project
|
||||
*/
|
||||
export type FilterDefinition = {
|
||||
/**
|
||||
* The name to display in the filters panel
|
||||
*/
|
||||
displayName: string
|
||||
/**
|
||||
* The tag which the filter corresponds to for when defined in {@link ProjectListingInfo.tags}
|
||||
*/
|
||||
tag: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever a filter is toggled in the filters panel, this holds the data regarding that change
|
||||
*/
|
||||
export type FilterChangeEvent = {
|
||||
/**
|
||||
* The tag that the filter is associated with
|
||||
*/
|
||||
tag: string
|
||||
/**
|
||||
* The toggle state of the filter
|
||||
*/
|
||||
value: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the state of the filters panel
|
||||
*/
|
||||
export type FilterState = { [tag: string]: boolean }
|
||||
|
||||
export type TagDefinition = {
|
||||
displayName: string
|
||||
className: string
|
||||
}
|
||||
|
||||
export type Tag =
|
||||
| TagDefinition
|
||||
| string
|
||||
|
||||
export type ProjectList = {
|
||||
projects: { [key: string]: {
|
||||
config: string
|
||||
content: string
|
||||
}}
|
||||
filters?: FilterDefinition[] | ProjectFilterCategory[]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue