aboutsummaryrefslogtreecommitdiff
path: root/packages/client/src
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2021-06-05 19:37:52 +0200
committerHampusM <hampus@hampusmat.com>2021-06-05 19:37:52 +0200
commit4da3272bf7893760f6710c9a1ec7de02358136e6 (patch)
tree92eb961bf20a7ef9f7c0650ba288baf512986fca /packages/client/src
parent4e3074dfd752dd52951d300090c642aee76cfaac (diff)
Reorganized into a monorepo, refactored the frontend again, goodbye Parcel
Diffstat (limited to 'packages/client/src')
-rw-r--r--packages/client/src/App.vue32
-rw-r--r--packages/client/src/assets/logo.pngbin0 -> 6849 bytes
-rw-r--r--packages/client/src/components/BaseBackButton.vue33
-rw-r--r--packages/client/src/components/BaseBreadcrumb.vue39
-rw-r--r--packages/client/src/components/CommitPatch.vue129
-rw-r--r--packages/client/src/components/HomeHeader.vue49
-rw-r--r--packages/client/src/components/HomeProjectsHeader.vue45
-rw-r--r--packages/client/src/components/RepositoryCloneDropdown.vue112
-rw-r--r--packages/client/src/components/RepositoryHeader.vue64
-rw-r--r--packages/client/src/components/RepositoryNavbar.vue59
-rw-r--r--packages/client/src/components/RepositoryTreeBlob.vue71
-rw-r--r--packages/client/src/components/RepositoryTreeTree.vue114
-rw-r--r--packages/client/src/main.js5
-rw-r--r--packages/client/src/router/index.js46
-rw-r--r--packages/client/src/scss/_bootstrap.scss75
-rw-r--r--packages/client/src/scss/_colors.scss10
-rw-r--r--packages/client/src/scss/_fonts.scss5
-rw-r--r--packages/client/src/util/hljs-languages.js45
-rw-r--r--packages/client/src/views/Home.vue79
-rw-r--r--packages/client/src/views/Repository.vue27
-rw-r--r--packages/client/src/views/RepositoryCommit.vue202
-rw-r--r--packages/client/src/views/RepositoryLog.vue111
-rw-r--r--packages/client/src/views/RepositoryTree.vue106
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
new file mode 100644
index 0000000..f3d2503
--- /dev/null
+++ b/packages/client/src/assets/logo.png
Binary files differ
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>