diff options
Diffstat (limited to 'packages/client/src/components')
-rw-r--r-- | packages/client/src/components/BaseBackButton.vue | 33 | ||||
-rw-r--r-- | packages/client/src/components/BaseBreadcrumb.vue | 39 | ||||
-rw-r--r-- | packages/client/src/components/CommitPatch.vue | 129 | ||||
-rw-r--r-- | packages/client/src/components/HomeHeader.vue | 49 | ||||
-rw-r--r-- | packages/client/src/components/HomeProjectsHeader.vue | 45 | ||||
-rw-r--r-- | packages/client/src/components/RepositoryCloneDropdown.vue | 112 | ||||
-rw-r--r-- | packages/client/src/components/RepositoryHeader.vue | 64 | ||||
-rw-r--r-- | packages/client/src/components/RepositoryNavbar.vue | 59 | ||||
-rw-r--r-- | packages/client/src/components/RepositoryTreeBlob.vue | 71 | ||||
-rw-r--r-- | packages/client/src/components/RepositoryTreeTree.vue | 114 |
10 files changed, 715 insertions, 0 deletions
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> |