diff --git a/package-lock.json b/package-lock.json index 93afde7f3c7f1d76bcb9645cdcb372c1ba9813ef..7ce95d5d58bab84e47ba52b3b9632398557c2b50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "vue-router": "^4.1.5" }, "devDependencies": { - "@inkline/inkline": "^3.2.0", + "@inkline/inkline": "^3.2.2", "@intlify/unplugin-vue-i18n": "^0.8.1", "@vitejs/plugin-vue": "^4.0.0", "eslint": "8.22.0", @@ -387,9 +387,9 @@ "dev": true }, "node_modules/@inkline/inkline": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@inkline/inkline/-/inkline-3.2.0.tgz", - "integrity": "sha512-h14lFCtcD0A80LtpAA+NF27CCC+wfmdxK9Q/No05Cqx6a4tnQFAVjYVtH6OktC/ZEAAn1gjjl7S0RQdXJz03YQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inkline/inkline/-/inkline-3.2.2.tgz", + "integrity": "sha512-qwMmoN9YaqfEtTx/PRbRwmg8W2ky2iWQuEb34ZcKh1vSff4HWg512EyCQwzO3Jv9KzG0x4lL32uYchIhM5YFzg==", "dev": true, "dependencies": { "@popperjs/core": "2.11.6" @@ -3622,9 +3622,9 @@ "dev": true }, "@inkline/inkline": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@inkline/inkline/-/inkline-3.2.0.tgz", - "integrity": "sha512-h14lFCtcD0A80LtpAA+NF27CCC+wfmdxK9Q/No05Cqx6a4tnQFAVjYVtH6OktC/ZEAAn1gjjl7S0RQdXJz03YQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inkline/inkline/-/inkline-3.2.2.tgz", + "integrity": "sha512-qwMmoN9YaqfEtTx/PRbRwmg8W2ky2iWQuEb34ZcKh1vSff4HWg512EyCQwzO3Jv9KzG0x4lL32uYchIhM5YFzg==", "dev": true, "requires": { "@popperjs/core": "2.11.6" diff --git a/package.json b/package.json index 7c8179e5d13f24679b92ebda666e2b46ff6ee12c..be2bedddb70d1d11011c5f613f61353a80e30aa6 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "vue-router": "^4.1.5" }, "devDependencies": { - "@inkline/inkline": "^3.2.0", + "@inkline/inkline": "^3.2.2", "@intlify/unplugin-vue-i18n": "^0.8.1", "@vitejs/plugin-vue": "^4.0.0", "eslint": "8.22.0", diff --git a/src/assets/app.scss b/src/assets/app.scss index 2cee20b0569763337f4235319041052d5c282b07..959d6d3a9b37fa7e4849876ba11ba3c866f060b2 100644 --- a/src/assets/app.scss +++ b/src/assets/app.scss @@ -10,6 +10,19 @@ --contrast-color-for-light-background: var(--color--gray-75); --contrast-color-for-dark-background: var(--color--gray-10); --body--color: var(--color--gray-75); + --h3--font-size: 1.4rem; + --color--negative-background: var(--color--red-20); + --color--negative-text: var(--color--red-65); + --color--negative-text-dark: var(--color--red-25); + --color--negative-background-dark: hsl(var(--color--red-70-h), 40%, var(--color--red-60-l)); + --color--medium-background: var(--color--yellow-20); + --color--medium-text: var(--color--yellow-70); + --color--medium-text-dark: var(--color--yellow-50); + --color--medium-background-dark: hsl(var(--color--yellow-70-h), 60%, var(--color--yellow-70-l)); + --color--positive-background: var(--color--green-20); + --color--positive-text: var(--color--green-50); + --color--positive-text-dark: var(--color--green-50); + --color--positive-background-dark: hsl(var(--color--green-70-h), 50%, var(--color--green-60-l)); } h3 { @@ -21,28 +34,45 @@ h3 { } .eval-negative { - background-color: var(--color--red-20); - + background-color: var(--color--negative-background); .-dark & { - background-color: hsl(var(--color--red-70-h), 40%, var(--color--red-60-l)); + background-color: var(--color--negative-background-dark); } } .eval-medium { - background-color: var(--color--yellow-20); - + background-color: var(--color--medium-background); .-dark & { - background-color: hsl(var(--color--yellow-70-h), 60%, var(--color--yellow-70-l)); + background-color: var(--color--negative-dark-background); } } .eval-positive { - background-color: var(--color--green-20); + background-color: var(--color--positive-background); + .-dark & { + background-color: var(--color--positive-dark-background); + } +} +.text-negative { + color: var(--color--negative-text); .-dark & { - background-color: hsl(var(--color--green-70-h), 50%, var(--color--green-60-l)); + color: var(--color--negative-text-dark); + } +} + +.text-medium { + color: var(--color--medium-text); + .-dark & { + color: var(--color--medium-text-dark); + } +} +.text-positive { + color: var(--color--positive-text); + .-dark & { + color: var(--color--positive-text-dark); } } @@ -101,3 +131,24 @@ h3 { } } } + +.popover-wrapper { + ----footer--background: var(----body--background) !important; + .popover-body, .popover-footer, .popover-header { + padding-left: 12px; + padding-right: 12px; + } +} + +.card { + .card-header { + padding-left: 16px; + padding-right: 0px; + } + .card-body { + padding-left: 16px; + padding-right: 16px; + min-height: 120px; + } +} + diff --git a/src/components/Projects.vue b/src/components/Projects.vue index 98c948358685999f2af9467c17d5d1e5a27bb24a..1a2ac887edcfc51f0baf38fc1e4880794f290680 100644 --- a/src/components/Projects.vue +++ b/src/components/Projects.vue @@ -1,10 +1,69 @@ <template> - <p>{{$t('no_content')}}</p> + <template v-if="releases.length === 0"> + <p>{{$t('no_content')}}</p> + </template> + <template v-else> + <div + v-for="{ tag, projects } in releases" + :key="tag" + class="_border-left _border-left-color:gray-40 _padding-bottom:5" + > + <div class="timeline-item _font-weight:bold _padding-left:3"> + <i-badge size="lg" class="_padding:1 _font-weight:bold">ocrd_all {{tag}}</i-badge> + </div> + <i-row class="projects-container _margin-left:2 _margin-top:3 _display:flex"> + <i-column v-for="projectId in projects" :key="projectId" sm="3" class="_margin-bottom:2 _display:flex"> + <Project :id="projectId" class="_flex-grow:1"></Project> + </i-column> + </i-row> + </div> + </template> </template> <script setup> + +import { onMounted, ref } from "vue"; +import { store } from "@/helpers/store"; +import api from "@/helpers/api"; +import Project from "@/components/projects/Project.vue"; + +const releases = ref([]); + +onMounted(async () => { + store.setRepos(await api.getProjects()); + + releases.value = await api.getOcrdAllReleases(); + store.setReleases(releases.value); +}); </script> -<style scoped> +<style lang="scss" scoped> +.timeline-item { + &:before { + content: ""; + position: absolute; + display: block; + width: 18px; + height: 18px; + background: var(--color--gray-40); + left: -10px; + top: 50%; + transform: translateY(-50%); + border-radius: 50%; + } +} +.badge { + &:before { + content: ""; + position: absolute; + display: block; + width: 8px; + height: 8px; + background: var(----background); + left: -4px; + top: 50%; + transform: translateY(-50%) rotate(45deg); + } +} </style> diff --git a/src/components/projects/Project.vue b/src/components/projects/Project.vue new file mode 100644 index 0000000000000000000000000000000000000000..7f89adcdc644882e4d0aed74da6f2e881423d7e2 --- /dev/null +++ b/src/components/projects/Project.vue @@ -0,0 +1,102 @@ +<template> + <i-card> + <template #header> + <div class="_display:flex"> + <h3 class="card-title _margin-top:0">{{ props.id }}</h3> + <div class="_margin-left:auto"> + <i-popover placement="top"> + <i-button link color="dark"> + <i-icon name="ink-info"></i-icon> + </i-button> + <template #header> + <h5 class="_margin-top:0">{{ $t('additional_info')}}</h5> + </template> + <template #body> + <p>This is the popover body. Useful information goes here.</p> + </template> + <template #footer> + <div class="_display:flex"> + <a :href="readmeUrl" title="README" target="_blank">README</a> + <a :href="changeLogUrl" title="CHANGELOG" target="_blank" class="_margin-left:auto">CHANGELOG</a> + </div> + </template> + </i-popover> + </div> + </div> + </template> + <div v-if="repo"> + <div v-for="{ icon, label } in statusList" :key="label" class="_display:flex _align-items:center"> + <Icon + :name="icon" + class="_margin-right:1" + :class="{ + 'text-negative': icon === 'slash', + 'text-medium': icon === 'alert-triangle', + 'text-positive': icon === 'check-circle' + }"> + + </Icon> + <span>{{ label }}</span> + </div> + <div class="_display:flex _margin-top:2 _margin-bottom:1"> + <i-badge v-if="projectType">{{ projectType }}</i-badge> + </div> + </div> + <div v-else class="_display:flex _height:100% _align-items:center _justify-content:center"> + <span>{{ $t('error_repo_not_found') }}</span> + </div> + </i-card> +</template> + +<script setup> +import { computed, ref, watch } from "vue"; +import { store } from "@/helpers/store"; +import Icon from "@/components/Icon.vue"; +import { useI18n } from "vue-i18n"; + + +const { t } = useI18n(); + +const props = defineProps(['id']); +const repo = ref(null); + +watch(() => props.id, (id) => { + repo.value = store.getRepoById(id); +}, { immediate: true }); + +const statusList = computed(() => { + return repo.value ? [ + ...(repo.value.unreleased_changes + ? [{ icon: 'alert-triangle', label: repo.value.unreleased_changes + ' ' + t('unreleased_changes') }] + : [{ icon: 'check-circle', label: t('version') + ' ' + repo.value.latest_version }] + ), + ...(repo.value.ocrd_tool_json_valid + ? [{ icon: 'check-circle', label: 'ocrd-tool.json ' + t('is_valid') }] + : [{ icon: 'slash', label: 'ocrd-tool.json ' + t('is_not_valid') }] + ), + ...(repo.value.dependency_conflicts + ? [{ icon: 'alert-triangle', label: t('dependency_conflicts') }] + : [{ icon: 'check-circle', label: t('no_dependency_conflicts') }] + ) + ] : []; +}); + +const readmeUrl = computed(() => { + if (!repo.value) return ''; + return repo.value.additional_info.links['README.md']; +}); + +const changeLogUrl = computed(() => { + if (!repo.value) return ''; + return repo.value.additional_info.links['README.md']; +}); + +const projectType = computed(() => { + if (!repo.value) return null; + return repo.value.project_type; +}); + +</script> + +<style lang="scss" scoped> +</style> diff --git a/src/helpers/api.js b/src/helpers/api.js index 650058af0e36fa427f041bdee145a3183ef941f4..e0e9d482a2c7b461735a23c3df8cefdd4f91f16c 100644 --- a/src/helpers/api.js +++ b/src/helpers/api.js @@ -4,6 +4,10 @@ async function getProjects() { return await request(baseUrl + '/repos.json'); } +async function getOcrdAllReleases() { + return await request(baseUrl + '/ocrd_all_releases.json'); +} + async function getWorkflows() { return await request(baseUrl + '/workflows.json'); // return Promise.resolve(workflowsJson); @@ -21,5 +25,6 @@ async function request (url) { export default { getProjects, getWorkflows, - getEvalDefinitions + getEvalDefinitions, + getOcrdAllReleases }; diff --git a/src/helpers/store.js b/src/helpers/store.js index 6d1f1796a41579429fafea1182b5ef865e77d0eb..fb5284668849eeed27fba80002a01763bb9825c6 100644 --- a/src/helpers/store.js +++ b/src/helpers/store.js @@ -2,15 +2,24 @@ import { reactive } from 'vue'; export const store = reactive({ repos: [], + releases: [], evaluations: [], metricDefinitions: {}, setRepos(repos) { this.repos = repos; }, + setReleases(releases) { + this.releases = releases; + }, setEvaluations(evaluations) { this.evaluations = evaluations; }, setMetricDefinitions(defs) { this.metricDefinitions = defs; + }, + getRepoById(id) { + return this.repos.find(repo => { + return repo.id === id; + }); } }); diff --git a/src/locales/de.json b/src/locales/de.json index 931e5e1573c5a43d44b10b8aae5c584598a09ede..5534205e0bf5481aa040383ea1094ef167123c37 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -35,5 +35,13 @@ "select_all": "Alle auswählen", "document": "Dokument", "workflow": "Workflow", - "fastest": "Am schnellsten" + "fastest": "Am schnellsten", + "unreleased_changes": "unveröffentliche Änderungen", + "version": "Version", + "is_valid": "ist gültig", + "is_not_valid": "ist ungültig", + "dependency_conflicts": "Konflikte mit Abhängigkeiten", + "no_dependency_conflicts": "Keine Konflikte mit Abhängigkeiten", + "additional_info": "Zusätzliche Info", + "error_repo_not_found": "Keine Daten zum Repository gefunden." } diff --git a/src/locales/en.json b/src/locales/en.json index 1fc303ee955c0ca85455885c782b6e6945685473..f221408ecb07de2688119f8909ba31933d18b4ab 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -29,5 +29,13 @@ "select_all": "Select all", "document": "Document", "workflow": "Workflow", - "fastest": "Fastest" + "fastest": "Fastest", + "unreleased_changes": "unreleased changes", + "version": "Version", + "is_valid": "is valid", + "is_not_valid": "is not valid", + "dependency_conflicts": "Dependency conflicts", + "no_dependency_conflicts": "No dependency conflicts", + "additional_info": "Additional Info", + "error_repo_not_found": "No repository data found." } diff --git a/src/main.js b/src/main.js index 2d0d30f82d7848923ea95561cec79593d18ecd02..90f9999a650e88e9d7c128447a19fd673775f361 100644 --- a/src/main.js +++ b/src/main.js @@ -12,7 +12,7 @@ import en from './locales/en.json'; import de from './locales/de.json'; const i18n = createI18n({ - locale: 'de', + locale: 'en', messages: { en, de } }); diff --git a/src/router/index.js b/src/router/index.js index f1b850f533a96e78e44c1c21b9e0fe7adaf7accc..ac774950b4d7b3ec1f4c22fff56d805b572819f2 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,19 +1,19 @@ -import { createRouter, createWebHashHistory } from 'vue-router'; +import { createRouter, createWebHistory } from 'vue-router'; import HomeView from '../views/HomeView.vue'; import Workflows from "@/components/Workflows.vue"; import Projects from "@/components/Projects.vue"; import Processors from "@/components/Processors.vue"; const router = createRouter({ - history: createWebHashHistory(import.meta.env.BASE_URL), + history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: HomeView, children: [ - { path: 'projects', name: 'Projects', component: Projects }, - { path: 'processors', name: 'Processors', component: Processors }, + { path: 'projects', name: 'projects', component: Projects }, + { path: 'processors', name: 'processors', component: Processors }, { path: 'workflows', alias: '/', name: 'workflows', component: Workflows }, ], }, diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 7b5c88b25133ca6a3340087c2d76f42f2b67e5c1..cfe9deb9fb815874a47a3d671b254c2427d80853 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -24,12 +24,14 @@ </template> <script setup> import { onMounted, ref, watch, inject } from "vue"; -import { useRouter } from "vue-router"; +import { useRoute, useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; import { getIcon } from '@/helpers/icon'; const router = useRouter(); +const route = useRoute(); + const { t } = useI18n(); const activeTab = ref('/workflows'); @@ -57,6 +59,7 @@ const items = ref([ onMounted(async () => { await router.isReady(); + activeTab.value = '/' + route.name; }); const inkline = inject('inkline', {});