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
219 changes: 219 additions & 0 deletions client-v3/src/components/MarkdownRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<div class="markdown-content" v-html="renderedHtml" />
<!-- eslint-enable vue/no-v-html -->
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import log from 'loglevel';

const props = defineProps<{ content: string }>();

const renderedHtml = ref('');

watch(
() => props.content,
async (content) => {
if (!content) {
renderedHtml.value = '';
return;
}
try {
const raw = await marked.parse(content);
let html = transformImagePaths(raw);
html = transformMarkdownLinks(html);
renderedHtml.value = DOMPurify.sanitize(html, {
ALLOWED_TAGS: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'p',
'a',
'ul',
'ol',
'li',
'img',
'code',
'pre',
'strong',
'em',
'blockquote',
'table',
'thead',
'tbody',
'tr',
'th',
'td',
'br',
'hr',
],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class'],
});
} catch (error) {
log.error('Error rendering markdown:', error);
renderedHtml.value = '<p class="text-danger">Error rendering documentation</p>';
}
},
{ immediate: true }
);

function transformImagePaths(html: string): string {
return html
.replace(/src="\.\.\/\.\.\/images\//g, 'src="/docs/images/')

Check warning on line 68 in client-v3/src/components/MarkdownRenderer.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ4nTIJAzQbBYyxT8cPK&open=AZ4nTIJAzQbBYyxT8cPK&pullRequest=1038
.replace(/src="\.\.\/images\//g, 'src="/docs/images/');

Check warning on line 69 in client-v3/src/components/MarkdownRenderer.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ4nTIJAzQbBYyxT8cPL&open=AZ4nTIJAzQbBYyxT8cPL&pullRequest=1038
}

function transformMarkdownLinks(html: string): string {
const base = import.meta.env.BASE_URL;
return html
.replace(
/href="(\.\.\/)*(pages\/[^"]+)\.md"/g,
(_match: string, _dots: string, path: string) => {
const slug = path.replace(/^pages\//, '').replace(/_/g, '-');

Check warning on line 78 in client-v3/src/components/MarkdownRenderer.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ4nTIJAzQbBYyxT8cPM&open=AZ4nTIJAzQbBYyxT8cPM&pullRequest=1038
return `href="${base}help/${slug}"`;
}
)
.replace(/href="\.\/([^"]+)\.md"/g, (_match: string, path: string) => {
const slug = path.replace(/_/g, '-');

Check warning on line 83 in client-v3/src/components/MarkdownRenderer.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#replace()`.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ4nTIJAzQbBYyxT8cPN&open=AZ4nTIJAzQbBYyxT8cPN&pullRequest=1038
return `href="${base}help/${slug}"`;
});
}
</script>

<style scoped>
.markdown-content {
text-align: left;
color: #ebebeb;
line-height: 1.6;
}

.markdown-content :deep(h1),
.markdown-content :deep(h2),
.markdown-content :deep(h3),
.markdown-content :deep(h4),
.markdown-content :deep(h5),
.markdown-content :deep(h6) {
color: #00bc8c;
margin-top: 1.5rem;
margin-bottom: 1rem;
font-weight: 500;
}

.markdown-content :deep(h1) {
font-size: 2.5rem;
border-bottom: 1px solid #375a7f;
padding-bottom: 0.5rem;
}

.markdown-content :deep(h2) {
font-size: 2rem;
border-bottom: 1px solid #375a7f;
padding-bottom: 0.3rem;
}

.markdown-content :deep(h3) {
font-size: 1.75rem;
}

.markdown-content :deep(h4) {
font-size: 1.5rem;
}

