aboutsummaryrefslogtreecommitdiff
path: root/src/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend')
-rw-r--r--src/frontend/App.vue11
-rw-r--r--src/frontend/app.html11
-rw-r--r--src/frontend/app.js7
-rw-r--r--src/frontend/components/BaseBackButton.vue25
-rw-r--r--src/frontend/components/CommitPatch.vue149
-rw-r--r--src/frontend/components/HomeHeader.vue39
-rw-r--r--src/frontend/components/RepositoryHeader.vue50
-rw-r--r--src/frontend/components/RepositoryNavbar.vue32
-rw-r--r--src/frontend/router/index.js39
-rw-r--r--src/frontend/scss/abstracts/_colors.scss9
-rw-r--r--src/frontend/scss/abstracts/_fonts.scss5
-rw-r--r--src/frontend/scss/style.scss284
-rw-r--r--src/frontend/util/hljs-languages.js45
-rw-r--r--src/frontend/views/Home.vue62
-rw-r--r--src/frontend/views/Repository.vue29
-rw-r--r--src/frontend/views/RepositoryCommit.vue98
-rw-r--r--src/frontend/views/RepositoryLog.vue91
17 files changed, 986 insertions, 0 deletions
diff --git a/src/frontend/App.vue b/src/frontend/App.vue
new file mode 100644
index 0000000..cbdce56
--- /dev/null
+++ b/src/frontend/App.vue
@@ -0,0 +1,11 @@
+<template>
+ <div id="container" class="container-fluid px-0">
+ <router-view/>
+ </div>
+</template>
+
+<script>
+export default {
+ name: "App"
+}
+</script> \ No newline at end of file
diff --git a/src/frontend/app.html b/src/frontend/app.html
new file mode 100644
index 0000000..348ca45
--- /dev/null
+++ b/src/frontend/app.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="stylesheet" href="scss/style.scss">
+ </head>
+ <body>
+ <div id="app"></div>
+ <script src="app.js"></script>
+ </body>
+</html> \ No newline at end of file
diff --git a/src/frontend/app.js b/src/frontend/app.js
new file mode 100644
index 0000000..1b43bbe
--- /dev/null
+++ b/src/frontend/app.js
@@ -0,0 +1,7 @@
+import { createApp } from "vue/dist/vue.esm-bundler";
+import App from "./App.vue";
+import router from "./router";
+
+createApp(App)
+ .use(router)
+ .mount("#app"); \ No newline at end of file
diff --git a/src/frontend/components/BaseBackButton.vue b/src/frontend/components/BaseBackButton.vue
new file mode 100644
index 0000000..64b1286
--- /dev/null
+++ b/src/frontend/components/BaseBackButton.vue
@@ -0,0 +1,25 @@
+<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> \ No newline at end of file
diff --git a/src/frontend/components/CommitPatch.vue b/src/frontend/components/CommitPatch.vue
new file mode 100644
index 0000000..53edeb9
--- /dev/null
+++ b/src/frontend/components/CommitPatch.vue
@@ -0,0 +1,149 @@
+<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)
+ {
+ const commit_patch = [
+ h("div", { "class": "commit-file-header" }, [
+ h("span", { "class": "fw-bold"}, (props.patch["to"] === "/dev/null") ? props.patch["from"] : props.patch["to"]),
+ h("span", (props.patch["to"] === "/dev/null") ? "Deleted" : "" ),
+ h("div", { class: "commit-file-add-del" }, [
+ h("span", `+${ props.patch["additions"] }`),
+ h("span", `-${ props.patch["deletions"] }`)
+ ])
+ ])
+ ];
+
+ 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(language["name"], all_hunks.join("\n")) : 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.push(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", "..."),
+ h("td", "..."),
+ h("td", "..."),
+ 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" }, Number(hunk["new_start"]) + line_index - new_offset);
+ third_td = h("td", { class: "line-new" }, "+")
+ deleted_offset++;
+ }
+ else if(hunk['deleted'].includes(line_index)) {
+ first_td = h("td", Number(hunk["old_start"]) + line_index - deleted_offset);
+ second_td = h("td", { class: "line-highlight-deleted" });
+ third_td = h("td", { class: "line-deleted" }, "-")
+ new_offset++;
+ }
+ else {
+ first_td = h("td", { class: "line-unchanged" }, Number(hunk["old_start"]) + line_index - deleted_offset);
+ second_td = h("td", { class: "line-unchanged" }, 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.push(h("div", { class: "ps-3 pt-3 patch-too-large" }, [
+ h("span", "Patch is too large to display.")
+ ]));
+ }
+
+ return () => h("div", { class: "commit-file" }, commit_patch);
+ }
+}
+</script> \ No newline at end of file
diff --git a/src/frontend/components/HomeHeader.vue b/src/frontend/components/HomeHeader.vue
new file mode 100644
index 0000000..f0366a3
--- /dev/null
+++ b/src/frontend/components/HomeHeader.vue
@@ -0,0 +1,39 @@
+<template>
+ <div class="row mx-0">
+ <div id="header" class="col d-flex mt-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>
+ </div>
+</template>
+
+<script>
+import { watch, reactive, toRefs } from "vue";
+
+export default {
+ name: "HomeHeader",
+ setup()
+ {
+ const state = reactive({ title: String, about: String });
+
+ watch(() =>
+ {
+ fetch(`http://localhost:1337/api/v1/info`)
+ .then((res) => res.json())
+ .then((data) =>
+ {
+ state.title = data["data"]["title"],
+ state.about = data["data"]["about"]
+ });
+ });
+
+ return {
+ ... toRefs(state)
+ };
+ }
+}
+</script> \ No newline at end of file
diff --git a/src/frontend/components/RepositoryHeader.vue b/src/frontend/components/RepositoryHeader.vue
new file mode 100644
index 0000000..b0db4f9
--- /dev/null
+++ b/src/frontend/components/RepositoryHeader.vue
@@ -0,0 +1,50 @@
+<template>
+ <div class="row mx-0">
+ <div id="header" class="col d-flex mt-3 ms-2">
+ <BaseBackButton to="/" />
+ <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>
+ </div>
+</template>
+
+<script>
+import BaseBackButton from "./BaseBackButton";
+import { watch, reactive, toRefs } from "vue";
+
+export default {
+ name: "RepositoryHeader",
+ components: {
+ BaseBackButton
+ },
+ props: {
+ repository: {
+ type: String,
+ required: true
+ }
+ },
+ setup(props)
+ {
+ const state = reactive({ title: "", about: "" });
+
+ watch(() =>
+ {
+ fetch(`http://localhost:1337/api/v1/repos/${props.repository}`)
+ .then((res) => res.json())
+ .then((data) =>
+ {
+ state.title = data["data"]["name"];
+ state.about = data["data"]["description"];
+ });
+ });
+
+ return {
+ ... toRefs(state)
+ };
+ }
+}
+</script> \ No newline at end of file
diff --git a/src/frontend/components/RepositoryNavbar.vue b/src/frontend/components/RepositoryNavbar.vue
new file mode 100644
index 0000000..a1e1002
--- /dev/null
+++ b/src/frontend/components/RepositoryNavbar.vue
@@ -0,0 +1,32 @@
+<template>
+ <div id="navbar" class="row mx-0">
+ <div id="repo-navbar" class="col 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">
+ <li v-for="(item, index) in nav_items" v-bind:key="index" class="nav-item">
+ <a class="nav-link fs-4" :class="{ active: activePage === item }" :aria-current="(activePage === item) ? 'page' : ''" :href="item">{{ item }}</a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </nav>
+ </div>
+ </div>
+</template>
+
+<script>
+export default {
+ name: "RepositoryNavbar",
+ props: {
+ activePage: String
+ },
+ data()
+ {
+ return {
+ nav_items: ["log", "refs", "tree"]
+ };
+ }
+}
+</script> \ No newline at end of file
diff --git a/src/frontend/router/index.js b/src/frontend/router/index.js
new file mode 100644
index 0000000..68762cd
--- /dev/null
+++ b/src/frontend/router/index.js
@@ -0,0 +1,39 @@
+import { createRouter, createWebHistory } from "vue-router";
+import Home from "../views/Home";
+import Repository from "../views/Repository";
+import RepositoryLog from "../views/RepositoryLog";
+import RepositoryCommit from "../views/RepositoryCommit";
+
+const routes = [
+ {
+ name: "Home",
+ path: "/",
+ component: Home
+ },
+ {
+ name: "Repository",
+ path: '/:repo([a-zA-Z0-9\\.\\-_]+)',
+ component: Repository,
+ props: route => ({ repository: route.params.repo }),
+ children: [
+ {
+ name: "Repository Log",
+ path: "log",
+ component: RepositoryLog,
+ },
+ {
+ name: "Commit",
+ path: "log/:commit([a-fA-F0-9]{40}$)",
+ component: RepositoryCommit,
+ props: route => ({ commit: route.params.commit })
+ }
+ ]
+ }
+]
+
+const router = createRouter({
+ history: createWebHistory(process.env.BASE_URL),
+ routes
+});
+
+export default router; \ No newline at end of file
diff --git a/src/frontend/scss/abstracts/_colors.scss b/src/frontend/scss/abstracts/_colors.scss
new file mode 100644
index 0000000..d7c43f5
--- /dev/null
+++ b/src/frontend/scss/abstracts/_colors.scss
@@ -0,0 +1,9 @@
+$primary: #023E8A;
+$primary-light: #0096C7;
+$secondary: #F48C06;
+$success: #40916C;
+$new: #06d6a0;
+$danger: #D00000;
+$text: #ffffff;
+$text-gray: #6c757d;
+$background: #121212; \ No newline at end of file
diff --git a/src/frontend/scss/abstracts/_fonts.scss b/src/frontend/scss/abstracts/_fonts.scss
new file mode 100644
index 0000000..6af5233
--- /dev/null
+++ b/src/frontend/scss/abstracts/_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');
+
+$title: 'Oxygen', sans-serif;
+$primary: 'Open Sans', sans-serif; \ No newline at end of file
diff --git a/src/frontend/scss/style.scss b/src/frontend/scss/style.scss
new file mode 100644
index 0000000..77fee90
--- /dev/null
+++ b/src/frontend/scss/style.scss
@@ -0,0 +1,284 @@
+@use "abstracts/colors";
+@use "abstracts/fonts";
+
+@import "../../../node_modules/bootstrap/scss/functions";
+@import "../../../node_modules/bootstrap/scss/variables";
+@import "../../../node_modules/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
+);
+
+$navbar-nav-link-padding-x: 0.5rem;
+
+@import "../../../node_modules/bootstrap/scss/breadcrumb";
+@import "../../../node_modules/bootstrap/scss/utilities";
+@import "../../../node_modules/bootstrap/scss/utilities/api";
+@import "../../../node_modules/bootstrap/scss/nav";
+@import "../../../node_modules/bootstrap/scss/navbar";
+
+$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,
+);
+
+@import "../../../node_modules/bootstrap/scss/tables";
+
+@import "../../../node_modules/bootstrap/scss/containers";
+@import "../../../node_modules/bootstrap/scss/grid";
+
+@import "../../../node_modules/highlight.js/scss/srcery.scss";
+
+body {
+ background-color: colors.$background;
+ color: colors.$text;
+ font-family: fonts.$primary;
+ margin: 0px;
+}
+
+ul {
+ list-style-type: none;
+ padding: 0;
+}
+
+li {
+ margin-bottom: 25px;
+ div {
+ h2 {
+ margin: 0px;
+ }
+ }
+}
+
+p {
+ margin: 0px;
+}
+
+#title {
+ font-family: fonts.$title;
+ font-weight: 300;
+ line-height: 0.6;
+}
+
+#about {
+ font-weight: 300;
+ padding-left: 1px;
+}
+
+#projects-search {
+ align-items: center;
+ form {
+ display: flex;
+ align-items: center;
+ height: 35px;
+ input[type=search] {
+ margin-right: 15px;
+ }
+ }
+}
+
+#repos {
+ margin-top: 25px;
+}
+
+.repo-last-updated {
+ display: block;
+ font-weight: 300;
+ font-style: italic;
+}
+
+input[type=submit] {
+ background-color: colors.$primary;
+ color: colors.$text;
+ font-size: 1rem;
+ border: 0px;
+ border-radius: 7px;
+ padding: 8px 15px 8px 15px;
+}
+
+a {
+ color: colors.$text;
+ text-decoration: none;
+ &:hover {
+ color: colors.$primary-light;
+ }
+}
+
+.breadcrumb {
+ li {
+ margin-bottom: 0.5rem;
+ }
+}
+
+#commit-info {
+ margin-bottom: 2rem;
+ tbody tr {
+ td {
+ padding: 0px;
+ padding-right: 10px;
+ }
+ }
+}
+
+.commit-file {
+ 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-file-add-del {
+ margin-left: auto;
+ margin-right: 23px;
+ span {
+ margin-right: 10px !important;
+ font-weight: 700;
+ &:nth-child(1) {
+ color: colors.$new;
+ }
+ }
+}
+
+.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;
+}
+
+.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;
+}
+
+.commit-file-header {
+ display: flex;
+ background-color: rgba($color: #ffffff, $alpha: 0.08);
+ padding: 10px;
+ span {
+ margin-right: 30px;
+ &:nth-child(2) {
+ color: colors.$danger;
+ }
+ }
+}
+
+#back:hover {
+ fill: colors.$primary-light;
+}
+
+#navbar {
+ line-height: 0;
+}
+
+th {
+ text-align: start;
+}
+
+.commit-info-title {
+ color: colors.$secondary;
+ padding-right: 30px;
+ width: 20px;
+}
+
+.patch-too-large {
+ font-weight: 600;
+}
+
+
+@include media-breakpoint-down(sm) {
+ .commit-file table tbody tr td {
+ padding-left: 4px;
+ &:nth-child(2) {
+ padding-right: 4px;
+ }
+ &:nth-child(3) {
+ padding-right: 5px;
+ }
+ }
+ .table > :not(caption) > * > * {
+ padding: 0.1rem;
+ }
+}
+
+@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/src/frontend/util/hljs-languages.js b/src/frontend/util/hljs-languages.js
new file mode 100644
index 0000000..c8576e0
--- /dev/null
+++ b/src/frontend/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; \ No newline at end of file
diff --git a/src/frontend/views/Home.vue b/src/frontend/views/Home.vue
new file mode 100644
index 0000000..79ec4ab
--- /dev/null
+++ b/src/frontend/views/Home.vue
@@ -0,0 +1,62 @@
+<template>
+ <HomeHeader />
+ <div class="row mx-0 mt-5">
+ <div id="projects-header" class="col ms-4">
+ <p class="fs-1">
+ Projects
+ </p>
+ </div>
+ <div id="projects-search" class="col d-flex justify-content-end">
+ <form>
+ <input type="search" name="q">
+ <input type="submit" value="Search">
+ </form>
+ </div>
+ </div>
+ <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">
+ <p class="fs-3">
+ <router-link :to="project_name">
+ {{ project_name }}
+ </router-link>
+ </p>
+ <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>
+</template>
+
+<script>
+import HomeHeader from "../components/HomeHeader";
+import { watch, reactive, toRefs } from "vue";
+
+export default {
+ name: "Home",
+ components: {
+ HomeHeader
+ },
+ setup()
+ {
+ const state = reactive({ projects: Object, search: String });
+
+ watch(() =>
+ {
+ fetch(`http://localhost:1337/api/v1/repos`)
+ .then((res) => res.json())
+ .then((data) => state.projects = data["data"]);
+ });
+
+ state.search = (new URLSearchParams(window.location.search)).get("q");
+
+ return {
+ ... toRefs(state)
+ };
+ }
+}
+</script> \ No newline at end of file
diff --git a/src/frontend/views/Repository.vue b/src/frontend/views/Repository.vue
new file mode 100644
index 0000000..8863529
--- /dev/null
+++ b/src/frontend/views/Repository.vue
@@ -0,0 +1,29 @@
+<template>
+ <RepositoryHeader :repository="repository" />
+ <router-view :repository="repository" />
+</template>
+
+<script>
+import RepositoryHeader from "../components/RepositoryHeader";
+
+export default {
+ name: "Repository",
+ components: {
+ RepositoryHeader
+ },
+ props: {
+ repository: {
+ type: String,
+ required: true
+ }
+ },
+ created()
+ {
+ if(/^\/[a-zA-Z0-9.\-_]+[/]?$/.test(window.location.pathname)) {
+ this.$router.push({ path: `${window.location.pathname}${( window.location.pathname.endsWith("/") ) ? "log" : "/log"}`, replace: true});
+ }
+
+ console.log(this.repository);
+ }
+}
+</script> \ No newline at end of file
diff --git a/src/frontend/views/RepositoryCommit.vue b/src/frontend/views/RepositoryCommit.vue
new file mode 100644
index 0000000..283ed69
--- /dev/null
+++ b/src/frontend/views/RepositoryCommit.vue
@@ -0,0 +1,98 @@
+<template>
+ <RepositoryNavbar active-page="log" />
+ <div class="row mx-0">
+ <div class="col ms-2 ps-4 ps-sm-5 fs-5 vld-parent">
+ <nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item">
+ <router-link :to="'/' + repository + '/log'">
+ Log
+ </router-link>
+ </li>
+ <li class="breadcrumb-item active" aria-current="page">
+ {{ commit }}
+ </li>
+ </ol>
+ </nav>
+ <Loading
+ v-model:active="is_loading" :height="24"
+ :width="24" color="#ffffff"
+ :opacity="0" />
+ <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>
+
+ <template
+ v-for="(patch, index) in commit_data['patches']" :key="index">
+ <CommitPatch :patch="patch" />
+ </template>
+ </div>
+ </div>
+</template>
+
+<script>
+import RepositoryNavbar from "../components/RepositoryNavbar";
+import CommitPatch from "../components/CommitPatch";
+import Loading from "vue-loading-overlay";
+import 'vue-loading-overlay/dist/vue-loading.css';
+import { watch, reactive, toRefs } from "vue";
+import { format } from "date-fns";
+
+export default {
+ name: "RepositoryCommit",
+ components: {
+ RepositoryNavbar,
+ CommitPatch,
+ Loading
+ },
+ props: {
+ repository: {
+ type: String,
+ required: true
+ },
+ commit: {
+ type: String,
+ required: true
+ }
+ },
+ setup(props)
+ {
+ const state = reactive({ commit_data: {}, is_loading: true });
+
+ watch(() =>
+ {
+ fetch(`http://localhost:1337/api/v1/repos/${props.repository}/log/${props.commit}`)
+ .then((res) => res.json())
+ .then((data) =>
+ {
+ data["data"]["date"] = format(new Date(data["data"]["date"]), "yyyy-MM-dd hh:mm");
+ state.commit_data = data["data"]
+ state.is_loading = false;
+ });
+ });
+
+ return {
+ ... toRefs(state)
+ };
+ }
+}
+</script>
diff --git a/src/frontend/views/RepositoryLog.vue b/src/frontend/views/RepositoryLog.vue
new file mode 100644
index 0000000..399fc78
--- /dev/null
+++ b/src/frontend/views/RepositoryLog.vue
@@ -0,0 +1,91 @@
+<template>
+ <RepositoryNavbar active-page="log" />
+ <div class="row mx-0 vld-parent">
+ <Loading
+ v-model: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 RepositoryNavbar from "../components/RepositoryNavbar";
+import Loading from "vue-loading-overlay";
+import 'vue-loading-overlay/dist/vue-loading.css';
+import { format } from "date-fns";
+import { watch, reactive, toRefs } from "vue";
+
+export default {
+ name: "RepositoryLog",
+ components: {
+ RepositoryNavbar,
+ Loading
+ },
+ props: {
+ repository: {
+ type: String,
+ required: true
+ }
+ },
+ data()
+ {
+ return {
+ format: format
+ };
+ },
+ setup(props)
+ {
+ const state = reactive({ commits: {}, is_loading: true });
+
+ watch(() =>
+ {
+ fetch(`http://localhost:1337/api/v1/repos/${props.repository}/log`)
+ .then((res) => res.json())
+ .then((data) => {
+ state.commits = data["data"];
+ state.is_loading = false;
+ });
+ });
+
+ return {
+ ... toRefs(state)
+ };
+ }
+}
+</script> \ No newline at end of file