aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue11
-rw-r--r--src/app.html4
-rw-r--r--src/app.js7
-rw-r--r--src/components/BaseBackButton.vue25
-rw-r--r--src/components/CommitPatch.vue149
-rw-r--r--src/components/HomeHeader.vue39
-rw-r--r--src/components/RepositoryHeader.vue50
-rw-r--r--src/components/RepositoryNavbar.vue32
-rw-r--r--src/js/app.js635
-rw-r--r--src/router/index.js39
-rw-r--r--src/util/hljs-languages.js45
-rw-r--r--src/views/Home.vue62
-rw-r--r--src/views/Repository.vue29
-rw-r--r--src/views/RepositoryCommit.vue90
-rw-r--r--src/views/RepositoryLog.vue81
15 files changed, 661 insertions, 637 deletions
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..cbdce56
--- /dev/null
+++ b/src/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/app.html b/src/app.html
index b980ff8..348ca45 100644
--- a/src/app.html
+++ b/src/app.html
@@ -3,9 +3,9 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="scss/style.scss">
- <script type="text/javascript" src="js/app.js"></script>
</head>
<body>
- <div id="container" class="container-fluid px-0"></div>
+ <div id="app"></div>
+ <script src="app.js"></script>
</body>
</html> \ No newline at end of file
diff --git a/src/app.js b/src/app.js
new file mode 100644
index 0000000..1b43bbe
--- /dev/null
+++ b/src/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/components/BaseBackButton.vue b/src/components/BaseBackButton.vue
new file mode 100644
index 0000000..64b1286
--- /dev/null
+++ b/src/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/components/CommitPatch.vue b/src/components/CommitPatch.vue
new file mode 100644
index 0000000..53edeb9
--- /dev/null
+++ b/src/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/components/HomeHeader.vue b/src/components/HomeHeader.vue
new file mode 100644
index 0000000..f0366a3
--- /dev/null
+++ b/src/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/components/RepositoryHeader.vue b/src/components/RepositoryHeader.vue
new file mode 100644
index 0000000..db1ab1e
--- /dev/null
+++ b/src/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: String, about: String });
+
+ 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/components/RepositoryNavbar.vue b/src/components/RepositoryNavbar.vue
new file mode 100644
index 0000000..a1e1002
--- /dev/null
+++ b/src/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/js/app.js b/src/js/app.js
deleted file mode 100644
index a853ae5..0000000
--- a/src/js/app.js
+++ /dev/null
@@ -1,635 +0,0 @@
-import { format } from "date-fns";
-import hljs from 'highlight.js';
-
-function request(method, source, data = null)
-{
- return new Promise(function (resolve, reject){
- let xhr = new XMLHttpRequest();
- xhr.open(method, source, true);
- xhr.setRequestHeader("Content-Type", "application/json");
- xhr.send(data);
-
- xhr.onload = function()
- {
- if(this.status >= 200 && this.status < 300){
- resolve(xhr.response);
- }
- resolve({ status: this.status, statusText: xhr.statusText });
- };
- xhr.onerror = () =>
- {
- resolve({ status: this.status, statusText: xhr.statusText });
- }
- });
-}
-
-/**
-* Create an HTML element
-* @param {String} tag
-* A HTML tag
-*
-* @param {String} id
-* An id
-*
-* @param {Array} class_list
-* An array of classes
-*
-* @param {Object} attributes
-* An object with attributes.
-*
-* @return {HTMLElement}
-* The resulting element
-*
-*/
-function createElement(tag, id, class_list, attributes)
-{
- const element = document.createElement(tag);
-
- if(id) {
- element.setAttribute("id", id);
- }
-
- if(class_list) {
- class_list.forEach(_class => {
- element.classList.add(_class);
- });
- }
-
- if(attributes) {
- for(const [key, value] of Object.entries(attributes)) {
- element.setAttribute(key, value);
- }
- }
-
- return element;
-}
-
-async function buildHeader(container, endpoint, title_text, about_text, repo_page = false)
-{
- const info = JSON.parse(await request("GET", `http://localhost:1337/api/v1/${endpoint}`))["data"];
-
- const row_div = createElement("div", null, ["row", "mx-0"]);
- const col_div = createElement("div", "header", ["col", "d-flex", "mt-3"], null);
-
- const title_div = createElement("div", null, ["d-inline"]);
-
- let title;
-
- switch(repo_page) {
- case true:
- title = createElement("span", "title", ["fs-1"]);
- col_div.classList.add("ms-2");
- title_div.classList.add("ms-3");
-
- const back_div = createElement("div", null, ["d-inline"]);
- const back_link = createElement("a", null, null, { "href": "/" });
- const back = createBackButtonSVG();
-
- back_link.appendChild(back);
- back_div.appendChild(back_link);
- col_div.appendChild(back_div);
- break;
- case false:
- title = createElement("a", "title", ["fs-1"], { "href": "/" });
- col_div.classList.add("ms-4");
- break;
- }
-
- title.appendChild(document.createTextNode(info[title_text]));
-
- const about = createElement("p", "about", ["mb-3", "fs-4"]);
- about.appendChild(document.createTextNode(info[about_text]));
-
- title_div.appendChild(title);
- title_div.appendChild(about);
-
- col_div.appendChild(title_div);
-
- row_div.appendChild(col_div);
-
- container.appendChild(row_div);
-}
-
-function buildProjectsHeader(container)
-{
- const row_div = createElement("div", null, ["row", "mx-0", "mt-5"]);
-
- // Title column
- const title_col_div = createElement("div", "projects-header", ["col", "ms-4"]);
-
- const projects_title = createElement("p", null, ["fs-1"]);
- projects_title.appendChild(document.createTextNode("Projects"));
-
- title_col_div.appendChild(projects_title);
-
- // Search column
- const search_col_div = createElement("div", "projects-search", ["col", "d-flex", "justify-content-end"]);
-
- const form = createElement("form");
- const search = createElement("input", null, null, { "type": "search", "name": "q" });
- const submit = createElement("input", null, null, { "type": "submit", "value": "Search" });
-
- form.appendChild(search);
- form.appendChild(submit);
- search_col_div.appendChild(form);
-
- row_div.appendChild(title_col_div);
- row_div.appendChild(search_col_div);
-
- container.appendChild(row_div);
-}
-
-async function buildProjects(container)
-{
- const row_div = createElement("div", null, ["row", "mx-0"], null);
- const col_div = createElement("div", null, ["col", "ms-4"], null);
-
- const list = createElement("ul", "repos");
-
- const repos = JSON.parse(await request("GET", "http://localhost:1337/api/v1/repos"))["data"];
-
- const params = new URLSearchParams(window.location.search);
- const search = params.get("q");
-
- for(const [key, value] of Object.entries(repos)) {
- const li = createElement("li");
- const repo_div = createElement("div");
-
- const repo_title = createElement("p", null, ["fs-3"]);
- const link = createElement("a", null, null, { "href": key });
-
- link.appendChild(document.createTextNode(key));
- repo_title.appendChild(link);
-
- const repo_last_updated = createElement("span", null, ["repo-last-updated", "fs-5"]);
- repo_last_updated.appendChild(document.createTextNode(`Last updated about ${value["last_updated"]} ago`));
-
- const repo_desc = createElement("span", null, ["fs-5"]);
- repo_desc.appendChild(document.createTextNode(value["description"]));
-
- repo_div.appendChild(repo_title)
- repo_div.appendChild(repo_last_updated)
- repo_div.appendChild(repo_desc)
-
- li.appendChild(repo_div);
-
- if(search !== null) {
- if(key.indexOf(search) != -1) {
- list.appendChild(li);
- }
- }
- else {
- list.appendChild(li);
- }
- }
-
- col_div.appendChild(list);
- row_div.appendChild(col_div);
- container.appendChild(row_div);
-}
-
-function buildRepoNavbar(container, repo, page)
-{
- const row_div = createElement("div", "navbar", ["row", "mx-0"]);
- const col_div = createElement("div", "repo-navbar", ["col", "ms-4", "ps-4"]);
-
- const nav = createElement("nav", null, ["navbar", "navbar-expand", "navbar-dark"]);
- const nav_container = createElement("div", null, ["container-fluid", "px-0"]);
- const nav_collapse = createElement("div", null, ["collapse", "navbar-collapse"]);
- const nav_nav = createElement("ul", null, ["navbar-nav"]);
-
- const nav_items = ["log", "refs", "tree"];
-
- nav_items.forEach(item =>
- {
- const item_li = createElement("li", null, ["nav-item"]);
- const item_link = createElement("a", null, ["nav-link", "fs-4"], { "href": `/${repo}/${item}` });
-
- if(item === page) {
- item_link.classList.add("active");
- item_link.setAttribute("aria-current", "page");
- }
-
- item_link.appendChild(document.createTextNode(item));
-
- item_li.appendChild(item_link);
-
- nav_nav.appendChild(item_li);
- });
-
- nav_collapse.appendChild(nav_nav);
- nav_container.appendChild(nav_collapse);
- nav.appendChild(nav_container);
- col_div.appendChild(nav);
- row_div.appendChild(col_div);
- container.appendChild(row_div);
-}
-
-async function buildLog(container, repo)
-{
- const row_div = createElement("div", null, ["row", "mx-0"], null);
- const col_div = createElement("div", null, ["col", "ms-4", "ps-4", "ps-sm-5", "mt-3"], null);
-
- const table = createElement("table", "log", ["table", "table-dark", "fs-5"]);
-
- const log = JSON.parse(await request("GET", `http://localhost:1337/api/v1/repos/${repo}/log`))["data"];
-
- const thead = createElement("thead");
- const header_tr = createElement("tr");
-
- ["Subject", "Author", "Date", "Files", "Del/Add"].forEach(header =>
- {
- const header_th = createElement("th", null, ["text-secondary"]);
- header_th.appendChild(document.createTextNode(header));
- header_tr.appendChild(header_th);
- });
-
- thead.appendChild(header_tr);
- table.appendChild(thead);
-
- const tbody = createElement("tbody");
-
- log.forEach(commit =>
- {
- const tr = createElement("tr");
- const message = createElement("td");
- const message_link = createElement("a", null, null, { "href": `log/${commit["commit"]}` });
- message_link.appendChild(document.createTextNode(commit["message"]));
- message.appendChild(message_link);
-
- const author = createElement("td");
- author.appendChild(document.createTextNode(commit["author_name"]));
-
- const date = createElement("td");
- date.appendChild(document.createTextNode(format(new Date(commit["date"]), "yyyy-MM-dd hh:mm")));
-
- const files_changed = createElement("td");
- files_changed.appendChild(document.createTextNode(commit["files_changed"]));
-
- const del_add = createElement("td");
-
- const deletions = createElement("span", null, ["text-danger"])
- deletions.appendChild(document.createTextNode(`-${commit["deletions"]}`));
-
- const insertions = createElement("span", null, ["text-success"])
- insertions.appendChild(document.createTextNode(`+${commit["insertions"]}`));
-
- del_add.appendChild(deletions);
- del_add.appendChild(document.createTextNode(" / "))
- del_add.appendChild(insertions);
-
- tr.appendChild(message);
- tr.appendChild(author);
- tr.appendChild(date);
- tr.appendChild(files_changed);
- tr.appendChild(del_add)
-
- tbody.appendChild(tr);
- });
-
- table.appendChild(tbody);
- col_div.appendChild(table);
- row_div.appendChild(col_div);
- container.appendChild(row_div);
-}
-
-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"]}
-];
-
-async function buildCommit(container, repo, hash)
-{
- const row_div = createElement("div", null, ["row", "mx-0"], null);
- const col_div = createElement("div", null, ["col", "ms-2", "ps-4", "ps-sm-5", "fs-5"], null);
-
- const breadcrumb = createElement("nav", null, null, { "aria-label": "breadcrumb" });
- const breadcrumb_ol = createElement("ol", null, ["breadcrumb"]);
- const breadcrumb_item_log = createElement("li", null, ["breadcrumb-item"], { "aria-current": "page" });
- const breadcrumb_item_log_link = createElement("a", null, null, { "href": ".." });
- breadcrumb_item_log_link.appendChild(document.createTextNode("Log"));
- breadcrumb_item_log.appendChild(breadcrumb_item_log_link);
- const breadcrumb_item_commit = createElement("li", null, ["breadcrumb-item", "active"], { "aria-current": "page" });
- breadcrumb_item_commit.appendChild(document.createTextNode(hash));
-
- breadcrumb_ol.appendChild(breadcrumb_item_log);
- breadcrumb_ol.appendChild(breadcrumb_item_commit);
- breadcrumb.appendChild(breadcrumb_ol);
- col_div.appendChild(breadcrumb);
-
- const commit = JSON.parse(await request("GET", `http://localhost:1337/api/v1/repos/${repo}/log/${hash}`));
-
- const commit_info = createElement("table", "commit-info", ["table", "table-dark"]);
- const tbody = createElement("tbody");
-
- ["author", "date", "message"].forEach((subject) =>
- {
- const info = createElement("tr");
- const title = createElement("td", null, ["commit-info-title"]);
- title.appendChild(document.createTextNode(subject.charAt(0).toUpperCase() + subject.slice(1)));
- const content = createElement("td", null);
- if(subject === "date") {
- content.appendChild(document.createTextNode(format(new Date(commit["data"]["date"]), "yyyy-MM-dd hh:mm")));
- }
- else {
- content.appendChild(document.createTextNode(commit["data"][subject]));
- }
- info.appendChild(title);
- info.appendChild(content);
- tbody.appendChild(info);
- });
-
- commit_info.appendChild(tbody);
- col_div.appendChild(commit_info);
-
- commit["data"]["patches"].forEach((patch) =>
- {
- const file_div = createElement("div", null, ["commit-file"]);
-
- // Header
- const file_header = createElement("div", null, ["commit-file-header"]);
- const file_name = createElement("span", null, ["fw-bold"]);
- const file_deleted = createElement("span");
-
- if(patch["to"] === "/dev/null") {
- file_name.appendChild(document.createTextNode(patch["from"]));
- file_deleted.appendChild(document.createTextNode("Deleted"));
- }
- else {
- file_name.appendChild(document.createTextNode(patch["to"]));
- file_deleted.appendChild(document.createTextNode(""));
- }
- file_header.appendChild(file_name);
- file_header.appendChild(file_deleted);
-
- const file_add_del = createElement("div", null, ["commit-file-add-del"]);
- const file_add = createElement("span");
- const file_del = createElement("span");
-
- file_add.appendChild(document.createTextNode(`+${patch["additions"]}`));
- file_add_del.appendChild(file_add);
-
- file_del.appendChild(document.createTextNode(`-${patch["deletions"]}`));
- file_add_del.appendChild(file_del);
-
- file_header.appendChild(file_add_del);
- file_div.appendChild(file_header);
-
- console.log(patch);
-
- // The diff
- if(patch["too_large"] === false) {
- let full_patch = "";
-
- patch["hunks"].forEach((hunk) =>
- {
- full_patch = `${full_patch}${hunk["hunk"]}\n`;
- });
-
- const patch_table = createElement("table", null, null, { "cellspacing": "0px" });
- const patch_tbody = createElement("tbody");
-
- const language = languages.find((lang) => lang["extensions"].some((extension) => patch["to"].endsWith(extension)));
-
- const highlighted = language ? hljs.highlight(language["name"], full_patch) : hljs.highlightAuto(full_patch);
- const highlighted_patch = highlighted["value"].split('\n');
-
- let index = 0;
- patch["hunks"].forEach((hunk) =>
- {
- const hunk_length = hunk["hunk"].split('\n').length;
- const end = index + hunk_length;
-
- const unhighlighted_hunk = hunk["hunk"].split('\n');
- hunk["hunk"] = highlighted_patch.slice(index, end);
-
- index = end;
-
- let new_offset = 0;
- let deleted_offset = 0;
- const multiline_tags = [];
- hunk["hunk"].forEach((line, line_index) =>
- {
- //console.log(line_index + " " + line);
- const line_tr = createElement("tr");
-
- const old_line_num = createElement("td");
- const line_num = createElement("td");
- const line_change = createElement("td");
-
- const line_content = createElement("td");
- const line_code = createElement("code");
-
- if(/^@@\ -[0-9,]+\ \+[0-9,]+\ @@/.test(unhighlighted_hunk[line_index])) {
- line_tr.classList.add("commit-file-pos-change");
-
- for(let i = 0; i < 3; i++) {
- const triple_dot = createElement("td");
- triple_dot.appendChild(document.createTextNode("..."));
- line_tr.appendChild(triple_dot);
- }
-
- line_code.innerHTML = unhighlighted_hunk[line_index];
- line_content.appendChild(line_code)
- line_tr.appendChild(line_content)
-
- new_offset++;
- deleted_offset++;
- }
- else if(/^\\\ No\ newline\ at\ end\ of\ file$/.test(unhighlighted_hunk[line_index])) {
- line_tr.classList.add("commit-file-no-newline");
-
- for(let i = 0; i < 3; i++) {
- const empty = createElement("td");
- empty.appendChild(document.createTextNode(""));
- line_tr.appendChild(empty);
- }
-
- line_code.innerHTML = unhighlighted_hunk[line_index];
- line_content.appendChild(line_code)
- line_tr.appendChild(line_content)
-
- new_offset++;
- deleted_offset++;
- }
- else {
- if(hunk["new"].includes(line_index)) {
- deleted_offset++;
- line_num.appendChild(document.createTextNode(Number(hunk["new_start"]) + line_index - new_offset));
- line_num.classList.add("line-highlight-new");
-
- line_change.appendChild(document.createTextNode("+"));
- line_change.classList.add("line-new");
- }
- else if(hunk["deleted"].includes(line_index)) {
- new_offset++;
- old_line_num.appendChild(document.createTextNode(Number(hunk["old_start"]) + line_index - deleted_offset));
- line_num.classList.add("line-highlight-deleted");
- line_change.appendChild(document.createTextNode("-"));
- line_change.classList.add("line-deleted");
-
- }
- else {
- old_line_num.appendChild(document.createTextNode(Number(hunk["old_start"]) + line_index - deleted_offset));
- old_line_num.classList.add("line-unchanged");
- line_num.appendChild(document.createTextNode(Number(hunk["new_start"]) + line_index - new_offset));
- line_num.classList.add("line-unchanged");
-
- line_change.appendChild(document.createTextNode(" "));
- }
-
- line_tr.appendChild(old_line_num);
- line_tr.appendChild(line_num);
- line_tr.appendChild(line_change);
-
- 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_code.innerHTML = line + "</span>";
- console.log("Öppning " + line);
- multiline_tags.push(comment_open);
- }
- else if(comment_open_cnt < comment_close_cnt && multiline_tags.length !== 0) {
- line_code.innerHTML = multiline_tags[multiline_tags.length - 1] + line;
- console.log("Stängning " + line + " " + multiline_tags[multiline_tags.length - 1]);
- multiline_tags.pop();
- }
- else if(multiline_tags.length !== 0) {
- line_code.innerHTML = multiline_tags[multiline_tags.length - 1] + line + "</span>";
- console.log("Mitt i " + line);
- }
- else {
- line_code.innerHTML = line;
- }
-
- line_content.appendChild(line_code);
- line_tr.appendChild(line_content)
- }
-
- patch_tbody.appendChild(line_tr);
- });
- })
-
- patch_table.appendChild(patch_tbody);
- file_div.appendChild(patch_table);
- }
- else {
- const patch_too_large_div = createElement("div", null, ["ps-3", "pt-3", "patch-too-large"]);
- const patch_too_large = createElement("span");
- patch_too_large.appendChild(document.createTextNode("Patch is too large to display."));
- patch_too_large_div.appendChild(patch_too_large);
- file_div.appendChild(patch_too_large_div);
- }
-
- col_div.appendChild(file_div);
- });
-
- row_div.appendChild(col_div);
- container.appendChild(row_div);
-}
-
-function createBackButtonSVG()
-{
- const xmlns = "http://www.w3.org/2000/svg";
-
- let svg = document.createElementNS(xmlns, "svg");
-
- svg.setAttributeNS(null, "id", "back");
- svg.setAttributeNS(null, "height", "24px");
- svg.setAttributeNS(null, "width", "24px");
- svg.setAttributeNS(null, "viewBox", "0 0 24 24");
- svg.setAttributeNS(null, "fill", "#FFFFFF");
-
- const path_one = document.createElementNS(xmlns, "path");
- path_one.setAttributeNS(null, "d", "M0 0h24v24H0z");
- path_one.setAttributeNS(null, "fill", "none");
-
- const path_two = document.createElementNS(xmlns, "path");
- path_two.setAttributeNS(null, "d", "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z");
-
- svg.appendChild(path_one);
- svg.appendChild(path_two);
-
- return svg;
-}
-
-document.addEventListener("DOMContentLoaded", async function ()
-{
- let path = window.location.pathname;
-
- const container = document.getElementById("container");
-
- if(path === "/") {
- await buildHeader(container, "info", "title", "about");
- buildProjectsHeader(container);
- buildProjects(container);
- return
- }
-
- const path_valid_and_split = /^\/([a-zA-Z0-9\.\-_]+)\/([a-z]+)(?:\/([0-9a-f]+))?$/;
-
- if(path_valid_and_split.test(path)) {
- path = path_valid_and_split.exec(path);
- const repo = path[1];
- const page = path[2];
- const sub_page = path[3];
-
- console.log("Tjena!");
-
- if(page === "log") {
- await buildHeader(container, `repos/${repo}`, "name", "description", true);
- buildRepoNavbar(container, repo, page);
-
- if(sub_page) {
- buildCommit(container, repo, sub_page);
- return;
- }
- buildLog(container, repo);
- }
- }
-}); \ No newline at end of file
diff --git a/src/router/index.js b/src/router/index.js
new file mode 100644
index 0000000..68762cd
--- /dev/null
+++ b/src/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/util/hljs-languages.js b/src/util/hljs-languages.js
new file mode 100644
index 0000000..c8576e0
--- /dev/null
+++ b/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; \ No newline at end of file
diff --git a/src/views/Home.vue b/src/views/Home.vue
new file mode 100644
index 0000000..79ec4ab
--- /dev/null
+++ b/src/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/views/Repository.vue b/src/views/Repository.vue
new file mode 100644
index 0000000..8863529
--- /dev/null
+++ b/src/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/views/RepositoryCommit.vue b/src/views/RepositoryCommit.vue
new file mode 100644
index 0000000..1fc1ea0
--- /dev/null
+++ b/src/views/RepositoryCommit.vue
@@ -0,0 +1,90 @@
+<template>
+ <RepositoryNavbar active-page="log" />
+ <div class="row mx-0">
+ <div class="col ms-2 ps-4 ps-sm-5 fs-5">
+ <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>
+ <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 { watch, reactive, toRefs } from "vue";
+import { format } from "date-fns";
+
+export default {
+ name: "RepositoryCommit",
+ components: {
+ RepositoryNavbar,
+ CommitPatch
+ },
+ props: {
+ repository: {
+ type: String,
+ required: true
+ },
+ commit: {
+ type: String,
+ required: true
+ }
+ },
+ setup(props)
+ {
+ const state = reactive({ commit_data: {} });
+
+ 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"]
+ });
+ });
+
+ return {
+ ... toRefs(state)
+ };
+ }
+}
+</script>
diff --git a/src/views/RepositoryLog.vue b/src/views/RepositoryLog.vue
new file mode 100644
index 0000000..da93624
--- /dev/null
+++ b/src/views/RepositoryLog.vue
@@ -0,0 +1,81 @@
+<template>
+ <RepositoryNavbar active-page="log" />
+ <div class="row mx-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 { format } from "date-fns";
+import { watch, reactive, toRefs } from "vue";
+
+export default {
+ name: "RepositoryLog",
+ components: {
+ RepositoryNavbar
+ },
+ props: {
+ repository: {
+ type: String,
+ required: true
+ }
+ },
+ data()
+ {
+ return {
+ format: format
+ };
+ },
+ setup(props)
+ {
+ const state = reactive({ commits: {} });
+
+ watch(() =>
+ {
+ fetch(`http://localhost:1337/api/v1/repos/${props.repository}/log`)
+ .then((res) => res.json())
+ .then((data) => state.commits = data["data"]);
+ });
+
+ return {
+ ... toRefs(state)
+ };
+ }
+}
+</script> \ No newline at end of file