diff options
author | HampusM <hampus@hampusmat.com> | 2021-06-05 19:37:52 +0200 |
---|---|---|
committer | HampusM <hampus@hampusmat.com> | 2021-06-05 19:37:52 +0200 |
commit | 4da3272bf7893760f6710c9a1ec7de02358136e6 (patch) | |
tree | 92eb961bf20a7ef9f7c0650ba288baf512986fca /packages/client/src | |
parent | 4e3074dfd752dd52951d300090c642aee76cfaac (diff) |
Reorganized into a monorepo, refactored the frontend again, goodbye Parcel
Diffstat (limited to 'packages/client/src')
23 files changed, 1458 insertions, 0 deletions
diff --git a/packages/client/src/App.vue b/packages/client/src/App.vue new file mode 100644 index 0000000..f4d00ca --- /dev/null +++ b/packages/client/src/App.vue @@ -0,0 +1,32 @@ +<template> + <router-view /> +</template> + +<style lang="scss"> +@use "./scss/colors"; + +@import "./scss/bootstrap"; + +@import "~bootstrap/scss/utilities"; +@import "~bootstrap/scss/utilities/api"; +@import "~bootstrap/scss/containers"; +@import "~bootstrap/scss/grid"; +@import "~bootstrap/scss/tooltip"; + +@import "./scss/fonts"; + +#app { + font-family: $font-primary; + color: colors.$text; + min-height: 100vh; + background-color: colors.$background; +} + +a { + color: colors.$text; + text-decoration: none; + &:hover { + color: colors.$primary-light; + } +} +</style> diff --git a/packages/client/src/assets/logo.png b/packages/client/src/assets/logo.png Binary files differnew file mode 100644 index 0000000..f3d2503 --- /dev/null +++ b/packages/client/src/assets/logo.png diff --git a/packages/client/src/components/BaseBackButton.vue b/packages/client/src/components/BaseBackButton.vue new file mode 100644 index 0000000..e1191f9 --- /dev/null +++ b/packages/client/src/components/BaseBackButton.vue @@ -0,0 +1,33 @@ +<template> + <div class="d-inline"> + <router-link :to="to"> + <svg + xmlns="http://www.w3.org/2000/svg" id="back" + height="24px" width="24px" + viewBox="0 0 24 24" fill="#ffffff"> + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" /> + </svg> + </router-link> + </div> +</template> + +<script> +export default { + name: "BaseBackButton", + props: { + to: { + type: String, + required: true + } + } +}; +</script> + +<style lang="scss" scoped> +@use "../scss/colors"; + +#back:hover { + fill: colors.$primary-light; +} +</style> diff --git a/packages/client/src/components/BaseBreadcrumb.vue b/packages/client/src/components/BaseBreadcrumb.vue new file mode 100644 index 0000000..91c0109 --- /dev/null +++ b/packages/client/src/components/BaseBreadcrumb.vue @@ -0,0 +1,39 @@ +<template> + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li + v-for="(item, index) in items" class="breadcrumb-item" + :key="index"> + <router-link :to="item.path"> + {{ item.name }} + </router-link> + </li> + <li class="breadcrumb-item active" aria-current="page"> + {{ activeItem }} + </li> + </ol> + </nav> +</template> + +<script> +export default { + name: "BaseBreadcrumb", + props: { + items: { + type: Array, + required: true + }, + activeItem: { + type: String, + required: true + } + } +}; +</script> + +<style lang="scss" scoped> +@use "../scss/colors"; +@import "../scss/bootstrap"; + +@import "~bootstrap/scss/breadcrumb"; +</style> diff --git a/packages/client/src/components/CommitPatch.vue b/packages/client/src/components/CommitPatch.vue new file mode 100644 index 0000000..0734b1e --- /dev/null +++ b/packages/client/src/components/CommitPatch.vue @@ -0,0 +1,129 @@ +<script> +import { h } from "vue"; +import hljs from "highlight.js"; +import hljs_languages from "../util/hljs-languages"; + +export default { + name: "CommitPatch", + props: { + patch: { + type: Object, + required: true + } + }, + setup(props) { + let commit_patch; + + if(props.patch.too_large === false) { + let all_hunks = props.patch.hunks.map((hunk) => hunk.hunk); + + const language = hljs_languages.find((lang) => lang.extensions.some((extension) => props.patch.to.endsWith(extension))); + let highlighted = language ? hljs.highlight(all_hunks.join("\n"), { language: language.name }) : hljs.highlightAuto(all_hunks.join("\n")); + console.log(highlighted); + highlighted = highlighted.value.split("\n"); + + const highlighted_hunks = []; + let hunk_start = 0; + all_hunks.forEach((hunk) => { + const hunk_row_cnt = hunk.split("\n").length; + highlighted_hunks.push(highlighted.slice(hunk_start, hunk_start + hunk_row_cnt)); + hunk_start = hunk_start + hunk_row_cnt; + }); + + all_hunks = all_hunks.map((hunk) => hunk.split("\n")); + + commit_patch = h("table", { cellspacing: "0px" }, [ + h("tbody", [ + props.patch.hunks.map((hunk, hunk_index) => { + let new_offset = 0; + let deleted_offset = 0; + const multiline_comments = []; + + return highlighted_hunks[hunk_index].map((line, line_index) => { + if(/^@@ -[0-9,]+ \+[0-9,]+ @@/.test(all_hunks[hunk_index][line_index])) { + new_offset++; + deleted_offset++; + return h("tr", { class: "commit-file-pos-change" }, [ + h("td", { "patch-line-col-unsel": "..." }), + h("td", { "patch-line-col-unsel": "..." }), + h("td", { "patch-line-col-unsel": "..." }), + h("td", [ + h("code", all_hunks[hunk_index][line_index]) + ]) + ]); + } else if(/^\\ No newline at end of file$/.test(all_hunks[hunk_index][line_index])) { + new_offset++; + deleted_offset++; + return h("tr", { class: "commit-file-no-newline" }, [ + h("td", ""), + h("td", ""), + h("td", ""), + h("td", [ + h("code", all_hunks[hunk_index][line_index]) + ]) + ]); + } else { + let first_td; + let second_td; + let third_td; + + if(hunk.new.includes(line_index)) { + first_td = h("td", ""); + second_td = h("td", { class: "line-highlight-new", "patch-line-col-unsel": Number(hunk.new_start) + line_index - new_offset }); + third_td = h("td", { class: "line-new", "patch-line-col-unsel": "+" }); + deleted_offset++; + } else if(hunk.deleted.includes(line_index)) { + first_td = h("td", { "patch-line-col-unsel": Number(hunk.old_start) + line_index - deleted_offset }); + second_td = h("td", { class: "line-highlight-deleted" }); + third_td = h("td", { class: "line-deleted", "patch-line-col-unsel": "-" }); + new_offset++; + } else { + first_td = h("td", { class: "line-unchanged", "patch-line-col-unsel": Number(hunk.old_start) + line_index - deleted_offset }); + second_td = h("td", { class: "line-unchanged", "patch-line-col-unsel": Number(hunk.new_start) + line_index - new_offset }); + third_td = h("td", ""); + } + + let comment_open = line.match(/<span class="hljs-comment">/g); + const comment_open_cnt = (comment_open !== null) ? comment_open.length : 0; + comment_open = (comment_open !== null) ? comment_open[0] : ""; + + let comment_close = line.match(/<\/span>/g); + const comment_close_cnt = (comment_close !== null) ? comment_close.length : 0; + comment_close = (comment_close !== null) ? comment_close[0] : ""; + + if(comment_open_cnt > comment_close_cnt) { + line = line + "</span>"; + console.log("Öppning " + line); + multiline_comments.push(comment_open); + } else if(comment_open_cnt < comment_close_cnt && multiline_comments.length !== 0) { + line = multiline_comments[multiline_comments.length - 1] + line; + console.log("Stängning " + line + " " + multiline_comments[multiline_comments.length - 1]); + multiline_comments.pop(); + } else if(multiline_comments.length !== 0) { + line = multiline_comments[multiline_comments.length - 1] + line + "</span>"; + console.log("Mitt i " + line); + } + + return h("tr", [ + first_td, + second_td, + third_td, + h("td", [ + h("code", { innerHTML: line }) + ]) + ]); + } + }); + }) + ]) + ]); + } else { + commit_patch = h("div", { class: "ps-3 pt-3 patch-too-large" }, [ + h("span", "Patch is too large to display.") + ]); + } + + return () => commit_patch; + } +}; +</script> diff --git a/packages/client/src/components/HomeHeader.vue b/packages/client/src/components/HomeHeader.vue new file mode 100644 index 0000000..24afd5b --- /dev/null +++ b/packages/client/src/components/HomeHeader.vue @@ -0,0 +1,49 @@ +<template> + <div id="header" class="d-flex mt-3 mb-3 ms-2"> + <div class="d-inline ms-3"> + <span id="title" class="fs-1">{{ title }}</span> + <p id="about" class="mb-3 fs-4"> + {{ about }} + </p> + </div> + </div> +</template> + +<script> +import { ref } from "vue"; + +export default { + name: "HomeHeader", + setup() { + const title = ref(""); + const about = ref(""); + + const fetchInfo = async() => { + const data = await (await fetch(`${window.location.protocol}//${window.location.host}/api/v1/info`)).json(); + console.log(data.data); + title.value = data.data.title; + about.value = data.data.about; + }; + + return { title, about, fetchInfo }; + }, + mounted() { + this.fetchInfo(); + } +}; +</script> + +<style lang="scss"> +@import "../scss/fonts"; + +#title { + font-family: $font-title; + font-weight: 300; + line-height: 0.6; +} + +#about { + font-weight: 300; + padding-left: 1px; +} +</style> diff --git a/packages/client/src/components/HomeProjectsHeader.vue b/packages/client/src/components/HomeProjectsHeader.vue new file mode 100644 index 0000000..6bc9a34 --- /dev/null +++ b/packages/client/src/components/HomeProjectsHeader.vue @@ -0,0 +1,45 @@ +<template> + <div class="row mx-0"> + <div id="projects-header" class="col ms-4"> + <span class="fs-1"> + Projects + </span> + </div> + <div id="projects-search" class="col d-flex justify-content-end"> + <form> + <input type="search" name="q"> + <input + type="submit" value="Search" + class="btn btn-primary"> + </form> + </div> + </div> +</template> + +<script> +export default { + name: "HomeProjectsHeader" +}; +</script> + +<style lang="scss" scoped> +@use "../scss/colors"; +@import "../scss/bootstrap"; + +@import "~bootstrap/scss/buttons"; + +#projects-search { + align-items: center; + form { + display: flex; + align-items: center; + height: 35px; + input[type=search] { + margin-right: 15px; + } + } + input[type=submit] { + margin-right: 15px; + } +} +</style> diff --git a/packages/client/src/components/RepositoryCloneDropdown.vue b/packages/client/src/components/RepositoryCloneDropdown.vue new file mode 100644 index 0000000..ed565ef --- /dev/null +++ b/packages/client/src/components/RepositoryCloneDropdown.vue @@ -0,0 +1,112 @@ +<template> + <div id="clone" class="d-flex align-items-center"> + <div class="dropdown"> + <button + class="btn btn-primary btn-sm dropdown-toggle" type="button" + id="dropdownMenuButton1" data-bs-toggle="dropdown" + data-bs-auto-close="outside" aria-expanded="false"> + Clone + </button> + <ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark" aria-labelledby="dropdownMenuButton1"> + <li class="pt-2"> + <span class="ms-2 fs-5 fw-bold">Clone with HTTP</span> + <label id="clone-url-copy"> + <input + type="text" :value="getURL()" + class="form-control form-control-sm ms-2 me-2" readonly> + <svg + xmlns="http://www.w3.org/2000/svg" height="18px" + viewBox="0 0 24 24" width="18px" + fill="#FFFFFF" @click="copyToClipboard"> + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" /> + </svg> + </label> + </li> + </ul> + </div> + </div> +</template> + +<script> +import { Tooltip } from "bootstrap/dist/js/bootstrap.esm"; +// Import Fan from "../util/sleep.worker"; + +export default { + name: "RepositoryCloneDropdown", + props: { + repository: { + type: String, + required: true + } + }, + methods: { + async copyToClipboard(event) { + const url_box = document.getElementById("clone").getElementsByTagName("input")[0]; + + url_box.select(); + url_box.setSelectionRange(0, 99999); + document.execCommand("copy"); + + event.stopPropagation(); + + const exampleEl = document.getElementById("clone-url-copy").getElementsByTagName("svg")[0]; + const tooltip = new Tooltip(exampleEl, { title: "Copied the URL", trigger: "manual" }); + console.log(tooltip); + tooltip.show(); + + await new Promise(resolve => setTimeout(resolve, 1700)); + tooltip.hide(); + }, + getURL() { + return `${window.location.protocol}//${window.location.host}/${this.repository}`; + } + } +}; +</script> + +<style lang="scss" scoped> +@use "../scss/colors"; +@import "../scss/bootstrap"; + +$dropdown-dark-bg: lighten(#000000, 10%); + +@import "~bootstrap/scss/buttons"; +@import "~bootstrap/scss/dropdown"; +@import "~bootstrap/scss/forms"; +@import "~bootstrap/scss/tooltip"; + +.form-control { + width: auto; +} + +#clone { + margin-left: auto; + margin-right: 40px; +} + +#clone-url-copy { + position: relative; + height: 30px; + display: block; + text-align: left; + margin: 10px auto; + input { + display: inline-block; + padding-right: 30px; + min-height: 0; + } + svg { + content: ""; + position: absolute; + right: 12px; + top: 7px; + bottom: 0; + width: 18px; + fill: colors.$not-selected; + &:hover { + fill: colors.$text; + } + } +} +</style> diff --git a/packages/client/src/components/RepositoryHeader.vue b/packages/client/src/components/RepositoryHeader.vue new file mode 100644 index 0000000..9f37df4 --- /dev/null +++ b/packages/client/src/components/RepositoryHeader.vue @@ -0,0 +1,64 @@ +<template> + <div id="header" class="mx-0 d-flex mt-3 ms-2"> + <BaseBackButton to="/" /> + <div class="d-inline ms-3"> + <span id="title" class="fs-1">{{ name }}</span> + <p id="about" class="fs-4"> + {{ description }} + </p> + </div> + </div> +</template> + +<script> +import BaseBackButton from "@/components/BaseBackButton"; +import { ref } from "vue"; + +export default { + name: "RepositoryHeader", + props: { + repository: { + type: String, + required: true + } + }, + components: { + BaseBackButton + }, + setup(props) { + const name = ref(""); + const description = ref(""); + + const fetchProjects = async() => { + const repository_data = await (await fetch(`${window.location.protocol}//${window.location.host}/api/v1/repos/${props.repository}`)).json(); + name.value = repository_data.data.name; + description.value = repository_data.data.description; + }; + + return { name, description, fetchProjects }; + }, + created() { + this.fetchProjects(); + } +}; +</script> + +<style lang="scss" scoped> +@use "../scss/colors"; + +@import "../scss/bootstrap"; +@import "../scss/fonts"; + +#title { + font-family: $font-title; + font-weight: 300; + line-height: 0.6; +} + +#about { + font-weight: 300; + padding-left: 1px; + margin: 0px; + margin-top: 10px; +} +</style> diff --git a/packages/client/src/components/RepositoryNavbar.vue b/packages/client/src/components/RepositoryNavbar.vue new file mode 100644 index 0000000..c69762f --- /dev/null +++ b/packages/client/src/components/RepositoryNavbar.vue @@ -0,0 +1,59 @@ +<template> + <div id="navbar"> + <div id="repo-navbar" class="ms-4 ps-4"> + <nav class="navbar navbar-expand navbar-dark"> + <div class="container-fluid px-0"> + <div class="collapse navbar-collapse"> + <ul class="navbar-nav align-items-center flex-fill"> + <li + v-for="(item, index) in ['log', 'refs', 'tree']" :key="index" + class="nav-item"> + <router-link + class="nav-link fs-4" :class="{ active: activePage === item }" + :aria-current="(activePage === item) ? 'page' : ''" :to="'/' + repository + '/' + item"> + {{ item }} + </router-link> + </li> + <li class="nav-item ms-auto me-4"> + <RepositoryCloneDropdown :repository="repository" class="d-block" /> + </li> + </ul> + </div> + </div> + </nav> + </div> + </div> +</template> + +<script> +import RepositoryCloneDropdown from "@/components/RepositoryCloneDropdown"; + +export default { + name: "RepositoryNavbar", + props: { + repository: { + type: String, + required: true + }, + activePage: { + type: String, + required: true + } + }, + components: { + RepositoryCloneDropdown + } +}; +</script> + +<style lang="scss" scoped> +@use "../scss/colors"; +@import "../scss/bootstrap"; + +@import "~bootstrap/scss/nav"; +@import "~bootstrap/scss/navbar"; + +#navbar { + line-height: 0; +} +</style> diff --git a/packages/client/src/components/RepositoryTreeBlob.vue b/packages/client/src/components/RepositoryTreeBlob.vue new file mode 100644 index 0000000..2da00ed --- /dev/null +++ b/packages/client/src/components/RepositoryTreeBlob.vue @@ -0,0 +1,71 @@ +<template> + <table cellspacing="0px"> + <tbody> + <tr v-for="(line, index) in content_lines" :key="index"> + <td :line="index + 1" /> + <td> + <code v-html="line" /> + </td> + </tr> + </tbody> + </table> +</template> + +<script> +import { ref } from "vue"; +import hljs from "highlight.js"; +import hljs_languages from "../util/hljs-languages"; +import path from "path"; + +export default { + name: "RepositoryTreeBlob", + props: { + repository: { + type: String, + required: true + }, + path: { + type: String, + required: true + }, + content: { + type: String, + required: true + } + }, + watch: { + content() { + this.initHighlightedContent(); + } + }, + mounted() { + this.initHighlightedContent(); + }, + setup(props) { + const content_lines = ref([]); + + const initHighlightedContent = async() => { + const language = hljs_languages.find((lang) => lang.extensions.some((extension) => path.extname(props.path) === extension)); + const highlighted = language ? hljs.highlight(props.content, { language: language.name }) : hljs.highlightAuto(props.content); + + content_lines.value = highlighted.value.split("\n"); + }; + + return { content_lines, initHighlightedContent }; + } +}; +</script> + +<style lang="scss"> +@import "~highlight.js/scss/srcery.scss"; + +code { + white-space: pre-wrap; + word-wrap: anywhere; +} + +[line]::before { + content: attr(line); + padding-right: 10px; +} +</style> diff --git a/packages/client/src/components/RepositoryTreeTree.vue b/packages/client/src/components/RepositoryTreeTree.vue new file mode 100644 index 0000000..89ffb81 --- /dev/null +++ b/packages/client/src/components/RepositoryTreeTree.vue @@ -0,0 +1,114 @@ +<template> + <table id="tree" class="fs-5"> + <thead> + <tr> + <th>Name</th> + <th>Last commit</th> + <th>Last updated</th> + </tr> + </thead> + <tbody> + <tr v-if="path !== ''" @click="$router.push(`/${repository}/tree/${path.split('/').slice(0, -1).join('/') }`)"> + <td + class="d-flex align-items-center"> + <div class="tree-entry-padding" /> + .. + </td> + <td /> + <td /> + </tr> + <tr + v-for="(entry, entry_name, index) in tree" :key="index" + @click="$router.push(`/${repository}/tree${path ? '/' + path : ''}/${entry_name}`)"> + <td class="d-flex align-items-center"> + <svg + xmlns="http://www.w3.org/2000/svg" height="18px" + viewBox="0 0 24 24" width="18px" + fill="#FFFFFF" v-if="entry['type'] === 'tree'" + preserveAspectRatio="xMidYMin"> + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" /> + </svg> + <span v-else class="tree-entry-padding" /> + <a @click="stopClick" :href="`/${repository}/tree${path ? '/' + path : ''}/${entry_name}`">{{ entry_name }}</a> + </td> + <td> + <a @click="routeToCommit(entry.last_commit.id, $event)" :href="`/${repository}/log/${entry.last_commit.id}`"> + {{ entry.last_commit.message }} + </a> + </td> + <td> + {{ getPrettyLastUpdated(entry.last_commit.time) }} + </td> + </tr> + </tbody> + </table> +</template> + +<script> +const { formatDistance } = require("date-fns"); + +export default { + name: "RepositoryTreeTree", + props: { + repository: { + type: String, + required: true + }, + path: { + type: String, + required: true + }, + tree: { + type: Object, + required: true + } + }, + methods: { + stopClick(event) { + event.preventDefault(); + }, + routeToCommit(commit_id, event) { + event.stopPropagation(); + event.preventDefault(); + this.$router.push(`/${this.repository}/log/${commit_id}`); + }, + getPrettyLastUpdated(date) { + return formatDistance(new Date(), new Date(date)); + } + } +}; +</script> + +<style lang="scss" scoped> +@use "../scss/colors"; +@import "../scss/bootstrap"; + +#tree { + border-spacing: 0; + th { + padding-bottom: 5px; + color: colors.$secondary; + text-align: start; + padding-right: 20px; + } + tbody tr:hover { + background-color: lighten(colors.$background, 10%); + } + td { + padding-top: 5px; + padding-bottom: 5px; + padding-right: 2vw; + &:nth-child(2) a, &:nth-child(3) { + font-weight: 300; + } + } + .tree-entry-padding, svg { + width: 18px; + padding-right: 5px; + } + a { + padding-right: 18px; + } +} +</style> diff --git a/packages/client/src/main.js b/packages/client/src/main.js new file mode 100644 index 0000000..10ea69d --- /dev/null +++ b/packages/client/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import router from "./router"; + +createApp(App).use(router).mount("#app"); diff --git a/packages/client/src/router/index.js b/packages/client/src/router/index.js new file mode 100644 index 0000000..69cd036 --- /dev/null +++ b/packages/client/src/router/index.js @@ -0,0 +1,46 @@ +import { createRouter, createWebHashHistory } from "vue-router"; +import Home from "../views/Home"; + +const routes = [ + { + path: "/", + name: "Home", + component: Home + }, + { + path: "/:repo([a-zA-Z0-9\\.\\-_]+)", + name: "Repository", + component: () => import("../views/Repository"), + props: route => ({ repository: route.params.repo }), + children: [ + { + path: "log", + name: "Repository Log", + component: () => import("../views/RepositoryLog") + }, + { + path: "log/:commit([a-fA-F0-9]{40}$)", + name: "Commit", + component: () => import("../views/RepositoryCommit"), + props: route => ({ commit: route.params.commit }) + }, + { + path: "tree/:path*", + name: "Tree Entry", + component: () => import("../views/RepositoryTree"), + props: route => ({ pathArr: route.params.path ? route.params.path : [] }) + }, + { + path: "", + redirect: to => `/${to.params.repo}/log` + } + ] + } +]; + +const router = createRouter({ + history: createWebHashHistory(), + routes +}); + +export default router; diff --git a/packages/client/src/scss/_bootstrap.scss b/packages/client/src/scss/_bootstrap.scss new file mode 100644 index 0000000..b7cc84f --- /dev/null +++ b/packages/client/src/scss/_bootstrap.scss @@ -0,0 +1,75 @@ +@import "~bootstrap/scss/functions"; +@import "~bootstrap/scss/variables"; +@import "~bootstrap/scss/mixins"; + +$theme-colors: ( + "primary": colors.$primary, + "secondary": colors.$secondary, + "success": colors.$success, + "info": $info, + "warning": $warning, + "danger": colors.$danger, + "light": $light, + "dark": $dark +); + +$font-size-base: 0.75rem; + +$h1-font-size: $font-size-base * 2.5; +$h2-font-size: $font-size-base * 2; +$h3-font-size: $font-size-base * 1.75; +$h4-font-size: $font-size-base * 1.5; +$h5-font-size: $font-size-base * 1.125; +$h6-font-size: $font-size-base; + +$font-sizes: ( + 1: $h1-font-size, + 2: $h2-font-size, + 3: $h3-font-size, + 4: $h4-font-size, + 5: $h5-font-size, + 6: $h6-font-size +); + +$input-bg: lighten(#000000, 12%); +$input-disabled-bg: lighten(#000000, 12%); +$input-color: colors.$text; +$input-focus-color: colors.$text; +$input-focus-box-shadow: none; +$input-disabled-border-color: lighten(#000000, 50%); +$input-height-sm: auto; + +$btn-padding-y-sm: 0.15rem; +$btn-padding-x-sm: 0.4rem; + +$table-cell-padding-x: 1rem; +$table-cell-padding-y: 0.2rem; + +$table-variants: ( + "primary": shift-color($primary, $table-bg-scale), + "secondary": shift-color($secondary, $table-bg-scale), + "success": shift-color($success, $table-bg-scale), + "info": shift-color($info, $table-bg-scale), + "warning": shift-color($warning, $table-bg-scale), + "danger": shift-color($danger, $table-bg-scale), + "light": $light, + "dark": colors.$background, +); + +@media (max-width: 1200px) { + .fs-1 { + font-size: calc(1.375rem + 0.667vw) !important; + } + .fs-2 { + font-size: calc(1.325rem + 1.584vw) !important; + } + .fs-3 { + font-size: calc(1.3rem + 0.017vw) !important; + } + .fs-4 { + font-size: calc(0.82rem + 0.4vw) !important; + } + .fs-5 { + font-size: calc(0.65rem + 0.25vw) !important; + } +}
\ No newline at end of file diff --git a/packages/client/src/scss/_colors.scss b/packages/client/src/scss/_colors.scss new file mode 100644 index 0000000..3c05336 --- /dev/null +++ b/packages/client/src/scss/_colors.scss @@ -0,0 +1,10 @@ +$primary: #023E8A; +$primary-light: #0077b6; +$secondary: #F48C06; +$success: #40916C; +$new: #06d6a0; +$danger: #D00000; +$text: #ffffff; +$text-gray: #6c757d; +$background: #121212; +$not-selected: #adb5bd;
\ No newline at end of file diff --git a/packages/client/src/scss/_fonts.scss b/packages/client/src/scss/_fonts.scss new file mode 100644 index 0000000..cc5561a --- /dev/null +++ b/packages/client/src/scss/_fonts.scss @@ -0,0 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300&display=swap'); + +$font-title: 'Oxygen', sans-serif; +$font-primary: 'Open Sans', sans-serif;
\ No newline at end of file diff --git a/packages/client/src/util/hljs-languages.js b/packages/client/src/util/hljs-languages.js new file mode 100644 index 0000000..2f50461 --- /dev/null +++ b/packages/client/src/util/hljs-languages.js @@ -0,0 +1,45 @@ +const languages = [ + { name: "arduino", extensions: [ ".ino" ] }, + { name: "actionscript", extensions: [ ".as" ] }, + { name: "bash", extensions: [ ".sh", ".zsh" ] }, + { name: "csharp", extensions: [ ".cs" ] }, + { name: "c", extensions: [ ".c", ".h" ] }, + { name: "cpp", extensions: [ ".cpp", ".hpp" ] }, + { name: "cmake", extensions: [ "cmake.in" ] }, + { name: "css", extensions: [ ".css" ] }, + { name: "d", extensions: [ ".d" ] }, + { name: "dos", extensions: [ ".bat", ".cmd" ] }, + { name: "dockerfile", extensions: [ "dockerfile", "Dockerfile" ] }, + { name: "go", extensions: [ ".go" ] }, + { name: "gradle", extensions: [ ".gradle" ] }, + { name: "xml", extensions: [ ".xml", ".html", ".xhtml", ".rss", ".atom", ".xjb", ".xsd", ".xsl", ".plist", ".svg" ] }, + { name: "haskell", extensions: [ ".hs" ] }, + { name: "ini", extensions: [ ".ini", ".toml" ] }, + { name: "json", extensions: [ ".json" ] }, + { name: "java", extensions: [ ".java", ".jsp" ] }, + { name: "javascript", extensions: [ ".js", ".jsx" ] }, + { name: "kotlin", extensions: [ ".kt" ] }, + { name: "lua", extensions: [ ".lua" ] }, + { name: "makefile", extensions: [ "makefile", "Makefile" ] }, + { name: "markdown", extensions: [ ".md" ] }, + { name: "objectivec", extensions: [ ".m", ".mm", ".M" ] }, + { name: "php", extensions: [ ".php" ] }, + { name: "perl", extensions: [ ".pl", ".pm" ] }, + { name: "plaintext", extensions: [ ".txt" ] }, + { name: "pgsql", extensions: [ ".pgsql" ] }, + { name: "powershell", extensions: [ ".ps", ".ps1" ] }, + { name: "python", extensions: [ ".py" ] }, + { name: "ruby", extensions: [ ".rb" ] }, + { name: "rust", extensions: [ ".rs" ] }, + { name: "scss", extensions: [ ".scss" ] }, + { name: "sql", extensions: [ ".sql" ] }, + { name: "swift", extensions: [ ".swift" ] }, + { name: "typescript", extensions: [ ".ts" ] }, + { name: "vbnet", extensions: [ ".vb" ] }, + { name: "vba", extensions: [ ".vba" ] }, + { name: "vbscript", extensions: [ ".vbs" ] }, + { name: "vim", extensions: [ ".vim" ] }, + { name: "yml", extensions: [ ".yml" ] } +]; + +export default languages; diff --git a/packages/client/src/views/Home.vue b/packages/client/src/views/Home.vue new file mode 100644 index 0000000..80f2e9a --- /dev/null +++ b/packages/client/src/views/Home.vue @@ -0,0 +1,79 @@ +<template> + <div class="container-fluid px-0"> + <HomeHeader /> + <HomeProjectsHeader /> + <div class="row mx-0"> + <div class="col ms-4"> + <ul id="repos"> + <li v-for="(project, project_name, index) in projects" :key="index"> + <div v-if="(search !== null && project_name.includes(search)) || search == null"> + <span class="fs-3"> + <router-link :to="project_name"> + {{ project_name }} + </router-link> + </span> + <span class="repo-last-updated fs-5">Last updated about {{ project["last_updated"] }} ago</span> + <span class="fs-5">{{ project["description"] }}</span> + </div> + </li> + </ul> + </div> + </div> + </div> +</template> + +<script> +import HomeHeader from "@/components/HomeHeader"; +import HomeProjectsHeader from "@/components/HomeProjectsHeader"; +import { ref } from "vue"; + +export default { + name: "Home", + components: { + HomeHeader, + HomeProjectsHeader + }, + setup() { + const projects = ref({}); + const search = ref(""); + + const fetchProjects = async() => { + const projects_data = await (await fetch(`${window.location.protocol}//${window.location.host}/api/v1/repos`)).json(); + projects.value = projects_data.data; + }; + + search.value = (new URLSearchParams(window.location.search)).get("q"); + + return { projects, search, fetchProjects }; + }, + mount() { + this.fetchProjects(); + }, + created() { + this.fetchProjects(); + } +}; +</script> + +<style lang="scss" scoped> +@use "../scss/colors"; +@import "../scss/bootstrap"; + +#repos { + margin-top: 25px; + li { + margin-bottom: 25px; + } +} + +.repo-last-updated { + display: block; + font-weight: 300; + font-style: italic; +} + +ul { + list-style-type: none; + padding: 0; +} +</style> diff --git a/packages/client/src/views/Repository.vue b/packages/client/src/views/Repository.vue new file mode 100644 index 0000000..93c3f82 --- /dev/null +++ b/packages/client/src/views/Repository.vue @@ -0,0 +1,27 @@ +<template> + <div class="container-fluid px-0 d-flex"> + <RepositoryHeader :repository="$router.currentRoute._rawValue.params.repo" /> + <RepositoryNavbar :repository="$router.currentRoute._rawValue.params.repo" :active-page="$router.currentRoute._rawValue.path.split('/')[2]" /> + <router-view /> + </div> +</template> + +<script> +import RepositoryHeader from "@/components/RepositoryHeader"; +import RepositoryNavbar from "@/components/RepositoryNavbar"; + +export default { + name: "Repository", + components: { + RepositoryHeader, + RepositoryNavbar + } +}; +</script> + +<style lang="scss" scoped> +.container-fluid { + flex-flow: column; + height: 100vh; +} +</style> diff --git a/packages/client/src/views/RepositoryCommit.vue b/packages/client/src/views/RepositoryCommit.vue new file mode 100644 index 0000000..6bbbcaa --- /dev/null +++ b/packages/client/src/views/RepositoryCommit.vue @@ -0,0 +1,202 @@ +<template> + <div class="row mx-0"> + <div class="col ms-2 ps-4 ps-sm-5 fs-5 vld-parent"> + <BaseBreadcrumb :items="[{ name: 'Log', path: '/' + $router.currentRoute._rawValue.params.repo + '/log' }]" :active-item="$router.currentRoute._rawValue.params.commit" /> + <table id="commit-info" class="table table-dark"> + <tbody> + <tr> + <td class="commit-info-title"> + Author + </td> + <td>{{ commit_data["author"] }}</td> + </tr> + <tr> + <td class="commit-info-title"> + Date + </td> + <td>{{ commit_data["date"] }}</td> + </tr> + <tr> + <td class="commit-info-title"> + Message + </td> + <td>{{ commit_data["message"] }}</td> + </tr> + </tbody> + </table> + <Loading + :active="is_loading" :height="24" + :width="24" color="#ffffff" + :opacity="0" :is-full-page="false" /> + <div + v-for="(patch, index) in commit_data['patches']" :key="index" + class="commit-patch"> + <div class="commit-patch-header"> + <span class="fw-bold">{{ (patch.to === "/dev/null") ? patch.from : patch.to }} </span> + <span v-if="patch.to === '/dev/null'">Deleted</span> + <div class="commit-patch-add-del"> + <span>+{{ patch.additions }}</span> + <span>-{{ patch.deletions }}</span> + </div> + </div> + <CommitPatch :patch="patch" /> + </div> + </div> + </div> +</template> + +<script> +import BaseBreadcrumb from "@/components/BaseBreadcrumb"; +import CommitPatch from "@/components/CommitPatch"; +import Loading from "vue-loading-overlay"; +import { ref } from "vue"; +import { format } from "date-fns"; + +export default { + name: "RepositoryCommit", + components: { + BaseBreadcrumb, + Loading, + CommitPatch + }, + setup() { + const commit_data = ref({}); + const is_loading = ref(true); + + const fetchCommit = async(repository, commit) => { + const data = await (await fetch(`${window.location.protocol}//${window.location.host}/api/v1/repos/${repository}/log/${commit}`)).json(); + data.data.date = format(new Date(data.data.date), "yyyy-MM-dd hh:mm"); + commit_data.value = data.data; + is_loading.value = false; + }; + + return { commit_data, is_loading, fetchCommit }; + }, + created() { + this.fetchCommit(this.$router.currentRoute._rawValue.params.repo, this.$router.currentRoute._rawValue.params.commit); + } +}; +</script> + +<style lang="scss"> +@use "../scss/colors"; +@import "../scss/bootstrap"; + +@import "~bootstrap/scss/tables"; + +@import "~vue-loading-overlay/dist/vue-loading.css"; +@import "~highlight.js/scss/srcery.scss"; + +#commit-info { + margin-bottom: 2rem; + tbody tr { + td { + padding: 0px; + padding-right: 10px; + } + } +} + +.commit-patch { + margin-bottom: 50px; + table { + padding-top: 15px; + tbody tr td { + padding: 0px; + padding-left: 8px; + vertical-align: top; + &:nth-child(2) { + padding-right: 7px; + } + &:nth-child(3) { + padding-right: 15px; + } + } + } +} + +.commit-patch-add-del { + margin-left: auto; + margin-right: 23px; + span { + margin-right: 10px !important; + font-weight: 700; + &:nth-child(1) { + color: colors.$new; + } + } +} + +.commit-patch-header { + display: flex; + background-color: lighten(#000000, 14%); + padding: 10px; + span { + margin-right: 30px; + &:nth-child(2) { + color: colors.$danger; + } + } +} + +.commit-info-title { + color: colors.$secondary; + padding-right: 30px; + width: 20px; +} + +.patch-too-large { + font-weight: 600; +} + +.commit-file-pos-change { + color: colors.$text-gray; +} + +.commit-file-no-newline { + color: colors.$text-gray; +} + +.line-new { + color: colors.$new; +} +.line-deleted { + color: colors.$danger; +} + +.line-unchanged { + color: colors.$text-gray; +} + +[patch-line-col-unsel]::before { + content: attr(patch-line-col-unsel); +} + +.line-highlight-new { + border-right: 1px solid colors.$new; +} +.line-highlight-deleted { + border-right: 1px solid colors.$danger; +} + +code { + white-space: pre-wrap; + word-wrap: anywhere; +} + +.row { + height: 100%; +} + +@include media-breakpoint-down(sm) { + .commit-patch table tbody tr td { + padding-left: 4px; + &:nth-child(2) { + padding-right: 4px; + } + &:nth-child(3) { + padding-right: 5px; + } + } +} +</style> diff --git a/packages/client/src/views/RepositoryLog.vue b/packages/client/src/views/RepositoryLog.vue new file mode 100644 index 0000000..542831f --- /dev/null +++ b/packages/client/src/views/RepositoryLog.vue @@ -0,0 +1,111 @@ +<template> + <div class="row mx-0 vld-parent"> + <Loading + :active="is_loading" :height="24" + :width="24" color="#ffffff" + :opacity="0" /> + <div class="col ms-4 ps-4 ps-sm-5 mt-3"> + <table id="log" class="table table-dark fs-5"> + <thead> + <tr> + <th class="text-secondary"> + Subject + </th> + <th class="text-secondary"> + Author + </th> + <th class="text-secondary"> + Date + </th> + <th class="text-secondary"> + Files + </th> + <th class="text-secondary"> + Del/Add + </th> + </tr> + </thead> + <tbody> + <tr v-for="(commit, index) in commits" :key="index"> + <td> + <router-link :to="'log/' + commit['commit']"> + {{ commit["message"] }} + </router-link> + </td> + <td>{{ commit["author_name"] }}</td> + <td>{{ format(new Date(commit["date"]), "yyyy-MM-dd hh:mm") }}</td> + <td>{{ commit["files_changed"] }}</td> + <td><span class="text-danger">-{{ commit["deletions"] }}</span> / <span class="text-success">+{{ commit["insertions"] }}</span></td> + </tr> + </tbody> + </table> + </div> + </div> +</template> + +<script> +import Loading from "vue-loading-overlay"; +import { ref } from "vue"; +import { format } from "date-fns"; + +export default { + name: "RepositoryLog", + components: { + Loading + }, + data() { + return { + format + }; + }, + setup() { + const commits = ref({}); + const is_loading = ref(true); + + const fetchLog = async(repository) => { + const log_data = await (await fetch(`${window.location.protocol}//${window.location.host}/api/v1/repos/${repository}/log`)).json(); + commits.value = log_data.data; + is_loading.value = false; + }; + + return { commits, is_loading, fetchLog }; + }, + created() { + this.fetchLog(this.$router.currentRoute._rawValue.params.repo); + } +}; +</script> + +<style lang="scss" scoped> +@use "../scss/colors"; + +@import "~vue-loading-overlay/dist/vue-loading.css"; +@import "../scss/bootstrap"; +@import "../scss/fonts"; + +@import "~bootstrap/scss/tables"; + +#log { + border-spacing: 0; + tbody tr { + &:hover { + --bs-table-bg: 0; + background-color: lighten(colors.$background, 5%); + } + td { + padding-bottom: 1em; + } + } + th { + text-align: start; + padding-bottom: 1em; + } +} + +@include media-breakpoint-down(sm) { + .table > :not(caption) > * > * { + padding: 0.1rem; + } +} + +</style> diff --git a/packages/client/src/views/RepositoryTree.vue b/packages/client/src/views/RepositoryTree.vue new file mode 100644 index 0000000..1b61c42 --- /dev/null +++ b/packages/client/src/views/RepositoryTree.vue @@ -0,0 +1,106 @@ +<template> + <div class="row mx-0"> + <div class="col ms-4 ps-4 ps-sm-5 mt-3 fs-5 vld-parent"> + <BaseBreadcrumb + :items="(pathArr.length === 0) ? [] : [{ name: $router.currentRoute._rawValue.params.repo, path: '/' + $router.currentRoute._rawValue.params.repo + '/tree' }].concat(pathArr.slice(0, -1).map((path_part, index) => + { + return { + name: path_part, + path: '/' + $router.currentRoute._rawValue.params.repo + '/tree/' + pathArr.slice(0, index + 1).join('/') + } + }))" :active-item="(pathArr.length === 0) ? $router.currentRoute._rawValue.params.repo : pathArr[pathArr.length - 1]" /> + <RepositoryTreeTree + :repository="$router.currentRoute._rawValue.params.repo" :path="path" + :tree="tree" v-if="type === 'tree'" + :is-loading="is_loading" /> + <RepositoryTreeBlob + :repository="$router.currentRoute._rawValue.params.repo" :path="path" + :content="blob_content" v-else /> + <Loading + :active="is_loading" :height="24" + :width="24" color="#ffffff" + :opacity="0" :is-full-page="false" /> + </div> + </div> +</template> + +<script> +import BaseBreadcrumb from "@/components/BaseBreadcrumb"; +import RepositoryTreeBlob from "@/components/RepositoryTreeBlob"; +import RepositoryTreeTree from "@/components/RepositoryTreeTree"; +import Loading from "vue-loading-overlay"; +import { ref } from "vue"; + +export default { + name: "RepositoryTree", + components: { + BaseBreadcrumb, + RepositoryTreeBlob, + RepositoryTreeTree, + Loading + }, + props: { + pathArr: { + type: Array, + required: true + } + }, + watch: { + pathArr() { + this.is_loading = true; + this.tree = {}; + this.fetchTree(this.$router.currentRoute._rawValue.params.repo); + } + }, + setup(props) { + const type = ref(""); + const tree = ref({}); + const blob_content = ref(""); + const is_loading = ref(true); + const path = ref(""); + + const fetchTree = async(repository) => { + path.value = props.pathArr ? props.pathArr.join("/") : undefined; + const data = await (await fetch(`${window.location.protocol}//${window.location.host}/api/v1/repos/${repository}/tree${path.value ? "?path=" + path.value : ""}`)).json(); + console.log(path.value); + type.value = data.data.type; + + if(data.data.type === "tree") { + const tree_data = data.data.tree; + + let tree_trees = Object.entries(tree_data).filter((entry) => entry[1].type === "tree"); + tree_trees = tree_trees.sort((a, b) => a[0].localeCompare(b[0])); + + let tree_blobs = Object.entries(tree_data).filter((entry) => entry[1].type === "blob"); + tree_blobs = tree_blobs.sort((a, b) => a[0].localeCompare(b[0])); + + tree.value = Object.fromEntries(tree_trees.concat(tree_blobs)); + } else { + blob_content.value = data.data.content; + } + + is_loading.value = false; + }; + + return { + type, + tree, + blob_content, + is_loading, + path, + fetchTree + }; + }, + created() { + this.fetchTree(this.$router.currentRoute._rawValue.params.repo); + } +}; +</script> + +<style lang="scss" scoped> +@import "~vue-loading-overlay/dist/vue-loading.css"; + +.row { + height: 100%; +} +</style> |