.markdown-content :deep(img) {
max-width: 100%;
height: auto;
border-radius: 0.25rem;
margin: 1rem 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

.markdown-content :deep(a) {
color: #00bc8c;
text-decoration: none;
}

.markdown-content :deep(a:hover) {
color: #00efb2;
text-decoration: underline;
}

.markdown-content :deep(code) {
background-color: #375a7f;
color: #ebebeb;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
font-size: 0.875rem;
}

.markdown-content :deep(pre) {
background-color: #375a7f;
color: #ebebeb;
padding: 1rem;
border-radius: 0.25rem;
overflow-x: auto;
margin: 1rem 0;
}

.markdown-content :deep(pre code) {
background-color: transparent;
padding: 0;
font-size: 0.875rem;
}

.markdown-content :deep(blockquote) {
border-left: 4px solid #00bc8c;
padding-left: 1rem;
margin: 1rem 0;
color: #b8b8b8;
font-style: italic;
}

.markdown-content :deep(ul),
.markdown-content :deep(ol) {
margin: 0.5rem 0;
padding-left: 2rem;
}

.markdown-content :deep(li) {
margin: 0.25rem 0;
}

.markdown-content :deep(table) {
width: 100%;
margin: 1rem 0;
border-collapse: collapse;
}

.markdown-content :deep(th),
.markdown-content :deep(td) {
padding: 0.75rem;
border: 1px solid #375a7f;
}

.markdown-content :deep(th) {
background-color: #375a7f;
color: #00bc8c;

Check warning on line 202 in client-v3/src/components/MarkdownRenderer.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Text does not meet the minimal contrast requirement with its background.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ4nTIJAzQbBYyxT8cPO&open=AZ4nTIJAzQbBYyxT8cPO&pullRequest=1038
font-weight: 600;
}

.markdown-content :deep(tr:nth-child(even)) {
background-color: rgba(55, 90, 127, 0.3);
}

.markdown-content :deep(hr) {
border: none;
border-top: 1px solid #375a7f;
margin: 2rem 0;
}

.markdown-content :deep(p) {
margin: 0.75rem 0;
}
</style>
10 changes: 5 additions & 5 deletions client-v3/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const router = createRouter({
{
path: '/about',
name: 'about',
component: PlaceholderView,
component: () => import('@/views/AboutView.vue'),
meta: { requiresAuth: false },
},
{
Expand Down Expand Up @@ -124,14 +124,14 @@ const router = createRouter({
},
{
path: '/help',
component: PlaceholderView,
component: () => import('@/views/HelpView.vue'),
meta: { requiresAuth: false },
children: [
{ path: '', redirect: 'getting-started' },
{ path: '', redirect: '/help/getting-started' },
{
name: 'help-doc',
path: ':slug(.*)',
component: PlaceholderView,
component: () => import('@/views/help/HelpDocView.vue'),
meta: { requiresAuth: false },
},
],
Expand All @@ -144,7 +144,7 @@ const router = createRouter({
},
{
path: '/:pathMatch(.*)*',
redirect: '/404',
component: NotFoundView,
},
],
});
Expand Down
109 changes: 109 additions & 0 deletions client-v3/src/stores/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import log from 'loglevel';
import Fuse from 'fuse.js';
import { defineStore } from 'pinia';

interface HelpManifestEntry {
title: string;
slug: string;
path: string;
category: string;
}

interface HelpState {
manifest: HelpManifestEntry[];
documents: Record<string, string>;
currentDocument: string | null;
loading: boolean;
error: string | null;
searchIndex: Fuse<HelpManifestEntry> | null;
searchResults: HelpManifestEntry[];
}

export const useHelpStore = defineStore('help', {
state: (): HelpState => ({
manifest: [],
documents: {},
currentDocument: null,
loading: false,
error: null,
searchIndex: null,
searchResults: [],
}),

getters: {
documentationManifest: (state) => state.manifest,
currentDocumentContent: (state) =>
state.currentDocument ? state.documents[state.currentDocument] : null,
isLoading: (state) => state.loading,
searchResults: (state) => state.searchResults,
},

actions: {
async loadManifest() {
try {
const response = await fetch('/docs/manifest.json');
if (!response.ok) throw new Error(`HTTP ${response.status}`);

const manifest: HelpManifestEntry[] = await response.json();
this.manifest = manifest;
this.searchIndex = new Fuse(manifest, {
keys: ['title', 'path'],
threshold: 0.3,
includeScore: true,
});
log.info(`Loaded documentation manifest with ${manifest.length} documents`);
} catch (error) {
log.error('Failed to load documentation manifest:', error);
this.error = 'Failed to load documentation manifest';
}
},

async loadDocument(slug: string) {
if (this.documents[slug]) {
this.currentDocument = slug;
this.error = null;
return;
}

this.loading = true;
const doc = this.manifest.find((d) => d.slug === slug);

if (!doc) {
this.error = 'Document not found';
this.loading = false;
return;
}

try {
const response = await fetch(`/docs/${doc.path}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);

const content = await response.text();
this.documents[slug] = content;
this.currentDocument = slug;
this.error = null;
} catch (error) {
log.error('Failed to load documentation:', error);
this.error = 'Failed to load documentation';
} finally {
this.loading = false;
}
},

searchDocuments(query: string) {
if (!this.searchIndex) {
log.warn('Search index not initialized');
return;
}
if (!query || query.trim() === '') {
this.searchResults = [];
return;
}
this.searchResults = this.searchIndex.search(query).map((r) => r.item);
},

clearSearch() {
this.searchResults = [];
},
},
});
17 changes: 17 additions & 0 deletions client-v3/src/views/AboutView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div class="about">
<h1>About DigiScript</h1>
<p>A digital script resource for queueing shows, produced by the Dream Team</p>
<p>The dream team is</p>
<ul style="list-style: none">
<li>Tim Bradgate</li>
<li>Jack Pollock</li>
<li>Matthew Stratford</li>
<li>Matthew Gaynor</li>
</ul>
</div>
</template>

<script setup lang="ts">
// Static content — no store dependencies
</script>
Loading
Loading