mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-08-17 08:10:52 +03:00
sort out lint/format setup
This commit is contained in:
parent
894e31881d
commit
d80c96ce80
18
.eslintignore
Normal file
18
.eslintignore
Normal file
@ -0,0 +1,18 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Ignore not svelte dirs
|
||||
/.github
|
||||
/.vscode
|
||||
/src-tauri
|
20
.eslintrc.cjs
Normal file
20
.eslintrc.cjs
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||
plugins: ['svelte3', '@typescript-eslint'],
|
||||
ignorePatterns: ['*.cjs'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
settings: {
|
||||
'svelte3/typescript': () => require('typescript')
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"es2022": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:unicorn/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:svelte/recommended",
|
||||
],
|
||||
"plugins": [
|
||||
"unicorn",
|
||||
"prettier",
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.svelte"],
|
||||
"processor": "svelte3/svelte3"
|
||||
}
|
||||
],
|
||||
"root": true
|
||||
}
|
18
.prettierignore
Normal file
18
.prettierignore
Normal file
@ -0,0 +1,18 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Ignore not svelte dirs
|
||||
/.github
|
||||
/.vscode
|
||||
/src-tauri
|
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"plugins": [
|
||||
"prettier-plugin-svelte",
|
||||
"prettier-plugin-tailwindcss"
|
||||
],
|
||||
"pluginSearchDirs": false
|
||||
}
|
107
package.json
107
package.json
@ -1,56 +1,55 @@
|
||||
{
|
||||
"name": "git-butler-tauri",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.2.0",
|
||||
"@codemirror/merge": "^0.1.3",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.7.3",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"fluent-svelte": "^1.6.0",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"inter-ui": "^3.19.3",
|
||||
"mm-jsr": "^3.0.2",
|
||||
"nanoid": "^4.0.1",
|
||||
"posthog-js": "^1.46.1",
|
||||
"seti-icons": "^0.0.4",
|
||||
"svelte-french-toast": "^1.0.3",
|
||||
"svelte-icons": "^2.1.0",
|
||||
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "next",
|
||||
"@sveltejs/kit": "next",
|
||||
"@tauri-apps/cli": "^1.2.2",
|
||||
"@types/diff": "^5.0.2",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-svelte": "^2.18.0",
|
||||
"eslint-plugin-unicorn": "^45.0.2",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^2.8.4",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-check": "^3.0.0",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tailwindcss": "^3.1.5",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
"name": "git-butler-tauri",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.2.0",
|
||||
"@codemirror/merge": "^0.1.3",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.7.3",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"fluent-svelte": "^1.6.0",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"inter-ui": "^3.19.3",
|
||||
"mm-jsr": "^3.0.2",
|
||||
"nanoid": "^4.0.1",
|
||||
"posthog-js": "^1.46.1",
|
||||
"seti-icons": "^0.0.4",
|
||||
"svelte-french-toast": "^1.0.3",
|
||||
"svelte-icons": "^2.1.0",
|
||||
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "next",
|
||||
"@sveltejs/kit": "next",
|
||||
"@tauri-apps/cli": "^1.2.2",
|
||||
"@types/diff": "^5.0.2",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-check": "^3.0.1",
|
||||
"tailwindcss": "^3.1.5",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
701
pnpm-lock.yaml
701
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,13 @@
|
||||
const tailwindcss = require("tailwindcss");
|
||||
const autoprefixer = require("autoprefixer");
|
||||
const tailwindcss = require('tailwindcss');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
|
||||
const config = {
|
||||
plugins: [
|
||||
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||
tailwindcss(),
|
||||
//But others, like autoprefixer, need to run after,
|
||||
autoprefixer,
|
||||
],
|
||||
plugins: [
|
||||
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||
tailwindcss(),
|
||||
//But others, like autoprefixer, need to run after,
|
||||
autoprefixer
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
18
src/app.html
18
src/app.html
@ -1,13 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body class="fixed h-full w-full overflow-hidden font-sans antialiased text-base">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
<body class="fixed h-full w-full overflow-hidden font-sans antialiased text-base">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "inter-ui/inter.css";
|
||||
@import 'inter-ui/inter.css';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
282
src/lib/api.ts
282
src/lib/api.ts
@ -1,174 +1,164 @@
|
||||
import { PUBLIC_API_BASE_URL } from "$env/static/public";
|
||||
import * as log from "$lib/log";
|
||||
import { nanoid } from "nanoid";
|
||||
import { PUBLIC_API_BASE_URL } from '$env/static/public';
|
||||
import * as log from '$lib/log';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
const apiUrl = new URL("/api/", new URL(PUBLIC_API_BASE_URL));
|
||||
const apiUrl = new URL('/api/', new URL(PUBLIC_API_BASE_URL));
|
||||
|
||||
const getUrl = (path: string) => new URL(path, apiUrl).toString();
|
||||
|
||||
export type LoginToken = {
|
||||
token: string;
|
||||
expires: string;
|
||||
url: string;
|
||||
token: string;
|
||||
expires: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
picture: string;
|
||||
locale: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
access_token: string;
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
picture: string;
|
||||
locale: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
access_token: string;
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
name: string;
|
||||
description: string | null;
|
||||
repository_id: string;
|
||||
git_url: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
repository_id: string;
|
||||
git_url: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
const parseResponseJSON = async (response: Response) => {
|
||||
if (response.status === 204 || response.status === 205) {
|
||||
return null;
|
||||
} else if (response.status >= 400) {
|
||||
throw new Error(
|
||||
`HTTP Error ${response.statusText}: ${await response.text()}`
|
||||
);
|
||||
} else {
|
||||
return await response.json();
|
||||
}
|
||||
if (response.status === 204 || response.status === 205) {
|
||||
return null;
|
||||
} else if (response.status >= 400) {
|
||||
throw new Error(`HTTP Error ${response.statusText}: ${await response.text()}`);
|
||||
} else {
|
||||
return await response.json();
|
||||
}
|
||||
};
|
||||
|
||||
type FetchMiddleware = (f: typeof fetch) => typeof fetch;
|
||||
|
||||
const fetchWith = (
|
||||
fetch: typeof window.fetch,
|
||||
...middlewares: FetchMiddleware[]
|
||||
) => middlewares.reduce((f, middleware) => middleware(f), fetch);
|
||||
const fetchWith = (fetch: typeof window.fetch, ...middlewares: FetchMiddleware[]) =>
|
||||
middlewares.reduce((f, middleware) => middleware(f), fetch);
|
||||
|
||||
const withRequestId: FetchMiddleware = (fetch) => async (url, options) => {
|
||||
const requestId = nanoid();
|
||||
if (!options) options = {};
|
||||
options.headers = {
|
||||
...options?.headers,
|
||||
"X-Request-Id": requestId,
|
||||
};
|
||||
return fetch(url, options);
|
||||
const requestId = nanoid();
|
||||
if (!options) options = {};
|
||||
options.headers = {
|
||||
...options?.headers,
|
||||
'X-Request-Id': requestId
|
||||
};
|
||||
return fetch(url, options);
|
||||
};
|
||||
|
||||
const withLog: FetchMiddleware = (fetch) => async (url, options) => {
|
||||
log.info("fetch", url, options);
|
||||
try {
|
||||
const resp = await fetch(url, options);
|
||||
log.info(resp);
|
||||
return resp;
|
||||
} catch (e: any) {
|
||||
log.error("fetch", e);
|
||||
throw e;
|
||||
}
|
||||
log.info('fetch', url, options);
|
||||
try {
|
||||
const resp = await fetch(url, options);
|
||||
log.info(resp);
|
||||
return resp;
|
||||
} catch (e: any) {
|
||||
log.error('fetch', e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export default (
|
||||
{ fetch: realFetch }: { fetch: typeof window.fetch } = {
|
||||
fetch: window.fetch,
|
||||
}
|
||||
{ fetch: realFetch }: { fetch: typeof window.fetch } = {
|
||||
fetch: window.fetch
|
||||
}
|
||||
) => {
|
||||
const fetch = fetchWith(realFetch, withRequestId, withLog);
|
||||
return {
|
||||
login: {
|
||||
token: {
|
||||
create: (params: {} = {}): Promise<LoginToken> =>
|
||||
fetch(getUrl("login/token.json"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
.then(parseResponseJSON)
|
||||
.then((token) => {
|
||||
const url = new URL(token.url);
|
||||
url.host = apiUrl.host;
|
||||
return {
|
||||
...token,
|
||||
url: url.toString(),
|
||||
};
|
||||
}),
|
||||
},
|
||||
user: {
|
||||
get: (token: string): Promise<User> =>
|
||||
fetch(getUrl(`login/user/${token}.json`), {
|
||||
method: "GET",
|
||||
}).then(parseResponseJSON),
|
||||
},
|
||||
},
|
||||
user: {
|
||||
get: async (token: string): Promise<User> =>
|
||||
fetch(getUrl(`user.json`), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
}).then(parseResponseJSON),
|
||||
update: async (
|
||||
token: string,
|
||||
params: { name?: string; picture?: File }
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
if (params.name) {
|
||||
formData.append("name", params.name);
|
||||
}
|
||||
if (params.picture) {
|
||||
formData.append("avatar", params.picture);
|
||||
}
|
||||
return fetch(getUrl(`user.json`), {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
body: formData,
|
||||
}).then(parseResponseJSON);
|
||||
},
|
||||
},
|
||||
projects: {
|
||||
create: (
|
||||
token: string,
|
||||
params: { name: string; uid?: string }
|
||||
): Promise<Project> =>
|
||||
fetch(getUrl("projects.json"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
}).then(parseResponseJSON),
|
||||
list: (token: string): Promise<Project[]> =>
|
||||
fetch(getUrl("projects.json"), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
}).then(parseResponseJSON),
|
||||
get: (token: string, repositoryId: string): Promise<Project> =>
|
||||
fetch(getUrl(`projects/${repositoryId}.json`), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
}).then(parseResponseJSON),
|
||||
delete: (token: string, repositoryId: string): Promise<void> =>
|
||||
fetch(getUrl(`projects/${repositoryId}.json`), {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-Auth-Token": token,
|
||||
},
|
||||
}).then(parseResponseJSON),
|
||||
},
|
||||
};
|
||||
const fetch = fetchWith(realFetch, withRequestId, withLog);
|
||||
return {
|
||||
login: {
|
||||
token: {
|
||||
create: (params: {} = {}): Promise<LoginToken> =>
|
||||
fetch(getUrl('login/token.json'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(parseResponseJSON)
|
||||
.then((token) => {
|
||||
const url = new URL(token.url);
|
||||
url.host = apiUrl.host;
|
||||
return {
|
||||
...token,
|
||||
url: url.toString()
|
||||
};
|
||||
})
|
||||
},
|
||||
user: {
|
||||
get: (token: string): Promise<User> =>
|
||||
fetch(getUrl(`login/user/${token}.json`), {
|
||||
method: 'GET'
|
||||
}).then(parseResponseJSON)
|
||||
}
|
||||
},
|
||||
user: {
|
||||
get: async (token: string): Promise<User> =>
|
||||
fetch(getUrl(`user.json`), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Auth-Token': token
|
||||
}
|
||||
}).then(parseResponseJSON),
|
||||
update: async (token: string, params: { name?: string; picture?: File }) => {
|
||||
const formData = new FormData();
|
||||
if (params.name) {
|
||||
formData.append('name', params.name);
|
||||
}
|
||||
if (params.picture) {
|
||||
formData.append('avatar', params.picture);
|
||||
}
|
||||
return fetch(getUrl(`user.json`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-Auth-Token': token
|
||||
},
|
||||
body: formData
|
||||
}).then(parseResponseJSON);
|
||||
}
|
||||
},
|
||||
projects: {
|
||||
create: (token: string, params: { name: string; uid?: string }): Promise<Project> =>
|
||||
fetch(getUrl('projects.json'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': token
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
}).then(parseResponseJSON),
|
||||
list: (token: string): Promise<Project[]> =>
|
||||
fetch(getUrl('projects.json'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Auth-Token': token
|
||||
}
|
||||
}).then(parseResponseJSON),
|
||||
get: (token: string, repositoryId: string): Promise<Project> =>
|
||||
fetch(getUrl(`projects/${repositoryId}.json`), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Auth-Token': token
|
||||
}
|
||||
}).then(parseResponseJSON),
|
||||
delete: (token: string, repositoryId: string): Promise<void> =>
|
||||
fetch(getUrl(`projects/${repositoryId}.json`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-Auth-Token': token
|
||||
}
|
||||
}).then(parseResponseJSON)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,3 +1,3 @@
|
||||
export const toHumanBranchName = (branch: string) => {
|
||||
return branch.replace("refs/heads/", "");
|
||||
return branch.replace('refs/heads/', '');
|
||||
};
|
||||
|
@ -1,18 +1,14 @@
|
||||
<script>
|
||||
import FaArrowLeft from "svelte-icons/fa/FaArrowLeft.svelte";
|
||||
import FaArrowRight from "svelte-icons/fa/FaArrowRight.svelte";
|
||||
let history = window.history;
|
||||
import FaArrowLeft from 'svelte-icons/fa/FaArrowLeft.svelte';
|
||||
import FaArrowRight from 'svelte-icons/fa/FaArrowRight.svelte';
|
||||
let history = window.history;
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3 text-zinc-400">
|
||||
<button
|
||||
class="w-4 h-4 hover:text-zinc-200"
|
||||
title="Go back"
|
||||
on:click={() => history.back()}><FaArrowLeft /></button
|
||||
>
|
||||
<button
|
||||
class="w-4 h-4 hover:text-zinc-200"
|
||||
title="Go forward"
|
||||
on:click={() => history.forward()}><FaArrowRight /></button
|
||||
>
|
||||
<button class="w-4 h-4 hover:text-zinc-200" title="Go back" on:click={() => history.back()}
|
||||
><FaArrowLeft /></button
|
||||
>
|
||||
<button class="w-4 h-4 hover:text-zinc-200" title="Go forward" on:click={() => history.forward()}
|
||||
><FaArrowRight /></button
|
||||
>
|
||||
</div>
|
||||
|
@ -1,42 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type { Project } from "$lib/projects";
|
||||
import type { Session } from "$lib/sessions";
|
||||
import { toHumanReadableTime } from "$lib/time";
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import IoIosBowtie from "svelte-icons/io/IoIosBowtie.svelte";
|
||||
import MdKeyboardArrowRight from "svelte-icons/md/MdKeyboardArrowRight.svelte";
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Session } from '$lib/sessions';
|
||||
import { toHumanReadableTime } from '$lib/time';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import IoIosBowtie from 'svelte-icons/io/IoIosBowtie.svelte';
|
||||
import MdKeyboardArrowRight from 'svelte-icons/md/MdKeyboardArrowRight.svelte';
|
||||
|
||||
let project: Writable<Project | null | undefined> = getContext("project");
|
||||
let session: Writable<Session | null | undefined> = getContext("session");
|
||||
let project: Writable<Project | null | undefined> = getContext('project');
|
||||
let session: Writable<Session | null | undefined> = getContext('session');
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex flex-row items-center space-x-1 bg-zinc-900 text-zinc-400 h-8"
|
||||
>
|
||||
<a class="hover:text-zinc-200" href="/">
|
||||
<div class="w-6 h-6">
|
||||
<IoIosBowtie />
|
||||
</div>
|
||||
</a>
|
||||
{#if $project}
|
||||
<div class="w-8 h-8 text-zinc-700">
|
||||
<MdKeyboardArrowRight />
|
||||
</div>
|
||||
<a class="hover:text-zinc-200" href="/projects/{$project.id}"
|
||||
>{$project.title}</a
|
||||
>
|
||||
{/if}
|
||||
{#if $project && $session}
|
||||
<div class="w-8 h-8 text-zinc-700">
|
||||
<MdKeyboardArrowRight />
|
||||
</div>
|
||||
<a
|
||||
class="hover:text-zinc-200"
|
||||
href="/projects/{$project.id}/sessions/{$session.id}"
|
||||
>
|
||||
{toHumanReadableTime($session.meta.startTimestampMs)}
|
||||
{toHumanReadableTime($session.meta.lastTimestampMs)}
|
||||
</a>
|
||||
{/if}
|
||||
<div class="flex flex-row items-center space-x-1 bg-zinc-900 text-zinc-400 h-8">
|
||||
<a class="hover:text-zinc-200" href="/">
|
||||
<div class="w-6 h-6">
|
||||
<IoIosBowtie />
|
||||
</div>
|
||||
</a>
|
||||
{#if $project}
|
||||
<div class="w-8 h-8 text-zinc-700">
|
||||
<MdKeyboardArrowRight />
|
||||
</div>
|
||||
<a class="hover:text-zinc-200" href="/projects/{$project.id}">{$project.title}</a>
|
||||
{/if}
|
||||
{#if $project && $session}
|
||||
<div class="w-8 h-8 text-zinc-700">
|
||||
<MdKeyboardArrowRight />
|
||||
</div>
|
||||
<a class="hover:text-zinc-200" href="/projects/{$project.id}/sessions/{$session.id}">
|
||||
{toHumanReadableTime($session.meta.startTimestampMs)}
|
||||
{toHumanReadableTime($session.meta.lastTimestampMs)}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,77 +1,75 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView, lineNumbers } from "@codemirror/view";
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView, lineNumbers } from '@codemirror/view';
|
||||
|
||||
let editorTheme = EditorView.theme(
|
||||
{
|
||||
"&": {
|
||||
color: "#d4d4d8",
|
||||
backgroundColor: "#18181b",
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: "#0e9",
|
||||
},
|
||||
"&.cm-focused .cm-cursor": {
|
||||
borderLeftColor: "#0e9",
|
||||
},
|
||||
"&.cm-focused .cm-selectionBackground, ::selection": {
|
||||
backgroundColor: "#0284c7",
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: "#18181b",
|
||||
color: "#3f3f46",
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
{ dark: true }
|
||||
);
|
||||
const fixedHeightEditor = EditorView.theme({
|
||||
"&": { height: "600px" },
|
||||
".cm-scroller": { overflow: "auto" },
|
||||
});
|
||||
let editorTheme = EditorView.theme(
|
||||
{
|
||||
'&': {
|
||||
color: '#d4d4d8',
|
||||
backgroundColor: '#18181b'
|
||||
},
|
||||
'.cm-content': {
|
||||
caretColor: '#0e9'
|
||||
},
|
||||
'&.cm-focused .cm-cursor': {
|
||||
borderLeftColor: '#0e9'
|
||||
},
|
||||
'&.cm-focused .cm-selectionBackground, ::selection': {
|
||||
backgroundColor: '#0284c7'
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: '#18181b',
|
||||
color: '#3f3f46',
|
||||
border: 'none'
|
||||
}
|
||||
},
|
||||
{ dark: true }
|
||||
);
|
||||
const fixedHeightEditor = EditorView.theme({
|
||||
'&': { height: '600px' },
|
||||
'.cm-scroller': { overflow: 'auto' }
|
||||
});
|
||||
|
||||
export let value: string;
|
||||
export let value: string;
|
||||
|
||||
let element: HTMLDivElement;
|
||||
let editorView: EditorView;
|
||||
let element: HTMLDivElement;
|
||||
let editorView: EditorView;
|
||||
|
||||
onMount(() => (editorView = create_editor_view(value)));
|
||||
onDestroy(() => editorView?.destroy());
|
||||
onMount(() => (editorView = create_editor_view(value)));
|
||||
onDestroy(() => editorView?.destroy());
|
||||
|
||||
$: editorView && update(value);
|
||||
$: editorView && update(value);
|
||||
|
||||
// There may be a more graceful way to update the two editors
|
||||
function update(value: string): void {
|
||||
editorView?.destroy();
|
||||
editorView = create_editor_view(value);
|
||||
}
|
||||
// There may be a more graceful way to update the two editors
|
||||
function update(value: string): void {
|
||||
editorView?.destroy();
|
||||
editorView = create_editor_view(value);
|
||||
}
|
||||
|
||||
function create_editor_state(
|
||||
value: string | null | undefined
|
||||
): EditorState {
|
||||
return EditorState.create({
|
||||
doc: value ?? undefined,
|
||||
extensions: state_extensions,
|
||||
});
|
||||
}
|
||||
function create_editor_state(value: string | null | undefined): EditorState {
|
||||
return EditorState.create({
|
||||
doc: value ?? undefined,
|
||||
extensions: state_extensions
|
||||
});
|
||||
}
|
||||
|
||||
function create_editor_view(value: string): EditorView {
|
||||
return new EditorView({
|
||||
state: create_editor_state(value),
|
||||
parent: element,
|
||||
});
|
||||
}
|
||||
function create_editor_view(value: string): EditorView {
|
||||
return new EditorView({
|
||||
state: create_editor_state(value),
|
||||
parent: element
|
||||
});
|
||||
}
|
||||
|
||||
let state_extensions = [
|
||||
EditorView.editable.of(false),
|
||||
EditorView.lineWrapping,
|
||||
lineNumbers(),
|
||||
editorTheme,
|
||||
fixedHeightEditor,
|
||||
];
|
||||
let state_extensions = [
|
||||
EditorView.editable.of(false),
|
||||
EditorView.lineWrapping,
|
||||
lineNumbers(),
|
||||
editorTheme,
|
||||
fixedHeightEditor
|
||||
];
|
||||
</script>
|
||||
|
||||
<code>
|
||||
<div bind:this={element} />
|
||||
<div bind:this={element} />
|
||||
</code>
|
||||
|
@ -1,51 +1,45 @@
|
||||
<script lang="ts">
|
||||
import type Users from "$lib/users";
|
||||
import type Api from "$lib/api";
|
||||
import type { LoginToken } from "$lib/api";
|
||||
import { derived, writable } from "svelte/store";
|
||||
import { open } from "@tauri-apps/api/shell";
|
||||
import type Users from '$lib/users';
|
||||
import type Api from '$lib/api';
|
||||
import type { LoginToken } from '$lib/api';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
|
||||
export let user: Awaited<ReturnType<typeof Users>>;
|
||||
export let api: Awaited<ReturnType<typeof Api>>;
|
||||
export let user: Awaited<ReturnType<typeof Users>>;
|
||||
export let api: Awaited<ReturnType<typeof Api>>;
|
||||
|
||||
const pollForUser = async (token: string) => {
|
||||
const apiUser = await api.login.user.get(token).catch(() => null);
|
||||
if (apiUser) {
|
||||
user.set(apiUser);
|
||||
return apiUser;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(async () => {
|
||||
resolve(await pollForUser(token));
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
const pollForUser = async (token: string) => {
|
||||
const apiUser = await api.login.user.get(token).catch(() => null);
|
||||
if (apiUser) {
|
||||
user.set(apiUser);
|
||||
return apiUser;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(async () => {
|
||||
resolve(await pollForUser(token));
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
const token = writable<LoginToken | null>(null);
|
||||
const authUrl = derived(token, ($token) => $token?.url as string);
|
||||
const token = writable<LoginToken | null>(null);
|
||||
const authUrl = derived(token, ($token) => $token?.url as string);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if $user}
|
||||
<button
|
||||
class="text-zinc-400 hover:underline"
|
||||
on:click={() => user.delete()}>Log out</button
|
||||
>
|
||||
{:else if $token !== null}
|
||||
{#await Promise.all([open($token.url), pollForUser($token.token)])}
|
||||
<div>Log in in your system browser</div>
|
||||
{/await}
|
||||
<p>
|
||||
<button class="underline" on:click={() => open($authUrl)}
|
||||
>Click here</button
|
||||
>
|
||||
if you are not redirected automatically, you can
|
||||
</p>
|
||||
{:else}
|
||||
<button
|
||||
class="py-1 px-3 rounded text-white bg-blue-400"
|
||||
on:click={() => api.login.token.create().then(token.set)}
|
||||
>Sign up or Log in</button
|
||||
>
|
||||
{/if}
|
||||
{#if $user}
|
||||
<button class="text-zinc-400 hover:underline" on:click={() => user.delete()}>Log out</button>
|
||||
{:else if $token !== null}
|
||||
{#await Promise.all([open($token.url), pollForUser($token.token)])}
|
||||
<div>Log in in your system browser</div>
|
||||
{/await}
|
||||
<p>
|
||||
<button class="underline" on:click={() => open($authUrl)}>Click here</button>
|
||||
if you are not redirected automatically, you can
|
||||
</p>
|
||||
{:else}
|
||||
<button
|
||||
class="py-1 px-3 rounded text-white bg-blue-400"
|
||||
on:click={() => api.login.token.create().then(token.set)}>Sign up or Log in</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
export { default as CodeViewer } from "./CodeViewer.svelte";
|
||||
export { default as BackForwardButtons } from "./BackForwardButtons.svelte";
|
||||
export { default as Login } from "./Login.svelte";
|
||||
export { default as Breadcrumbs } from "./Breadcrumbs.svelte";
|
||||
export { default as CodeViewer } from './CodeViewer.svelte';
|
||||
export { default as BackForwardButtons } from './BackForwardButtons.svelte';
|
||||
export { default as Login } from './Login.svelte';
|
||||
export { default as Breadcrumbs } from './Breadcrumbs.svelte';
|
||||
|
@ -1,111 +1,104 @@
|
||||
<script lang="ts">
|
||||
import { themeIcons } from "seti-icons";
|
||||
import type { Session } from "$lib/sessions";
|
||||
import { toHumanReadableTime, toHumanReadableDate } from "$lib/time";
|
||||
import { toHumanBranchName } from "$lib/branch";
|
||||
import TimelineDaySessionActivities from "./TimelineDaySessionActivities.svelte";
|
||||
import { list } from "$lib/deltas";
|
||||
export let session: Session;
|
||||
export let projectId: string;
|
||||
import { themeIcons } from 'seti-icons';
|
||||
import type { Session } from '$lib/sessions';
|
||||
import { toHumanReadableTime, toHumanReadableDate } from '$lib/time';
|
||||
import { toHumanBranchName } from '$lib/branch';
|
||||
import TimelineDaySessionActivities from './TimelineDaySessionActivities.svelte';
|
||||
import { list } from '$lib/deltas';
|
||||
export let session: Session;
|
||||
export let projectId: string;
|
||||
|
||||
const getIcon = themeIcons({
|
||||
blue: "#268bd2",
|
||||
grey: "#657b83",
|
||||
"grey-light": "#839496",
|
||||
green: "#859900",
|
||||
orange: "#cb4b16",
|
||||
pink: "#d33682",
|
||||
purple: "#6c71c4",
|
||||
red: "#dc322f",
|
||||
white: "#fdf6e3",
|
||||
yellow: "#b58900",
|
||||
ignore: "#586e75",
|
||||
});
|
||||
const getIcon = themeIcons({
|
||||
blue: '#268bd2',
|
||||
grey: '#657b83',
|
||||
'grey-light': '#839496',
|
||||
green: '#859900',
|
||||
orange: '#cb4b16',
|
||||
pink: '#d33682',
|
||||
purple: '#6c71c4',
|
||||
red: '#dc322f',
|
||||
white: '#fdf6e3',
|
||||
yellow: '#b58900',
|
||||
ignore: '#586e75'
|
||||
});
|
||||
|
||||
function pathToName(path: string) {
|
||||
return path.split("/").slice(-1)[0];
|
||||
}
|
||||
function pathToIconSvg(path: string) {
|
||||
let name: string = pathToName(path);
|
||||
let { svg, color } = getIcon(name);
|
||||
return svg;
|
||||
}
|
||||
const colorFromBranchName = (branchName: string) => {
|
||||
const colors = [
|
||||
"bg-red-500 border-red-700",
|
||||
"bg-green-500 border-green-700",
|
||||
"bg-blue-500 border-blue-700",
|
||||
"bg-yellow-500 border-yellow-700",
|
||||
"bg-purple-500 border-purple-700",
|
||||
"bg-pink-500 border-pink-700",
|
||||
"bg-indigo-500 border-indigo-700",
|
||||
"bg-orange-500 border-orange-700",
|
||||
];
|
||||
const hash = branchName.split("").reduce((acc, char) => {
|
||||
return acc + char.charCodeAt(0);
|
||||
}, 0);
|
||||
return colors[hash % colors.length];
|
||||
};
|
||||
function pathToName(path: string) {
|
||||
return path.split('/').slice(-1)[0];
|
||||
}
|
||||
function pathToIconSvg(path: string) {
|
||||
let name: string = pathToName(path);
|
||||
let { svg, color } = getIcon(name);
|
||||
return svg;
|
||||
}
|
||||
const colorFromBranchName = (branchName: string) => {
|
||||
const colors = [
|
||||
'bg-red-500 border-red-700',
|
||||
'bg-green-500 border-green-700',
|
||||
'bg-blue-500 border-blue-700',
|
||||
'bg-yellow-500 border-yellow-700',
|
||||
'bg-purple-500 border-purple-700',
|
||||
'bg-pink-500 border-pink-700',
|
||||
'bg-indigo-500 border-indigo-700',
|
||||
'bg-orange-500 border-orange-700'
|
||||
];
|
||||
const hash = branchName.split('').reduce((acc, char) => {
|
||||
return acc + char.charCodeAt(0);
|
||||
}, 0);
|
||||
return colors[hash % colors.length];
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col space-y-2">
|
||||
<span class="relative inline-flex">
|
||||
<a
|
||||
id="block"
|
||||
class="inline-flex flex-grow items-center truncate transition ease-in-out duration-150 border px-4 py-2 text-slate-50 rounded-lg {colorFromBranchName(
|
||||
session.meta.branch
|
||||
)}"
|
||||
title={session.meta.branch}
|
||||
href="/projects/{projectId}/sessions/{session.id}/"
|
||||
>
|
||||
{toHumanBranchName(session.meta.branch)}
|
||||
</a>
|
||||
{#if !session.hash}
|
||||
<span
|
||||
class="flex absolute h-3 w-3 top-0 right-0 -mt-1 -mr-1"
|
||||
title="Current session"
|
||||
>
|
||||
<span
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-200 opacity-75"
|
||||
/>
|
||||
<span
|
||||
class="relative inline-flex rounded-full h-3 w-3 bg-zinc-200 border border-orange-200"
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
<div id="activities">
|
||||
<div class="my-2 mx-1">
|
||||
<TimelineDaySessionActivities
|
||||
activities={session.activity}
|
||||
sessionStart={session.meta.startTimestampMs}
|
||||
sessionEnd={session.meta.lastTimestampMs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="time-range" class="">
|
||||
{toHumanReadableDate(session.meta.startTimestampMs)},
|
||||
{toHumanReadableTime(session.meta.startTimestampMs)}
|
||||
<div class=" text-zinc-600">
|
||||
{Math.round(
|
||||
(session.meta.lastTimestampMs - session.meta.startTimestampMs) /
|
||||
60 /
|
||||
1000
|
||||
)} min
|
||||
</div>
|
||||
</div>
|
||||
<div id="files">
|
||||
{#await list( { projectId: projectId, sessionId: session.id } ) then deltas}
|
||||
{#each Object.keys(deltas) as delta}
|
||||
<div class="flex flex-row w-32 items-center">
|
||||
<div class="w-6 h-6 text-white fill-blue-400">
|
||||
{@html pathToIconSvg(delta)}
|
||||
</div>
|
||||
<div class="text-white w-24 truncate">
|
||||
{pathToName(delta)}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/await}
|
||||
</div>
|
||||
<span class="relative inline-flex">
|
||||
<a
|
||||
id="block"
|
||||
class="inline-flex flex-grow items-center truncate transition ease-in-out duration-150 border px-4 py-2 text-slate-50 rounded-lg {colorFromBranchName(
|
||||
session.meta.branch
|
||||
)}"
|
||||
title={session.meta.branch}
|
||||
href="/projects/{projectId}/sessions/{session.id}/"
|
||||
>
|
||||
{toHumanBranchName(session.meta.branch)}
|
||||
</a>
|
||||
{#if !session.hash}
|
||||
<span class="flex absolute h-3 w-3 top-0 right-0 -mt-1 -mr-1" title="Current session">
|
||||
<span
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-200 opacity-75"
|
||||
/>
|
||||
<span
|
||||
class="relative inline-flex rounded-full h-3 w-3 bg-zinc-200 border border-orange-200"
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
<div id="activities">
|
||||
<div class="my-2 mx-1">
|
||||
<TimelineDaySessionActivities
|
||||
activities={session.activity}
|
||||
sessionStart={session.meta.startTimestampMs}
|
||||
sessionEnd={session.meta.lastTimestampMs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="time-range" class="">
|
||||
{toHumanReadableDate(session.meta.startTimestampMs)},
|
||||
{toHumanReadableTime(session.meta.startTimestampMs)}
|
||||
<div class=" text-zinc-600">
|
||||
{Math.round((session.meta.lastTimestampMs - session.meta.startTimestampMs) / 60 / 1000)} min
|
||||
</div>
|
||||
</div>
|
||||
<div id="files">
|
||||
{#await list({ projectId: projectId, sessionId: session.id }) then deltas}
|
||||
{#each Object.keys(deltas) as delta}
|
||||
<div class="flex flex-row w-32 items-center">
|
||||
<div class="w-6 h-6 text-white fill-blue-400">
|
||||
{@html pathToIconSvg(delta)}
|
||||
</div>
|
||||
<div class="text-white w-24 truncate">
|
||||
{pathToName(delta)}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,63 +1,59 @@
|
||||
<script lang="ts">
|
||||
import type { Activity } from "$lib/sessions";
|
||||
import FaSquare from "svelte-icons/fa/FaSquare.svelte";
|
||||
import FaCircle from "svelte-icons/fa/FaCircle.svelte";
|
||||
import FaAdjust from "svelte-icons/fa/FaAdjust.svelte";
|
||||
import FaMapMarker from "svelte-icons/fa/FaMapMarker.svelte";
|
||||
import type { Activity } from '$lib/sessions';
|
||||
import FaSquare from 'svelte-icons/fa/FaSquare.svelte';
|
||||
import FaCircle from 'svelte-icons/fa/FaCircle.svelte';
|
||||
import FaAdjust from 'svelte-icons/fa/FaAdjust.svelte';
|
||||
import FaMapMarker from 'svelte-icons/fa/FaMapMarker.svelte';
|
||||
|
||||
export let activities: Activity[];
|
||||
export let sessionStart: number;
|
||||
export let sessionEnd: number;
|
||||
export let activities: Activity[];
|
||||
export let sessionStart: number;
|
||||
export let sessionEnd: number;
|
||||
|
||||
$: sessionDuration = sessionEnd - sessionStart;
|
||||
$: sessionDuration = sessionEnd - sessionStart;
|
||||
|
||||
let proportionOfTime = (time: number) => {
|
||||
return ((time - sessionStart) / sessionDuration) * 100;
|
||||
};
|
||||
const toHumanReadableTime = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
});
|
||||
};
|
||||
let proportionOfTime = (time: number) => {
|
||||
return ((time - sessionStart) / sessionDuration) * 100;
|
||||
};
|
||||
const toHumanReadableTime = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<hr class="h-px bg-slate-400 border-0 z-0" />
|
||||
<div class="absolute inset-0 -mt-1.5">
|
||||
{#each activities as activity}
|
||||
<div
|
||||
class="flex -mx-1.5"
|
||||
style="position:relative; left: {proportionOfTime(
|
||||
activity.timestampMs
|
||||
)}%;"
|
||||
>
|
||||
<div
|
||||
class="w-3 h-3 text-slate-700 z-50 absolute inset-0"
|
||||
style=""
|
||||
title="{activity.type}: {activity.message} at {toHumanReadableTime(
|
||||
activity.timestampMs
|
||||
)}"
|
||||
>
|
||||
{#if activity.type === "commit"}
|
||||
<div class="text-sky-500 hover:text-sky-600">
|
||||
<FaSquare />
|
||||
</div>
|
||||
{:else if activity.type === "merge"}
|
||||
<div class="text-green-500 hover:text-green-600">
|
||||
<FaMapMarker />
|
||||
</div>
|
||||
{:else if activity.type === "rebase"}
|
||||
<div class="text-orange-500 hover:text-orange-600">
|
||||
<FaAdjust />
|
||||
</div>
|
||||
{:else if activity.type === "push"}
|
||||
<div class="text-purple-500 hover:text-purple-600">
|
||||
<FaCircle />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<hr class="h-px bg-slate-400 border-0 z-0" />
|
||||
<div class="absolute inset-0 -mt-1.5">
|
||||
{#each activities as activity}
|
||||
<div
|
||||
class="flex -mx-1.5"
|
||||
style="position:relative; left: {proportionOfTime(activity.timestampMs)}%;"
|
||||
>
|
||||
<div
|
||||
class="w-3 h-3 text-slate-700 z-50 absolute inset-0"
|
||||
style=""
|
||||
title="{activity.type}: {activity.message} at {toHumanReadableTime(activity.timestampMs)}"
|
||||
>
|
||||
{#if activity.type === 'commit'}
|
||||
<div class="text-sky-500 hover:text-sky-600">
|
||||
<FaSquare />
|
||||
</div>
|
||||
{:else if activity.type === 'merge'}
|
||||
<div class="text-green-500 hover:text-green-600">
|
||||
<FaMapMarker />
|
||||
</div>
|
||||
{:else if activity.type === 'rebase'}
|
||||
<div class="text-orange-500 hover:text-orange-600">
|
||||
<FaAdjust />
|
||||
</div>
|
||||
{:else if activity.type === 'push'}
|
||||
<div class="text-purple-500 hover:text-purple-600">
|
||||
<FaCircle />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1 +1 @@
|
||||
export { default as TimelineDaySession } from "./TimelineDaySession.svelte";
|
||||
export { default as TimelineDaySession } from './TimelineDaySession.svelte';
|
||||
|
@ -1,50 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { toHumanBranchName } from "$lib/branch";
|
||||
import { toHumanBranchName } from '$lib/branch';
|
||||
|
||||
export let startTime: Date;
|
||||
export let endTime: Date;
|
||||
export let label: string;
|
||||
export let href: string;
|
||||
export let startTime: Date;
|
||||
export let endTime: Date;
|
||||
export let label: string;
|
||||
export let href: string;
|
||||
|
||||
const timeToGridRow = (time: Date) => {
|
||||
const hours = time.getHours();
|
||||
const minutes = time.getMinutes();
|
||||
const totalMinutes = hours * 60 + minutes;
|
||||
const totalMinutesPerDay = 24 * 60;
|
||||
const gridRow = Math.floor((totalMinutes / totalMinutesPerDay) * 96);
|
||||
return gridRow + 1; // offset the first row
|
||||
};
|
||||
const timeToGridRow = (time: Date) => {
|
||||
const hours = time.getHours();
|
||||
const minutes = time.getMinutes();
|
||||
const totalMinutes = hours * 60 + minutes;
|
||||
const totalMinutesPerDay = 24 * 60;
|
||||
const gridRow = Math.floor((totalMinutes / totalMinutesPerDay) * 96);
|
||||
return gridRow + 1; // offset the first row
|
||||
};
|
||||
|
||||
const dateToGridCol = (date: Date) => {
|
||||
return date.getDay();
|
||||
};
|
||||
const dateToGridCol = (date: Date) => {
|
||||
return date.getDay();
|
||||
};
|
||||
|
||||
const timeToSpan = (startTime: Date, endTime: Date) => {
|
||||
const startMinutes = startTime.getHours() * 60 + startTime.getMinutes();
|
||||
const endMinutes = endTime.getHours() * 60 + endTime.getMinutes();
|
||||
const span = Math.round((endMinutes - startMinutes) / 15); // 4 spans per hour
|
||||
if (span < 1) {
|
||||
return 1;
|
||||
} else {
|
||||
return span;
|
||||
}
|
||||
};
|
||||
const timeToSpan = (startTime: Date, endTime: Date) => {
|
||||
const startMinutes = startTime.getHours() * 60 + startTime.getMinutes();
|
||||
const endMinutes = endTime.getHours() * 60 + endTime.getMinutes();
|
||||
const span = Math.round((endMinutes - startMinutes) / 15); // 4 spans per hour
|
||||
if (span < 1) {
|
||||
return 1;
|
||||
} else {
|
||||
return span;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<li
|
||||
class="relative mt-px flex col-start-{dateToGridCol(startTime)}"
|
||||
style="grid-row: {timeToGridRow(startTime)} / span {timeToSpan(
|
||||
startTime,
|
||||
endTime
|
||||
)};"
|
||||
class="relative mt-px flex col-start-{dateToGridCol(startTime)}"
|
||||
style="grid-row: {timeToGridRow(startTime)} / span {timeToSpan(startTime, endTime)};"
|
||||
>
|
||||
<a
|
||||
{href}
|
||||
title={startTime.toLocaleTimeString()}
|
||||
class="group absolute inset-1 flex flex-col items-center justify-center rounded-lg bg-zinc-300 p-3 leading-5 hover:bg-zinc-200 shadow"
|
||||
>
|
||||
<p class="order-1 font-semibold text-zinc-800">
|
||||
{toHumanBranchName(label)}
|
||||
</p>
|
||||
</a>
|
||||
<a
|
||||
{href}
|
||||
title={startTime.toLocaleTimeString()}
|
||||
class="group absolute inset-1 flex flex-col items-center justify-center rounded-lg bg-zinc-300 p-3 leading-5 hover:bg-zinc-200 shadow"
|
||||
>
|
||||
<p class="order-1 font-semibold text-zinc-800">
|
||||
{toHumanBranchName(label)}
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -1 +1 @@
|
||||
export { default as WeekBlockEntry } from "./WeekBlockEntry.svelte";
|
||||
export { default as WeekBlockEntry } from './WeekBlockEntry.svelte';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { log } from "$lib";
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { writable, type Readable } from "svelte/store";
|
||||
import { log } from '$lib';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
|
||||
export type OperationDelete = { delete: [number, number] };
|
||||
export type OperationInsert = { insert: [number, string] };
|
||||
@ -9,37 +9,35 @@ export type OperationInsert = { insert: [number, string] };
|
||||
export type Operation = OperationDelete | OperationInsert;
|
||||
|
||||
export namespace Operation {
|
||||
export const isDelete = (
|
||||
operation: Operation
|
||||
): operation is OperationDelete => "delete" in operation;
|
||||
export const isDelete = (operation: Operation): operation is OperationDelete =>
|
||||
'delete' in operation;
|
||||
|
||||
export const isInsert = (
|
||||
operation: Operation
|
||||
): operation is OperationInsert => "insert" in operation;
|
||||
export const isInsert = (operation: Operation): operation is OperationInsert =>
|
||||
'insert' in operation;
|
||||
}
|
||||
|
||||
export type Delta = { timestampMs: number; operations: Operation[] };
|
||||
|
||||
type DeltasEvent = {
|
||||
deltas: Delta[];
|
||||
filePath: string;
|
||||
deltas: Delta[];
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
export const list = (params: { projectId: string; sessionId: string }) =>
|
||||
invoke<Record<string, Delta[]>>("list_deltas", params);
|
||||
invoke<Record<string, Delta[]>>('list_deltas', params);
|
||||
|
||||
export default async (params: { projectId: string; sessionId: string }) => {
|
||||
const init = await list(params);
|
||||
const init = await list(params);
|
||||
|
||||
const store = writable<Record<string, Delta[]>>(init);
|
||||
const eventName = `project://${params.projectId}/sessions/${params.sessionId}/deltas`;
|
||||
await appWindow.listen<DeltasEvent>(eventName, (event) => {
|
||||
log.info(`Received deltas event ${eventName}`);
|
||||
store.update((deltas) => ({
|
||||
...deltas,
|
||||
[event.payload.filePath]: event.payload.deltas,
|
||||
}));
|
||||
});
|
||||
const store = writable<Record<string, Delta[]>>(init);
|
||||
const eventName = `project://${params.projectId}/sessions/${params.sessionId}/deltas`;
|
||||
await appWindow.listen<DeltasEvent>(eventName, (event) => {
|
||||
log.info(`Received deltas event ${eventName}`);
|
||||
store.update((deltas) => ({
|
||||
...deltas,
|
||||
[event.payload.filePath]: event.payload.deltas
|
||||
}));
|
||||
});
|
||||
|
||||
return store as Readable<Record<string, Delta[]>>;
|
||||
return store as Readable<Record<string, Delta[]>>;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
export * as deltas from "./deltas";
|
||||
export * as projects from "./projects";
|
||||
export * as log from "./log";
|
||||
export * as toasts from "./toasts";
|
||||
export * as sessions from "./sessions";
|
||||
export * as week from "./week";
|
||||
export * as deltas from './deltas';
|
||||
export * as projects from './projects';
|
||||
export * as log from './log';
|
||||
export * as toasts from './toasts';
|
||||
export * as sessions from './sessions';
|
||||
export * as week from './week';
|
||||
|
@ -1,39 +1,36 @@
|
||||
import { building } from "$app/environment";
|
||||
import { building } from '$app/environment';
|
||||
|
||||
export const setup = async () => {
|
||||
if (!building) {
|
||||
await (await import("tauri-plugin-log-api")).attachConsole();
|
||||
}
|
||||
if (!building) {
|
||||
await (await import('tauri-plugin-log-api')).attachConsole();
|
||||
}
|
||||
};
|
||||
|
||||
const logger = async () =>
|
||||
building
|
||||
? {
|
||||
debug: (..._: any[]) => { },
|
||||
info: (..._: any[]) => { },
|
||||
error: (..._: any[]) => { },
|
||||
}
|
||||
: import("tauri-plugin-log-api").then((tauri) => ({
|
||||
debug: tauri.debug,
|
||||
info: tauri.info,
|
||||
error: tauri.error,
|
||||
}));
|
||||
building
|
||||
? {
|
||||
debug: (..._: any[]) => {},
|
||||
info: (..._: any[]) => {},
|
||||
error: (..._: any[]) => {}
|
||||
}
|
||||
: import('tauri-plugin-log-api').then((tauri) => ({
|
||||
debug: tauri.debug,
|
||||
info: tauri.info,
|
||||
error: tauri.error
|
||||
}));
|
||||
|
||||
const toString = (value: any) => {
|
||||
if (value instanceof Error) {
|
||||
return value.message;
|
||||
} else if (typeof value === "object") {
|
||||
return JSON.stringify(value);
|
||||
} else {
|
||||
return value.toString();
|
||||
}
|
||||
if (value instanceof Error) {
|
||||
return value.message;
|
||||
} else if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
} else {
|
||||
return value.toString();
|
||||
}
|
||||
};
|
||||
|
||||
export const debug = async (...args: any[]) =>
|
||||
(await logger()).debug(args.map(toString).join(" "));
|
||||
export const debug = async (...args: any[]) => (await logger()).debug(args.map(toString).join(' '));
|
||||
|
||||
export const info = async (...args: any[]) =>
|
||||
(await logger()).info(args.map(toString).join(" "));
|
||||
export const info = async (...args: any[]) => (await logger()).info(args.map(toString).join(' '));
|
||||
|
||||
export const error = async (...args: any[]) =>
|
||||
(await logger()).error(args.map(toString).join(" "));
|
||||
export const error = async (...args: any[]) => (await logger()).error(args.map(toString).join(' '));
|
||||
|
@ -1,31 +1,31 @@
|
||||
import posthog from "posthog-js";
|
||||
import { PUBLIC_POSTHOG_API_KEY } from "$env/static/public";
|
||||
import type { User } from "$lib/api";
|
||||
import * as log from "$lib/log";
|
||||
import posthog from 'posthog-js';
|
||||
import { PUBLIC_POSTHOG_API_KEY } from '$env/static/public';
|
||||
import type { User } from '$lib/api';
|
||||
import * as log from '$lib/log';
|
||||
|
||||
export default () => {
|
||||
const instance = posthog.init(PUBLIC_POSTHOG_API_KEY, {
|
||||
api_host: "https://eu.posthog.com",
|
||||
capture_performance: false,
|
||||
});
|
||||
log.info("posthog initialized");
|
||||
return {
|
||||
identify: (user: User | undefined) => {
|
||||
if (user) {
|
||||
log.info("posthog identify", {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
});
|
||||
instance?.identify(`user_${user.id}`, {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
});
|
||||
} else {
|
||||
log.info("posthog reset");
|
||||
instance?.capture("log-out");
|
||||
instance?.reset();
|
||||
}
|
||||
},
|
||||
};
|
||||
const instance = posthog.init(PUBLIC_POSTHOG_API_KEY, {
|
||||
api_host: 'https://eu.posthog.com',
|
||||
capture_performance: false
|
||||
});
|
||||
log.info('posthog initialized');
|
||||
return {
|
||||
identify: (user: User | undefined) => {
|
||||
if (user) {
|
||||
log.info('posthog identify', {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email
|
||||
});
|
||||
instance?.identify(`user_${user.id}`, {
|
||||
email: user.email,
|
||||
name: user.name
|
||||
});
|
||||
} else {
|
||||
log.info('posthog reset');
|
||||
instance?.capture('log-out');
|
||||
instance?.reset();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,67 +1,58 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { derived, writable } from "svelte/store";
|
||||
import type { Project as ApiProject } from "$lib/api";
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import type { Project as ApiProject } from '$lib/api';
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
api: ApiProject & { sync: boolean };
|
||||
id: string;
|
||||
title: string;
|
||||
path: string;
|
||||
api: ApiProject & { sync: boolean };
|
||||
};
|
||||
|
||||
const list = () => invoke<Project[]>("list_projects");
|
||||
const list = () => invoke<Project[]>('list_projects');
|
||||
|
||||
const update = (params: {
|
||||
project: {
|
||||
id: string;
|
||||
title?: string;
|
||||
api?: ApiProject & { sync: boolean };
|
||||
};
|
||||
}) => invoke<Project>("update_project", params);
|
||||
project: {
|
||||
id: string;
|
||||
title?: string;
|
||||
api?: ApiProject & { sync: boolean };
|
||||
};
|
||||
}) => invoke<Project>('update_project', params);
|
||||
|
||||
const add = (params: { path: string }) =>
|
||||
invoke<Project>("add_project", params);
|
||||
const add = (params: { path: string }) => invoke<Project>('add_project', params);
|
||||
|
||||
const del = (params: { id: string }) => invoke("delete_project", params);
|
||||
const del = (params: { id: string }) => invoke('delete_project', params);
|
||||
|
||||
export default async () => {
|
||||
const init = await list();
|
||||
const store = writable<Project[]>(init);
|
||||
const init = await list();
|
||||
const store = writable<Project[]>(init);
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
get: (id: string) => {
|
||||
const project = derived(store, (store) =>
|
||||
store.find((p) => p.id === id)
|
||||
);
|
||||
return {
|
||||
subscribe: project.subscribe,
|
||||
update: (params: { title?: string; api?: Project["api"] }) =>
|
||||
update({
|
||||
project: {
|
||||
id,
|
||||
...params,
|
||||
},
|
||||
}).then((project) => {
|
||||
store.update((projects) =>
|
||||
projects.map((p) =>
|
||||
p.id === project.id ? project : p
|
||||
)
|
||||
);
|
||||
return project;
|
||||
}),
|
||||
};
|
||||
},
|
||||
add: (params: { path: string }) =>
|
||||
add(params).then((project) => {
|
||||
store.update((projects) => [...projects, project]);
|
||||
return project;
|
||||
}),
|
||||
delete: (params: { id: string }) =>
|
||||
del(params).then(() => {
|
||||
store.update((projects) =>
|
||||
projects.filter((p) => p.id !== params.id)
|
||||
);
|
||||
}),
|
||||
};
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
get: (id: string) => {
|
||||
const project = derived(store, (store) => store.find((p) => p.id === id));
|
||||
return {
|
||||
subscribe: project.subscribe,
|
||||
update: (params: { title?: string; api?: Project['api'] }) =>
|
||||
update({
|
||||
project: {
|
||||
id,
|
||||
...params
|
||||
}
|
||||
}).then((project) => {
|
||||
store.update((projects) => projects.map((p) => (p.id === project.id ? project : p)));
|
||||
return project;
|
||||
})
|
||||
};
|
||||
},
|
||||
add: (params: { path: string }) =>
|
||||
add(params).then((project) => {
|
||||
store.update((projects) => [...projects, project]);
|
||||
return project;
|
||||
}),
|
||||
delete: (params: { id: string }) =>
|
||||
del(params).then(() => {
|
||||
store.update((projects) => projects.filter((p) => p.id !== params.id));
|
||||
})
|
||||
};
|
||||
};
|
||||
|
@ -1,56 +1,49 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { writable } from "svelte/store";
|
||||
import { log } from "$lib";
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
export type Activity = {
|
||||
type: string;
|
||||
timestampMs: number;
|
||||
message: string;
|
||||
type: string;
|
||||
timestampMs: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type Session = {
|
||||
id: string;
|
||||
hash?: string;
|
||||
meta: {
|
||||
startTimestampMs: number;
|
||||
lastTimestampMs: number;
|
||||
branch: string;
|
||||
commit: string;
|
||||
};
|
||||
activity: Activity[];
|
||||
id: string;
|
||||
hash?: string;
|
||||
meta: {
|
||||
startTimestampMs: number;
|
||||
lastTimestampMs: number;
|
||||
branch: string;
|
||||
commit: string;
|
||||
};
|
||||
activity: Activity[];
|
||||
};
|
||||
|
||||
export const listFiles = (params: { projectId: string; sessionId: string }) =>
|
||||
invoke<Record<string, string>>("list_session_files", params);
|
||||
invoke<Record<string, string>>('list_session_files', params);
|
||||
|
||||
const list = (params: { projectId: string }) =>
|
||||
invoke<Session[]>("list_sessions", params);
|
||||
const list = (params: { projectId: string }) => invoke<Session[]>('list_sessions', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const init = await list(params);
|
||||
const store = writable(init);
|
||||
const eventName = `project://${params.projectId}/sessions`;
|
||||
const init = await list(params);
|
||||
const store = writable(init);
|
||||
const eventName = `project://${params.projectId}/sessions`;
|
||||
|
||||
await appWindow.listen<Session>(eventName, (event) => {
|
||||
log.info(`Received sessions event ${eventName}`);
|
||||
store.update((sessions) => {
|
||||
const index = sessions.findIndex(
|
||||
(session) => session.id === event.payload.id
|
||||
);
|
||||
if (index === -1) {
|
||||
return [...sessions, event.payload];
|
||||
} else {
|
||||
return [
|
||||
...sessions.slice(0, index),
|
||||
event.payload,
|
||||
...sessions.slice(index + 1),
|
||||
];
|
||||
}
|
||||
});
|
||||
});
|
||||
await appWindow.listen<Session>(eventName, (event) => {
|
||||
log.info(`Received sessions event ${eventName}`);
|
||||
store.update((sessions) => {
|
||||
const index = sessions.findIndex((session) => session.id === event.payload.id);
|
||||
if (index === -1) {
|
||||
return [...sessions, event.payload];
|
||||
} else {
|
||||
return [...sessions.slice(0, index), event.payload, ...sessions.slice(index + 1)];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
};
|
||||
return {
|
||||
subscribe: store.subscribe
|
||||
};
|
||||
};
|
||||
|
@ -1,12 +1,12 @@
|
||||
export const toHumanReadableTime = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
});
|
||||
return new Date(timestamp).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
export const toHumanReadableDate = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleDateString("en-US", {
|
||||
dateStyle: "short",
|
||||
});
|
||||
return new Date(timestamp).toLocaleDateString('en-US', {
|
||||
dateStyle: 'short'
|
||||
});
|
||||
};
|
||||
|
@ -1,15 +1,12 @@
|
||||
import toast, {
|
||||
type ToastOptions,
|
||||
type ToastPosition,
|
||||
} from "svelte-french-toast";
|
||||
import toast, { type ToastOptions, type ToastPosition } from 'svelte-french-toast';
|
||||
|
||||
const defaultOptions = {
|
||||
position: "bottom-center" as ToastPosition,
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
position: 'bottom-center' as ToastPosition,
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
};
|
||||
|
||||
export const error = (msg: string, options: ToastOptions = {}) =>
|
||||
toast.error(msg, { ...defaultOptions, ...options });
|
||||
toast.error(msg, { ...defaultOptions, ...options });
|
||||
|
||||
export const success = (msg: string, options: ToastOptions = {}) =>
|
||||
toast.success(msg, { ...defaultOptions, ...options });
|
||||
toast.success(msg, { ...defaultOptions, ...options });
|
||||
|
@ -1,27 +1,27 @@
|
||||
import type { User } from "$lib/api";
|
||||
import { writable } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import type { User } from '$lib/api';
|
||||
import { writable } from 'svelte/store';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
const get = () => invoke<User | undefined>("get_user");
|
||||
const get = () => invoke<User | undefined>('get_user');
|
||||
|
||||
const set = (params: { user: User }) => invoke<void>("set_user", params);
|
||||
const set = (params: { user: User }) => invoke<void>('set_user', params);
|
||||
|
||||
const del = () => invoke<void>("delete_user");
|
||||
const del = () => invoke<void>('delete_user');
|
||||
|
||||
export default async () => {
|
||||
const store = writable<User | undefined>(undefined);
|
||||
const store = writable<User | undefined>(undefined);
|
||||
|
||||
const init = await get();
|
||||
store.set(init);
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
set: async (user: User) => {
|
||||
await set({ user });
|
||||
store.set(user);
|
||||
},
|
||||
delete: async () => {
|
||||
await del();
|
||||
store.set(undefined);
|
||||
},
|
||||
};
|
||||
const init = await get();
|
||||
store.set(init);
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
set: async (user: User) => {
|
||||
await set({ user });
|
||||
store.set(user);
|
||||
},
|
||||
delete: async () => {
|
||||
await del();
|
||||
store.set(undefined);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,23 +1,23 @@
|
||||
import { startOfWeek, endOfWeek, addWeeks, subWeeks, addDays } from "date-fns";
|
||||
import { startOfWeek, endOfWeek, addWeeks, subWeeks, addDays } from 'date-fns';
|
||||
|
||||
export type Week = {
|
||||
start: Date;
|
||||
end: Date;
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
export namespace Week {
|
||||
export const from = (date: Date): Week => {
|
||||
return {
|
||||
start: startOfWeek(date, { weekStartsOn: 1 }),
|
||||
end: endOfWeek(date),
|
||||
};
|
||||
};
|
||||
export const next = (week: Week): Week => {
|
||||
return { start: addWeeks(week.start, 1), end: addWeeks(week.end, 1) };
|
||||
};
|
||||
export const previous = (week: Week): Week => {
|
||||
return { start: subWeeks(week.start, 1), end: subWeeks(week.end, 1) };
|
||||
};
|
||||
export const nThDay = (week: Week, n: number): Date => {
|
||||
return addDays(week.start, n);
|
||||
};
|
||||
export const from = (date: Date): Week => {
|
||||
return {
|
||||
start: startOfWeek(date, { weekStartsOn: 1 }),
|
||||
end: endOfWeek(date)
|
||||
};
|
||||
};
|
||||
export const next = (week: Week): Week => {
|
||||
return { start: addWeeks(week.start, 1), end: addWeeks(week.end, 1) };
|
||||
};
|
||||
export const previous = (week: Week): Week => {
|
||||
return { start: subWeeks(week.start, 1), end: subWeeks(week.end, 1) };
|
||||
};
|
||||
export const nThDay = (week: Week, n: number): Date => {
|
||||
return addDays(week.start, n);
|
||||
};
|
||||
}
|
||||
|
@ -1,52 +1,45 @@
|
||||
<script lang="ts">
|
||||
import "../app.postcss";
|
||||
import '../app.postcss';
|
||||
|
||||
import { Toaster } from "svelte-french-toast";
|
||||
import type { LayoutData } from "./$types";
|
||||
import { BackForwardButtons } from "$lib/components";
|
||||
import { setContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import Breadcrumbs from "$lib/components/Breadcrumbs.svelte";
|
||||
import { Toaster } from 'svelte-french-toast';
|
||||
import type { LayoutData } from './$types';
|
||||
import { BackForwardButtons } from '$lib/components';
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import Breadcrumbs from '$lib/components/Breadcrumbs.svelte';
|
||||
|
||||
export let data: LayoutData;
|
||||
const { user, posthog } = data;
|
||||
export let data: LayoutData;
|
||||
const { user, posthog } = data;
|
||||
|
||||
setContext("project", writable(null));
|
||||
setContext("session", writable(null));
|
||||
setContext('project', writable(null));
|
||||
setContext('session', writable(null));
|
||||
|
||||
user.subscribe(posthog.identify);
|
||||
user.subscribe(posthog.identify);
|
||||
</script>
|
||||
|
||||
<header
|
||||
data-tauri-drag-region
|
||||
class="sticky top-0 z-50 flex flex-row items-center h-8 overflow-hidden border-b select-none text-zinc-400 border-zinc-700 bg-zinc-900 "
|
||||
data-tauri-drag-region
|
||||
class="sticky top-0 z-50 flex flex-row items-center h-8 overflow-hidden border-b select-none text-zinc-400 border-zinc-700 bg-zinc-900 "
|
||||
>
|
||||
<div class="ml-24">
|
||||
<BackForwardButtons />
|
||||
</div>
|
||||
<div class="ml-6"><Breadcrumbs /></div>
|
||||
<div class="flex-grow" />
|
||||
<a
|
||||
href="/users/"
|
||||
class="flex items-center gap-2 mr-4 font-medium hover:text-zinc-200"
|
||||
>
|
||||
{#if $user}
|
||||
{#if $user.picture}
|
||||
<img
|
||||
class="inline-block w-6 h-6 rounded-full"
|
||||
src={$user.picture}
|
||||
alt="Avatar"
|
||||
/>
|
||||
{/if}
|
||||
<span>{$user.name}</span>
|
||||
{:else}
|
||||
<span>Connect to GitButler Cloud</span>
|
||||
{/if}
|
||||
</a>
|
||||
<div class="ml-24">
|
||||
<BackForwardButtons />
|
||||
</div>
|
||||
<div class="ml-6"><Breadcrumbs /></div>
|
||||
<div class="flex-grow" />
|
||||
<a href="/users/" class="flex items-center gap-2 mr-4 font-medium hover:text-zinc-200">
|
||||
{#if $user}
|
||||
{#if $user.picture}
|
||||
<img class="inline-block w-6 h-6 rounded-full" src={$user.picture} alt="Avatar" />
|
||||
{/if}
|
||||
<span>{$user.name}</span>
|
||||
{:else}
|
||||
<span>Connect to GitButler Cloud</span>
|
||||
{/if}
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<div class="h-0 min-h-full bg-zinc-800 text-zinc-400">
|
||||
<slot />
|
||||
<slot />
|
||||
|
||||
<Toaster />
|
||||
<Toaster />
|
||||
</div>
|
||||
|
@ -1,43 +1,43 @@
|
||||
import { readable } from "svelte/store";
|
||||
import type { LayoutLoad } from "./$types";
|
||||
import { building } from "$app/environment";
|
||||
import type { Project } from "$lib/projects";
|
||||
import Api from "$lib/api";
|
||||
import Posthog from "$lib/posthog";
|
||||
import * as log from "$lib/log";
|
||||
import { readable } from 'svelte/store';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { building } from '$app/environment';
|
||||
import type { Project } from '$lib/projects';
|
||||
import Api from '$lib/api';
|
||||
import Posthog from '$lib/posthog';
|
||||
import * as log from '$lib/log';
|
||||
|
||||
export const ssr = false;
|
||||
export const prerender = true;
|
||||
export const csr = true;
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch }) => {
|
||||
const projects = building
|
||||
? {
|
||||
...readable<Project[]>([]),
|
||||
add: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
get: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
}
|
||||
: await (await import("$lib/projects")).default();
|
||||
const user = building
|
||||
? {
|
||||
...readable<undefined>(undefined),
|
||||
set: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
delete: () => {
|
||||
throw new Error("not implemented");
|
||||
},
|
||||
}
|
||||
: await (await import("$lib/users")).default();
|
||||
await log.setup();
|
||||
return {
|
||||
projects,
|
||||
user,
|
||||
api: Api({ fetch }),
|
||||
posthog: Posthog(),
|
||||
};
|
||||
const projects = building
|
||||
? {
|
||||
...readable<Project[]>([]),
|
||||
add: () => {
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
get: () => {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
: await (await import('$lib/projects')).default();
|
||||
const user = building
|
||||
? {
|
||||
...readable<undefined>(undefined),
|
||||
set: () => {
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
delete: () => {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
: await (await import('$lib/users')).default();
|
||||
await log.setup();
|
||||
return {
|
||||
projects,
|
||||
user,
|
||||
api: Api({ fetch }),
|
||||
posthog: Posthog()
|
||||
};
|
||||
};
|
||||
|
@ -1,59 +1,53 @@
|
||||
<script lang="ts">
|
||||
import FaFolderOpen from "svelte-icons/fa/FaFolderOpen.svelte";
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import type { LayoutData } from "./$types";
|
||||
export let data: LayoutData;
|
||||
import FaFolderOpen from 'svelte-icons/fa/FaFolderOpen.svelte';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import type { LayoutData } from './$types';
|
||||
export let data: LayoutData;
|
||||
|
||||
const { projects } = data;
|
||||
const { projects } = data;
|
||||
|
||||
const onAddLocalRepositoryClick = async () => {
|
||||
const selectedPath = await open({
|
||||
directory: true,
|
||||
recursive: true,
|
||||
});
|
||||
if (selectedPath === null) return;
|
||||
if (Array.isArray(selectedPath) && selectedPath.length !== 1) return;
|
||||
const projectPath = Array.isArray(selectedPath)
|
||||
? selectedPath[0]
|
||||
: selectedPath;
|
||||
const onAddLocalRepositoryClick = async () => {
|
||||
const selectedPath = await open({
|
||||
directory: true,
|
||||
recursive: true
|
||||
});
|
||||
if (selectedPath === null) return;
|
||||
if (Array.isArray(selectedPath) && selectedPath.length !== 1) return;
|
||||
const projectPath = Array.isArray(selectedPath) ? selectedPath[0] : selectedPath;
|
||||
|
||||
const projectExists = $projects.some((p) => p.path === projectPath);
|
||||
if (projectExists) return;
|
||||
const projectExists = $projects.some((p) => p.path === projectPath);
|
||||
if (projectExists) return;
|
||||
|
||||
await projects.add({ path: projectPath });
|
||||
};
|
||||
await projects.add({ path: projectPath });
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="select-none p-16">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-4xl text-zinc-200 mb-2">GitButler</h1>
|
||||
<h2 class="text-2xl">Your Personal VCS Assistant</h2>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<h1 class="text-4xl text-zinc-200 mb-2">GitButler</h1>
|
||||
<h2 class="text-2xl">Your Personal VCS Assistant</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row">
|
||||
<div class="w-1/2">
|
||||
<div class="text-xl text-zinc-200 mb-1">Start</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-4 h-4"><FaFolderOpen /></div>
|
||||
<button
|
||||
on:click={onAddLocalRepositoryClick}
|
||||
class="hover:text-zinc-200">Add a local repository...</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2">
|
||||
<div class="text-xl text-zinc-200 mb-1">Recent</div>
|
||||
<div class="flex flex-col space-y-1">
|
||||
{#each $projects as project}
|
||||
<div class="space-x-2">
|
||||
<a
|
||||
class="hover:text-zinc-200"
|
||||
href="/projects/{project.id}/">{project.title}</a
|
||||
>
|
||||
<span class="text-zinc-500">{project.path}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="w-1/2">
|
||||
<div class="text-xl text-zinc-200 mb-1">Start</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-4 h-4"><FaFolderOpen /></div>
|
||||
<button on:click={onAddLocalRepositoryClick} class="hover:text-zinc-200"
|
||||
>Add a local repository...</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2">
|
||||
<div class="text-xl text-zinc-200 mb-1">Recent</div>
|
||||
<div class="flex flex-col space-y-1">
|
||||
{#each $projects as project}
|
||||
<div class="space-x-2">
|
||||
<a class="hover:text-zinc-200" href="/projects/{project.id}/">{project.title}</a>
|
||||
<span class="text-zinc-500">{project.path}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,128 +1,113 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from "./$types";
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { Project } from "$lib/projects";
|
||||
import { onDestroy } from "svelte";
|
||||
import { page } from "$app/stores";
|
||||
import type { LayoutData } from './$types';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Project } from '$lib/projects';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let data: LayoutData;
|
||||
export let data: LayoutData;
|
||||
|
||||
$: project = data.project;
|
||||
$: sessions = data.sessions;
|
||||
$: lastSessionId = $sessions[$sessions.length - 1]?.id;
|
||||
$: project = data.project;
|
||||
$: sessions = data.sessions;
|
||||
$: lastSessionId = $sessions[$sessions.length - 1]?.id;
|
||||
|
||||
function projectUrl(project: Project) {
|
||||
const gitUrl = project.api?.git_url;
|
||||
// get host from git url
|
||||
const url = new URL(gitUrl);
|
||||
const host = url.origin;
|
||||
const projectId = gitUrl.split("/").pop();
|
||||
function projectUrl(project: Project) {
|
||||
const gitUrl = project.api?.git_url;
|
||||
// get host from git url
|
||||
const url = new URL(gitUrl);
|
||||
const host = url.origin;
|
||||
const projectId = gitUrl.split('/').pop();
|
||||
|
||||
return `${host}/projects/${projectId}`;
|
||||
}
|
||||
return `${host}/projects/${projectId}`;
|
||||
}
|
||||
|
||||
const contextProjectStore: Writable<Project | null | undefined> =
|
||||
getContext("project");
|
||||
$: contextProjectStore.set($project);
|
||||
onDestroy(() => {
|
||||
contextProjectStore.set(null);
|
||||
});
|
||||
const contextProjectStore: Writable<Project | null | undefined> = getContext('project');
|
||||
$: contextProjectStore.set($project);
|
||||
onDestroy(() => {
|
||||
contextProjectStore.set(null);
|
||||
});
|
||||
|
||||
$: selection = $page?.route?.id?.split("/")?.[3];
|
||||
$: selection = $page?.route?.id?.split('/')?.[3];
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class="flex items-center flex-none justify-between py-2 px-8 space-x-3 border-b select-none text-zinc-300 border-zinc-700"
|
||||
class="flex items-center flex-none justify-between py-2 px-8 space-x-3 border-b select-none text-zinc-300 border-zinc-700"
|
||||
>
|
||||
<div
|
||||
class="text-zinc-400 w-64 font-medium grid grid-cols-3 items-center bg-zinc-700/50 rounded-lg h-7 px-4 gap-1"
|
||||
>
|
||||
<a
|
||||
class="
|
||||
<div
|
||||
class="text-zinc-400 w-64 font-medium grid grid-cols-3 items-center bg-zinc-700/50 rounded-lg h-7 px-4 gap-1"
|
||||
>
|
||||
<a
|
||||
class="
|
||||
{selection === 'week' ? 'bg-zinc-600/70 text-zinc-100' : ''}
|
||||
rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100"
|
||||
href="/projects/{$project?.id}/week">Week</a
|
||||
>
|
||||
<a
|
||||
href="/projects/{$project?.id}/day"
|
||||
class="
|
||||
href="/projects/{$project?.id}/week">Week</a
|
||||
>
|
||||
<a
|
||||
href="/projects/{$project?.id}/day"
|
||||
class="
|
||||
{selection === 'day' ? 'bg-zinc-600/70 text-zinc-100' : ''}
|
||||
rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100"
|
||||
>Day</a
|
||||
>
|
||||
<a
|
||||
href="/projects/{$project?.id}/sessions/{lastSessionId}"
|
||||
class="
|
||||
rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100">Day</a
|
||||
>
|
||||
<a
|
||||
href="/projects/{$project?.id}/sessions/{lastSessionId}"
|
||||
class="
|
||||
{selection === 'sessions' ? 'bg-zinc-600/70 text-zinc-100' : ''}
|
||||
rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100"
|
||||
title="go to current session">Session</a
|
||||
>
|
||||
</div>
|
||||
title="go to current session">Session</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="/projects/{$project?.id}/settings"
|
||||
class="text-zinc-400 hover:text-zinc-300"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<slot />
|
||||
|
||||
<div class="absolute bottom-0 left-0 w-full">
|
||||
<div
|
||||
class="flex items-center flex-shrink-0 h-6 border-t select-none border-zinc-700 bg-zinc-900 "
|
||||
>
|
||||
<div
|
||||
class="flex flex-row mx-4 items-center space-x-2 justify-between w-full"
|
||||
>
|
||||
{#if $project?.api?.sync}
|
||||
<a
|
||||
href="/projects/{$project?.id}/settings"
|
||||
class="text-zinc-400 hover:text-zinc-300"
|
||||
>
|
||||
<div class="flex flex-row items-center space-x-2 ">
|
||||
<div class="w-2 h-2 bg-green-700 rounded-full" />
|
||||
<div class="text-zinc-200">Syncing</div>
|
||||
</div>
|
||||
</a>
|
||||
<a target="_blank" rel="noreferrer" href={projectUrl($project)}
|
||||
>Open in GitButler Cloud</a
|
||||
>
|
||||
{:else}
|
||||
<a
|
||||
href="/projects/{$project?.id}/settings"
|
||||
class="text-zinc-400 hover:text-zinc-300"
|
||||
>
|
||||
<div class="flex flex-row items-center space-x-2 ">
|
||||
<div class="w-2 h-2 bg-red-700 rounded-full" />
|
||||
<div class="text-zinc-200">Offline</div>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center flex-shrink-0 h-6 border-t select-none border-zinc-700 bg-zinc-900 "
|
||||
>
|
||||
<div class="flex flex-row mx-4 items-center space-x-2 justify-between w-full">
|
||||
{#if $project?.api?.sync}
|
||||
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
|
||||
<div class="flex flex-row items-center space-x-2 ">
|
||||
<div class="w-2 h-2 bg-green-700 rounded-full" />
|
||||
<div class="text-zinc-200">Syncing</div>
|
||||
</div>
|
||||
</a>
|
||||
<a target="_blank" rel="noreferrer" href={projectUrl($project)}>Open in GitButler Cloud</a>
|
||||
{:else}
|
||||
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
|
||||
<div class="flex flex-row items-center space-x-2 ">
|
||||
<div class="w-2 h-2 bg-red-700 rounded-full" />
|
||||
<div class="text-zinc-200">Offline</div>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,24 +1,20 @@
|
||||
import { readable, derived } from "svelte/store";
|
||||
import type { LayoutLoad } from "./$types";
|
||||
import { building } from "$app/environment";
|
||||
import type { Session } from "$lib/sessions";
|
||||
import { readable, derived } from 'svelte/store';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { building } from '$app/environment';
|
||||
import type { Session } from '$lib/sessions';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const load: LayoutLoad = async ({ parent, params }) => {
|
||||
const { projects } = await parent();
|
||||
const sessions = building
|
||||
? readable<Session[]>([])
|
||||
: await (
|
||||
await import("$lib/sessions")
|
||||
).default({ projectId: params.projectId });
|
||||
const orderedSessions = derived(sessions, (sessions) => {
|
||||
return sessions
|
||||
.slice()
|
||||
.sort((a, b) => a.meta.startTimestampMs - b.meta.startTimestampMs);
|
||||
});
|
||||
return {
|
||||
project: projects.get(params.projectId),
|
||||
sessions: orderedSessions,
|
||||
};
|
||||
const { projects } = await parent();
|
||||
const sessions = building
|
||||
? readable<Session[]>([])
|
||||
: await (await import('$lib/sessions')).default({ projectId: params.projectId });
|
||||
const orderedSessions = derived(sessions, (sessions) => {
|
||||
return sessions.slice().sort((a, b) => a.meta.startTimestampMs - b.meta.startTimestampMs);
|
||||
});
|
||||
return {
|
||||
project: projects.get(params.projectId),
|
||||
sessions: orderedSessions
|
||||
};
|
||||
};
|
||||
|
@ -1,20 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from "./$types";
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
const { project } = data;
|
||||
export let data: PageData;
|
||||
const { project } = data;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col mt-12">
|
||||
<h1 class="text-zinc-200 text-2xl flex justify-center">
|
||||
Overview of {$project?.title}
|
||||
</h1>
|
||||
<div class="flex justify-center space-x-2">
|
||||
<a class="hover:text-zinc-200" href="/projects/{$project?.id}/week"
|
||||
>Week</a
|
||||
>
|
||||
<a href="/projects/{$project?.id}/day" class="hover:text-zinc-200"
|
||||
>Day</a
|
||||
>
|
||||
</div>
|
||||
<h1 class="text-zinc-200 text-2xl flex justify-center">
|
||||
Overview of {$project?.title}
|
||||
</h1>
|
||||
<div class="flex justify-center space-x-2">
|
||||
<a class="hover:text-zinc-200" href="/projects/{$project?.id}/week">Week</a>
|
||||
<a href="/projects/{$project?.id}/day" class="hover:text-zinc-200">Day</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,96 +1,85 @@
|
||||
<script lang="ts">
|
||||
import MdKeyboardArrowLeft from "svelte-icons/md/MdKeyboardArrowLeft.svelte";
|
||||
import MdKeyboardArrowRight from "svelte-icons/md/MdKeyboardArrowRight.svelte";
|
||||
import { TimelineDaySession } from "$lib/components/timeline";
|
||||
import type { PageData } from "./$types";
|
||||
import type { Session } from "$lib/sessions";
|
||||
import { derived } from "svelte/store";
|
||||
export let data: PageData;
|
||||
const { project, sessions } = data;
|
||||
import MdKeyboardArrowLeft from 'svelte-icons/md/MdKeyboardArrowLeft.svelte';
|
||||
import MdKeyboardArrowRight from 'svelte-icons/md/MdKeyboardArrowRight.svelte';
|
||||
import { TimelineDaySession } from '$lib/components/timeline';
|
||||
import type { PageData } from './$types';
|
||||
import type { Session } from '$lib/sessions';
|
||||
import { derived } from 'svelte/store';
|
||||
export let data: PageData;
|
||||
const { project, sessions } = data;
|
||||
|
||||
let date = new Date();
|
||||
$: canNavigateForwad =
|
||||
new Date(date.getTime() + 24 * 60 * 60 * 1000) < new Date();
|
||||
let date = new Date();
|
||||
$: canNavigateForwad = new Date(date.getTime() + 24 * 60 * 60 * 1000) < new Date();
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat("default", {
|
||||
weekday: "short",
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
}).format(date);
|
||||
};
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('default', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short'
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const sessionDisplayWidth = (session: Session) => {
|
||||
let sessionDurationMinutes =
|
||||
(session.meta.lastTimestampMs - session.meta.startTimestampMs) / 60;
|
||||
if (sessionDurationMinutes <= 10) {
|
||||
return "w-40 min-w-40";
|
||||
} else {
|
||||
return "w-60 min-w-60";
|
||||
}
|
||||
};
|
||||
const sessionDisplayWidth = (session: Session) => {
|
||||
let sessionDurationMinutes =
|
||||
(session.meta.lastTimestampMs - session.meta.startTimestampMs) / 60;
|
||||
if (sessionDurationMinutes <= 10) {
|
||||
return 'w-40 min-w-40';
|
||||
} else {
|
||||
return 'w-60 min-w-60';
|
||||
}
|
||||
};
|
||||
|
||||
$: sessionsInDay = derived([sessions], ([sessions]) => {
|
||||
const start = new Date(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate()
|
||||
);
|
||||
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
|
||||
return sessions.filter((session) => {
|
||||
return (
|
||||
start <= new Date(session.meta.startTimestampMs) &&
|
||||
new Date(session.meta.startTimestampMs) <= end
|
||||
);
|
||||
});
|
||||
});
|
||||
$: sessionsInDay = derived([sessions], ([sessions]) => {
|
||||
const start = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
|
||||
return sessions.filter((session) => {
|
||||
return (
|
||||
start <= new Date(session.meta.startTimestampMs) &&
|
||||
new Date(session.meta.startTimestampMs) <= end
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full select-none text-zinc-400">
|
||||
<header
|
||||
class="flex items-center justify-between flex-none px-8 py-1.5 border-b border-zinc-700"
|
||||
>
|
||||
<div class="flex items-center justify-start w-64">
|
||||
<button
|
||||
class="-ml-2 w-8 h-8 hover:text-zinc-100"
|
||||
on:click={() =>
|
||||
(date = new Date(date.getTime() - 24 * 60 * 60 * 1000))}
|
||||
>
|
||||
<MdKeyboardArrowLeft />
|
||||
</button>
|
||||
<div class="flex-grow text-center">
|
||||
{formatDate(date)}
|
||||
</div>
|
||||
<button
|
||||
class="-mr-2 w-8 h-8 hover:text-zinc-100 disabled:text-zinc-700"
|
||||
disabled={!canNavigateForwad}
|
||||
on:click={() => {
|
||||
if (canNavigateForwad) {
|
||||
date = new Date(date.getTime() + 24 * 60 * 60 * 1000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MdKeyboardArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<header class="flex items-center justify-between flex-none px-8 py-1.5 border-b border-zinc-700">
|
||||
<div class="flex items-center justify-start w-64">
|
||||
<button
|
||||
class="-ml-2 w-8 h-8 hover:text-zinc-100"
|
||||
on:click={() => (date = new Date(date.getTime() - 24 * 60 * 60 * 1000))}
|
||||
>
|
||||
<MdKeyboardArrowLeft />
|
||||
</button>
|
||||
<div class="flex-grow text-center">
|
||||
{formatDate(date)}
|
||||
</div>
|
||||
<button
|
||||
class="-mr-2 w-8 h-8 hover:text-zinc-100 disabled:text-zinc-700"
|
||||
disabled={!canNavigateForwad}
|
||||
on:click={() => {
|
||||
if (canNavigateForwad) {
|
||||
date = new Date(date.getTime() + 24 * 60 * 60 * 1000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MdKeyboardArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="w-full h-full overflow-scroll mx-2 flex">
|
||||
{#if $project}
|
||||
<div class="flex-grow items-center justify-center mt-4">
|
||||
<div class="justify-center flex flex-row space-x-2 pt-2">
|
||||
{#each $sessionsInDay as session}
|
||||
<div class={sessionDisplayWidth(session)}>
|
||||
<TimelineDaySession
|
||||
projectId={$project.id}
|
||||
{session}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p>Project not found</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-full h-full overflow-scroll mx-2 flex">
|
||||
{#if $project}
|
||||
<div class="flex-grow items-center justify-center mt-4">
|
||||
<div class="justify-center flex flex-row space-x-2 pt-2">
|
||||
{#each $sessionsInDay as session}
|
||||
<div class={sessionDisplayWidth(session)}>
|
||||
<TimelineDaySession projectId={$project.id} {session} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p>Project not found</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,297 +1,267 @@
|
||||
<script lang="ts">
|
||||
import MdKeyboardArrowLeft from "svelte-icons/md/MdKeyboardArrowLeft.svelte";
|
||||
import MdKeyboardArrowRight from "svelte-icons/md/MdKeyboardArrowRight.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
import { add, format, differenceInSeconds, addSeconds } from "date-fns";
|
||||
import { page } from "$app/stores";
|
||||
import { onMount } from "svelte";
|
||||
import { derived } from "svelte/store";
|
||||
import { Operation } from "$lib/deltas";
|
||||
import { Slider } from "fluent-svelte";
|
||||
import { CodeViewer } from "$lib/components";
|
||||
import "fluent-svelte/theme.css";
|
||||
import MdKeyboardArrowLeft from 'svelte-icons/md/MdKeyboardArrowLeft.svelte';
|
||||
import MdKeyboardArrowRight from 'svelte-icons/md/MdKeyboardArrowRight.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { add, format, differenceInSeconds, addSeconds } from 'date-fns';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { derived } from 'svelte/store';
|
||||
import { Operation } from '$lib/deltas';
|
||||
import { Slider } from 'fluent-svelte';
|
||||
import { CodeViewer } from '$lib/components';
|
||||
import 'fluent-svelte/theme.css';
|
||||
|
||||
export let data: PageData;
|
||||
$: session = data.session;
|
||||
$: previousSesssion = data.previousSesssion;
|
||||
$: nextSession = data.nextSession;
|
||||
$: deltas = data.deltas;
|
||||
export let data: PageData;
|
||||
$: session = data.session;
|
||||
$: previousSesssion = data.previousSesssion;
|
||||
$: nextSession = data.nextSession;
|
||||
$: deltas = data.deltas;
|
||||
|
||||
let time = new Date();
|
||||
$: start = new Date($session.meta.startTimestampMs);
|
||||
$: end = $session.hash
|
||||
? addSeconds(new Date($session.meta.lastTimestampMs), 10)
|
||||
: time; // For some reason, some deltas are stamped a few seconds after the session end.
|
||||
// Also, if the session is current, the end time moves.
|
||||
let time = new Date();
|
||||
$: start = new Date($session.meta.startTimestampMs);
|
||||
$: end = $session.hash ? addSeconds(new Date($session.meta.lastTimestampMs), 10) : time; // For some reason, some deltas are stamped a few seconds after the session end.
|
||||
// Also, if the session is current, the end time moves.
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
time = new Date();
|
||||
}, 10000);
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
time = new Date();
|
||||
}, 10000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
$: midpoint = add(start, {
|
||||
seconds: differenceInSeconds(end, start) * 0.5,
|
||||
});
|
||||
$: quarter = add(start, {
|
||||
seconds: differenceInSeconds(end, start) * 0.25,
|
||||
});
|
||||
$: threequarters = add(start, {
|
||||
seconds: differenceInSeconds(end, start) * 0.75,
|
||||
});
|
||||
const timeStampToCol = (deltaTimestamp: Date) => {
|
||||
if (deltaTimestamp < start || deltaTimestamp > end) {
|
||||
console.error(
|
||||
`Delta timestamp out of session range. Delta timestamp: ${deltaTimestamp}, Session start: ${start}, Session end: ${end}`
|
||||
);
|
||||
}
|
||||
// there are 88 columns
|
||||
// start is column 17
|
||||
const totalDiff = differenceInSeconds(end, start);
|
||||
const eventDiff = differenceInSeconds(deltaTimestamp, start);
|
||||
const rat = eventDiff / totalDiff;
|
||||
const col = Math.floor(rat * 63 + 17);
|
||||
return col;
|
||||
};
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
$: midpoint = add(start, {
|
||||
seconds: differenceInSeconds(end, start) * 0.5
|
||||
});
|
||||
$: quarter = add(start, {
|
||||
seconds: differenceInSeconds(end, start) * 0.25
|
||||
});
|
||||
$: threequarters = add(start, {
|
||||
seconds: differenceInSeconds(end, start) * 0.75
|
||||
});
|
||||
const timeStampToCol = (deltaTimestamp: Date) => {
|
||||
if (deltaTimestamp < start || deltaTimestamp > end) {
|
||||
console.error(
|
||||
`Delta timestamp out of session range. Delta timestamp: ${deltaTimestamp}, Session start: ${start}, Session end: ${end}`
|
||||
);
|
||||
}
|
||||
// there are 88 columns
|
||||
// start is column 17
|
||||
const totalDiff = differenceInSeconds(end, start);
|
||||
const eventDiff = differenceInSeconds(deltaTimestamp, start);
|
||||
const rat = eventDiff / totalDiff;
|
||||
const col = Math.floor(rat * 63 + 17);
|
||||
return col;
|
||||
};
|
||||
|
||||
const colToTimestamp = (col: number) => {
|
||||
const totalDiff = differenceInSeconds(end, start);
|
||||
const colDiff = col - 17;
|
||||
const rat = colDiff / 63;
|
||||
const eventDiff = totalDiff * rat;
|
||||
const timestamp = addSeconds(start, eventDiff);
|
||||
return timestamp;
|
||||
};
|
||||
const colToTimestamp = (col: number) => {
|
||||
const totalDiff = differenceInSeconds(end, start);
|
||||
const colDiff = col - 17;
|
||||
const rat = colDiff / 63;
|
||||
const eventDiff = totalDiff * rat;
|
||||
const timestamp = addSeconds(start, eventDiff);
|
||||
return timestamp;
|
||||
};
|
||||
|
||||
$: tickSizeMs = Math.floor((end.getTime() - start.getTime()) / 63); // how many ms each column represents
|
||||
let selectedFileIdx = 0;
|
||||
let value = 0;
|
||||
$: tickSizeMs = Math.floor((end.getTime() - start.getTime()) / 63); // how many ms each column represents
|
||||
let selectedFileIdx = 0;
|
||||
let value = 0;
|
||||
|
||||
$: doc = derived([data.deltas], ([allDeltas]) => {
|
||||
const filePath = Object.keys(allDeltas)[selectedFileIdx];
|
||||
const deltas = allDeltas[filePath];
|
||||
$: doc = derived([data.deltas], ([allDeltas]) => {
|
||||
const filePath = Object.keys(allDeltas)[selectedFileIdx];
|
||||
const deltas = allDeltas[filePath];
|
||||
|
||||
let text = data.files[filePath] || "";
|
||||
if (!deltas) return text;
|
||||
let text = data.files[filePath] || '';
|
||||
if (!deltas) return text;
|
||||
|
||||
const sliderValueTimestampMs =
|
||||
colToTimestamp(value).getTime() + tickSizeMs; // Include the tick size so that the slider value is always in the future
|
||||
// Filter operations based on the current slider value
|
||||
const operations = deltas
|
||||
.filter(
|
||||
(delta) =>
|
||||
delta.timestampMs >= start.getTime() &&
|
||||
delta.timestampMs <= sliderValueTimestampMs
|
||||
)
|
||||
.sort((a, b) => a.timestampMs - b.timestampMs)
|
||||
.flatMap((delta) => delta.operations);
|
||||
const sliderValueTimestampMs = colToTimestamp(value).getTime() + tickSizeMs; // Include the tick size so that the slider value is always in the future
|
||||
// Filter operations based on the current slider value
|
||||
const operations = deltas
|
||||
.filter(
|
||||
(delta) =>
|
||||
delta.timestampMs >= start.getTime() && delta.timestampMs <= sliderValueTimestampMs
|
||||
)
|
||||
.sort((a, b) => a.timestampMs - b.timestampMs)
|
||||
.flatMap((delta) => delta.operations);
|
||||
|
||||
operations.forEach((operation) => {
|
||||
if (Operation.isInsert(operation)) {
|
||||
text =
|
||||
text.slice(0, operation.insert[0]) +
|
||||
operation.insert[1] +
|
||||
text.slice(operation.insert[0]);
|
||||
} else if (Operation.isDelete(operation)) {
|
||||
text =
|
||||
text.slice(0, operation.delete[0]) +
|
||||
text.slice(operation.delete[0] + operation.delete[1]);
|
||||
}
|
||||
});
|
||||
operations.forEach((operation) => {
|
||||
if (Operation.isInsert(operation)) {
|
||||
text =
|
||||
text.slice(0, operation.insert[0]) +
|
||||
operation.insert[1] +
|
||||
text.slice(operation.insert[0]);
|
||||
} else if (Operation.isDelete(operation)) {
|
||||
text =
|
||||
text.slice(0, operation.delete[0]) +
|
||||
text.slice(operation.delete[0] + operation.delete[1]);
|
||||
}
|
||||
});
|
||||
|
||||
return text;
|
||||
});
|
||||
return text;
|
||||
});
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat("default", {
|
||||
weekday: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
}).format(date);
|
||||
};
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('default', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
}).format(date);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full text-zinc-400 overflow-hidden">
|
||||
<header
|
||||
class="flex items-center justify-between flex-none px-8 py-1.5 border-b border-zinc-700"
|
||||
>
|
||||
<div class="flex items-center justify-start w-64">
|
||||
<a
|
||||
href="/projects/{$page.params
|
||||
.projectId}/sessions/{$previousSesssion?.id}"
|
||||
class="-ml-2 w-8 h-8 hover:text-zinc-100 {$previousSesssion
|
||||
? ''
|
||||
: 'opacity-50 pointer-events-none cursor-not-allowed'}"
|
||||
>
|
||||
<MdKeyboardArrowLeft />
|
||||
</a>
|
||||
<div class="flex-grow text-center cursor-default grid grid-cols-7">
|
||||
<span class="col-span-3">{formatDate(start)}</span>
|
||||
<span>—</span>
|
||||
<span class="col-span-3">{formatDate(end)}</span>
|
||||
</div>
|
||||
<a
|
||||
href="/projects/{$page.params
|
||||
.projectId}/sessions/{$nextSession?.id}"
|
||||
class="-mr-2 w-8 h-8 hover:text-zinc-100 {$nextSession
|
||||
? ''
|
||||
: 'text-zinc-700 pointer-events-none cursor-not-allowed'}"
|
||||
>
|
||||
<MdKeyboardArrowRight />
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<header class="flex items-center justify-between flex-none px-8 py-1.5 border-b border-zinc-700">
|
||||
<div class="flex items-center justify-start w-64">
|
||||
<a
|
||||
href="/projects/{$page.params.projectId}/sessions/{$previousSesssion?.id}"
|
||||
class="-ml-2 w-8 h-8 hover:text-zinc-100 {$previousSesssion
|
||||
? ''
|
||||
: 'opacity-50 pointer-events-none cursor-not-allowed'}"
|
||||
>
|
||||
<MdKeyboardArrowLeft />
|
||||
</a>
|
||||
<div class="flex-grow text-center cursor-default grid grid-cols-7">
|
||||
<span class="col-span-3">{formatDate(start)}</span>
|
||||
<span>—</span>
|
||||
<span class="col-span-3">{formatDate(end)}</span>
|
||||
</div>
|
||||
<a
|
||||
href="/projects/{$page.params.projectId}/sessions/{$nextSession?.id}"
|
||||
class="-mr-2 w-8 h-8 hover:text-zinc-100 {$nextSession
|
||||
? ''
|
||||
: 'text-zinc-700 pointer-events-none cursor-not-allowed'}"
|
||||
>
|
||||
<MdKeyboardArrowRight />
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- main part -->
|
||||
<div
|
||||
class="flex flex-col flex-none max-w-full select-none border-b border-zinc-700 h-full overflow-auto"
|
||||
>
|
||||
<div class="flex flex-col flex-none max-w-full mb-40">
|
||||
<!-- sticky header -->
|
||||
<div
|
||||
class="overflow-hidden sticky top-0 z-30 bg-zinc-800 flex-none shadow shadow-zinc-700 ring-1 ring-zinc-700 ring-opacity-5 mb-1"
|
||||
>
|
||||
<div class="grid-cols-11 -mr-px border-zinc-700 grid">
|
||||
<div />
|
||||
<div
|
||||
class="col-span-2 flex items-center justify-center py-2"
|
||||
>
|
||||
<span>{format(start, "hh:mm")}</span>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-2 flex items-center justify-center py-2"
|
||||
>
|
||||
<span>{format(quarter, "hh:mm")}</span>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-2 flex items-center justify-center py-2"
|
||||
>
|
||||
<span>{format(midpoint, "hh:mm")}</span>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-2 flex items-center justify-center py-2"
|
||||
>
|
||||
<span>{format(threequarters, "hh:mm")}</span>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-2 flex items-center justify-center py-2"
|
||||
>
|
||||
<span>{format(end, "hh:mm")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- needle -->
|
||||
<div class="grid grid-cols-11">
|
||||
<div class="col-span-2 flex items-center justify-center" />
|
||||
<div
|
||||
class="-mx-1 col-span-8 flex items-center justify-center"
|
||||
>
|
||||
<Slider min={17} max={80} step={1} bind:value>
|
||||
<svelte:fragment slot="tooltip" let:value>
|
||||
{format(colToTimestamp(value), "hh:mm")}
|
||||
</svelte:fragment>
|
||||
</Slider>
|
||||
</div>
|
||||
<div class="col-span-1 flex items-center justify-center" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-auto mb-1">
|
||||
<div class="grid flex-auto grid-cols-1 grid-rows-1">
|
||||
<!-- file names list -->
|
||||
<div
|
||||
class="bg-col-start-1 col-end-2 row-start-1 grid divide-y divide-zinc-700/20"
|
||||
style="grid-template-rows: repeat({Object.keys($deltas)
|
||||
.length}, minmax(2rem, 1fr));"
|
||||
>
|
||||
<!-- <div class="row-end-1 h-7" /> -->
|
||||
<!-- main part -->
|
||||
<div
|
||||
class="flex flex-col flex-none max-w-full select-none border-b border-zinc-700 h-full overflow-auto"
|
||||
>
|
||||
<div class="flex flex-col flex-none max-w-full mb-40">
|
||||
<!-- sticky header -->
|
||||
<div
|
||||
class="overflow-hidden sticky top-0 z-30 bg-zinc-800 flex-none shadow shadow-zinc-700 ring-1 ring-zinc-700 ring-opacity-5 mb-1"
|
||||
>
|
||||
<div class="grid-cols-11 -mr-px border-zinc-700 grid">
|
||||
<div />
|
||||
<div class="col-span-2 flex items-center justify-center py-2">
|
||||
<span>{format(start, 'hh:mm')}</span>
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center justify-center py-2">
|
||||
<span>{format(quarter, 'hh:mm')}</span>
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center justify-center py-2">
|
||||
<span>{format(midpoint, 'hh:mm')}</span>
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center justify-center py-2">
|
||||
<span>{format(threequarters, 'hh:mm')}</span>
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center justify-center py-2">
|
||||
<span>{format(end, 'hh:mm')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- needle -->
|
||||
<div class="grid grid-cols-11">
|
||||
<div class="col-span-2 flex items-center justify-center" />
|
||||
<div class="-mx-1 col-span-8 flex items-center justify-center">
|
||||
<Slider min={17} max={80} step={1} bind:value>
|
||||
<svelte:fragment slot="tooltip" let:value>
|
||||
{format(colToTimestamp(value), 'hh:mm')}
|
||||
</svelte:fragment>
|
||||
</Slider>
|
||||
</div>
|
||||
<div class="col-span-1 flex items-center justify-center" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-auto mb-1">
|
||||
<div class="grid flex-auto grid-cols-1 grid-rows-1">
|
||||
<!-- file names list -->
|
||||
<div
|
||||
class="bg-col-start-1 col-end-2 row-start-1 grid divide-y divide-zinc-700/20"
|
||||
style="grid-template-rows: repeat({Object.keys($deltas).length}, minmax(2rem, 1fr));"
|
||||
>
|
||||
<!-- <div class="row-end-1 h-7" /> -->
|
||||
|
||||
{#each Object.keys($deltas) as filePath, i}
|
||||
<div
|
||||
class="flex {i == selectedFileIdx
|
||||
? 'bg-zinc-500/70'
|
||||
: ''}"
|
||||
>
|
||||
<button
|
||||
class="z-20 flex justify-end items-center overflow-hidden sticky left-0 w-1/6 leading-5
|
||||
{#each Object.keys($deltas) as filePath, i}
|
||||
<div class="flex {i == selectedFileIdx ? 'bg-zinc-500/70' : ''}">
|
||||
<button
|
||||
class="z-20 flex justify-end items-center overflow-hidden sticky left-0 w-1/6 leading-5
|
||||
{selectedFileIdx == i
|
||||
? 'text-zinc-200 cursor-default'
|
||||
: 'text-zinc-400 hover:text-zinc-200 cursor-pointer'}"
|
||||
on:click={() => (selectedFileIdx = i)}
|
||||
title={filePath}
|
||||
>
|
||||
{filePath.split("/").pop()}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
? 'text-zinc-200 cursor-default'
|
||||
: 'text-zinc-400 hover:text-zinc-200 cursor-pointer'}"
|
||||
on:click={() => (selectedFileIdx = i)}
|
||||
title={filePath}
|
||||
>
|
||||
{filePath.split('/').pop()}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- col selection -->
|
||||
<div
|
||||
class="col-start-1 col-end-2 row-start-1 grid"
|
||||
style="grid-template-columns: repeat(88, minmax(0, 1fr));"
|
||||
>
|
||||
<div
|
||||
class="bg-sky-400/60 "
|
||||
style=" grid-column: {value};"
|
||||
/>
|
||||
</div>
|
||||
<!-- time vertical lines -->
|
||||
<div
|
||||
class="col-start-1 col-end-2 row-start-1 grid-rows-1 divide-x divide-zinc-700/50 grid grid-cols-11"
|
||||
>
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
</div>
|
||||
<!-- col selection -->
|
||||
<div
|
||||
class="col-start-1 col-end-2 row-start-1 grid"
|
||||
style="grid-template-columns: repeat(88, minmax(0, 1fr));"
|
||||
>
|
||||
<div class="bg-sky-400/60 " style=" grid-column: {value};" />
|
||||
</div>
|
||||
<!-- time vertical lines -->
|
||||
<div
|
||||
class="col-start-1 col-end-2 row-start-1 grid-rows-1 divide-x divide-zinc-700/50 grid grid-cols-11"
|
||||
>
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
<div class="col-span-2 row-span-full" />
|
||||
</div>
|
||||
|
||||
<!-- actual entries -->
|
||||
<ol
|
||||
class="col-start-1 col-end-2 row-start-1 grid"
|
||||
style="
|
||||
<!-- actual entries -->
|
||||
<ol
|
||||
class="col-start-1 col-end-2 row-start-1 grid"
|
||||
style="
|
||||
grid-template-columns: repeat(88, minmax(0, 1fr));
|
||||
grid-template-rows: repeat({Object.keys($deltas)
|
||||
.length}, minmax(0px, 1fr)) auto;"
|
||||
>
|
||||
{#each Object.entries($deltas) as [filePath, fileDeltas], idx}
|
||||
{#each fileDeltas as delta}
|
||||
<li
|
||||
class="relative flex items-center bg-zinc-300 hover:bg-zinc-100 rounded m-0.5 cursor-pointer"
|
||||
style="
|
||||
.length}, minmax(0px, 1fr)) auto;"
|
||||
>
|
||||
{#each Object.entries($deltas) as [filePath, fileDeltas], idx}
|
||||
{#each fileDeltas as delta}
|
||||
<li
|
||||
class="relative flex items-center bg-zinc-300 hover:bg-zinc-100 rounded m-0.5 cursor-pointer"
|
||||
style="
|
||||
grid-row: {idx + 1} / span 1;
|
||||
grid-column: {timeStampToCol(
|
||||
new Date(delta.timestampMs)
|
||||
)} / span 1;"
|
||||
>
|
||||
<button
|
||||
class="z-20 h-full flex flex-col w-full items-center justify-center"
|
||||
on:click={() => {
|
||||
value = timeStampToCol(
|
||||
new Date(delta.timestampMs)
|
||||
);
|
||||
selectedFileIdx = idx;
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-11 mt-6">
|
||||
<div class="col-span-2" />
|
||||
<div class="col-span-8 p-1 bg-zinc-500/70 rounded select-text">
|
||||
{#if $doc}
|
||||
<CodeViewer value={$doc} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
new Date(delta.timestampMs)
|
||||
)} / span 1;"
|
||||
>
|
||||
<button
|
||||
class="z-20 h-full flex flex-col w-full items-center justify-center"
|
||||
on:click={() => {
|
||||
value = timeStampToCol(new Date(delta.timestampMs));
|
||||
selectedFileIdx = idx;
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-11 mt-6">
|
||||
<div class="col-span-2" />
|
||||
<div class="col-span-8 p-1 bg-zinc-500/70 rounded select-text">
|
||||
{#if $doc}
|
||||
<CodeViewer value={$doc} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,51 +1,45 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import { derived, readable } from "svelte/store";
|
||||
import { building } from "$app/environment";
|
||||
import type { Delta } from "$lib/deltas";
|
||||
import type { PageLoad } from './$types';
|
||||
import { derived, readable } from 'svelte/store';
|
||||
import { building } from '$app/environment';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
|
||||
export const prerender = false;
|
||||
export const load: PageLoad = async ({ parent, params }) => {
|
||||
const { sessions } = await parent();
|
||||
const deltas = building
|
||||
? readable({} as Record<string, Delta[]>)
|
||||
: (await import("$lib/deltas")).default({
|
||||
projectId: params.projectId,
|
||||
sessionId: params.sessionId,
|
||||
});
|
||||
const files = building
|
||||
? ({} as Record<string, string>)
|
||||
: (await import("$lib/sessions")).listFiles({
|
||||
projectId: params.projectId,
|
||||
sessionId: params.sessionId,
|
||||
});
|
||||
return {
|
||||
session: derived(sessions, (sessions) => {
|
||||
const result = sessions.find(
|
||||
(session) => session.id === params.sessionId
|
||||
);
|
||||
return result ? result : sessions[0];
|
||||
}),
|
||||
previousSesssion: derived(sessions, (sessions) => {
|
||||
const currentSessionIndex = sessions.findIndex(
|
||||
(session) => session.id === params.sessionId
|
||||
);
|
||||
if (currentSessionIndex - 1 < sessions.length) {
|
||||
return sessions[currentSessionIndex - 1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
nextSession: derived(sessions, (sessions) => {
|
||||
const currentSessionIndex = sessions.findIndex(
|
||||
(session) => session.id === params.sessionId
|
||||
);
|
||||
if (currentSessionIndex + 1 < sessions.length) {
|
||||
return sessions[currentSessionIndex + 1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
files: files,
|
||||
deltas: deltas,
|
||||
}
|
||||
}
|
||||
const { sessions } = await parent();
|
||||
const deltas = building
|
||||
? readable({} as Record<string, Delta[]>)
|
||||
: (await import('$lib/deltas')).default({
|
||||
projectId: params.projectId,
|
||||
sessionId: params.sessionId
|
||||
});
|
||||
const files = building
|
||||
? ({} as Record<string, string>)
|
||||
: (await import('$lib/sessions')).listFiles({
|
||||
projectId: params.projectId,
|
||||
sessionId: params.sessionId
|
||||
});
|
||||
return {
|
||||
session: derived(sessions, (sessions) => {
|
||||
const result = sessions.find((session) => session.id === params.sessionId);
|
||||
return result ? result : sessions[0];
|
||||
}),
|
||||
previousSesssion: derived(sessions, (sessions) => {
|
||||
const currentSessionIndex = sessions.findIndex((session) => session.id === params.sessionId);
|
||||
if (currentSessionIndex - 1 < sessions.length) {
|
||||
return sessions[currentSessionIndex - 1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
nextSession: derived(sessions, (sessions) => {
|
||||
const currentSessionIndex = sessions.findIndex((session) => session.id === params.sessionId);
|
||||
if (currentSessionIndex + 1 < sessions.length) {
|
||||
return sessions[currentSessionIndex + 1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}),
|
||||
files: files,
|
||||
deltas: deltas
|
||||
};
|
||||
};
|
||||
|
@ -1,148 +1,138 @@
|
||||
<script lang="ts">
|
||||
import { derived } from "svelte/store";
|
||||
import { Login } from "$lib/components";
|
||||
import type { PageData } from "./$types";
|
||||
import { derived } from 'svelte/store';
|
||||
import { Login } from '$lib/components';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
const { project, user, api } = data;
|
||||
export let data: PageData;
|
||||
const { project, user, api } = data;
|
||||
|
||||
function repo_id(url: string) {
|
||||
const hurl = new URL(url);
|
||||
const path = hurl.pathname.split("/");
|
||||
return path[path.length - 1];
|
||||
}
|
||||
function repo_id(url: string) {
|
||||
const hurl = new URL(url);
|
||||
const path = hurl.pathname.split('/');
|
||||
return path[path.length - 1];
|
||||
}
|
||||
|
||||
function hostname(url: string) {
|
||||
const hurl = new URL(url);
|
||||
return hurl.hostname;
|
||||
}
|
||||
function hostname(url: string) {
|
||||
const hurl = new URL(url);
|
||||
return hurl.hostname;
|
||||
}
|
||||
|
||||
const isSyncing = derived(project, (project) => project?.api?.sync);
|
||||
const isSyncing = derived(project, (project) => project?.api?.sync);
|
||||
|
||||
const onSyncChange = async (event: Event) => {
|
||||
if ($project === undefined) return;
|
||||
if ($user === undefined) return;
|
||||
const onSyncChange = async (event: Event) => {
|
||||
if ($project === undefined) return;
|
||||
if ($user === undefined) return;
|
||||
|
||||
const target = event.target as HTMLInputElement;
|
||||
const sync = target.checked;
|
||||
const target = event.target as HTMLInputElement;
|
||||
const sync = target.checked;
|
||||
|
||||
if (!$project.api) {
|
||||
const apiProject = await api.projects.create($user.access_token, {
|
||||
name: $project.title,
|
||||
uid: $project.id,
|
||||
});
|
||||
await project.update({ api: { ...apiProject, sync } });
|
||||
} else {
|
||||
await project.update({ api: { ...$project.api, sync } });
|
||||
}
|
||||
};
|
||||
if (!$project.api) {
|
||||
const apiProject = await api.projects.create($user.access_token, {
|
||||
name: $project.title,
|
||||
uid: $project.id
|
||||
});
|
||||
await project.update({ api: { ...apiProject, sync } });
|
||||
} else {
|
||||
await project.update({ api: { ...$project.api, sync } });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="p-4 mx-auto h-full overflow-auto">
|
||||
<div class="max-w-2xl mx-auto p-4">
|
||||
<div class="flex flex-col text-zinc-100 space-y-6">
|
||||
<div class="space-y-0">
|
||||
<div class="text-lg font-medium">Project Settings</div>
|
||||
<div class="text-zinc-400">
|
||||
Manage your project settings for <strong
|
||||
>{$project?.title}</strong
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-zinc-600" />
|
||||
{#if $user}
|
||||
<div class="space-y-2">
|
||||
<div class="ml-1">GitButler Cloud</div>
|
||||
<div
|
||||
class="flex flex-row justify-between border border-zinc-600 rounded-lg p-2 items-center"
|
||||
>
|
||||
<div class="flex flex-row space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="flex flex-row">
|
||||
{#if $project?.api?.git_url}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-zinc-300">
|
||||
Git Host
|
||||
</div>
|
||||
<div class="text-zinc-400 font-mono">
|
||||
{hostname($project?.api?.git_url)}
|
||||
</div>
|
||||
<div class="text-zinc-300 mt-3">
|
||||
Repository ID
|
||||
</div>
|
||||
<div class="text-zinc-400 font-mono">
|
||||
{repo_id($project?.api?.git_url)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<form disabled={$user === undefined}>
|
||||
<input
|
||||
class="mr-1"
|
||||
disabled={$user === undefined}
|
||||
type="checkbox"
|
||||
checked={$isSyncing}
|
||||
on:change={onSyncChange}
|
||||
/>
|
||||
<label for="sync"
|
||||
>Send Data to Server</label
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-row space-x-2 items-end">
|
||||
<div class="">GitButler Cloud</div>
|
||||
<div class="text-zinc-400">
|
||||
backup your work and access advanced features
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-center space-x-2">
|
||||
<Login {user} {api} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="space-y-2">
|
||||
<div class="ml-1">Path</div>
|
||||
<div class="text-zinc-400 font-mono">
|
||||
{$project?.path}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="ml-1">Project Name</div>
|
||||
<!-- text box -->
|
||||
<input
|
||||
type="text"
|
||||
class="p-2 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
|
||||
value={$project?.title}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="ml-1">Project Description</div>
|
||||
<!-- text box -->
|
||||
<textarea
|
||||
rows="3"
|
||||
class="p-2 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
|
||||
value={$project?.api?.description}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-2xl mx-auto p-4">
|
||||
<div class="flex flex-col text-zinc-100 space-y-6">
|
||||
<div class="space-y-0">
|
||||
<div class="text-lg font-medium">Project Settings</div>
|
||||
<div class="text-zinc-400">
|
||||
Manage your project settings for <strong>{$project?.title}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-zinc-600" />
|
||||
{#if $user}
|
||||
<div class="space-y-2">
|
||||
<div class="ml-1">GitButler Cloud</div>
|
||||
<div
|
||||
class="flex flex-row justify-between border border-zinc-600 rounded-lg p-2 items-center"
|
||||
>
|
||||
<div class="flex flex-row space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="flex flex-row">
|
||||
{#if $project?.api?.git_url}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-zinc-300">Git Host</div>
|
||||
<div class="text-zinc-400 font-mono">
|
||||
{hostname($project?.api?.git_url)}
|
||||
</div>
|
||||
<div class="text-zinc-300 mt-3">Repository ID</div>
|
||||
<div class="text-zinc-400 font-mono">
|
||||
{repo_id($project?.api?.git_url)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<form disabled={$user === undefined}>
|
||||
<input
|
||||
class="mr-1"
|
||||
disabled={$user === undefined}
|
||||
type="checkbox"
|
||||
checked={$isSyncing}
|
||||
on:change={onSyncChange}
|
||||
/>
|
||||
<label for="sync">Send Data to Server</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-row space-x-2 items-end">
|
||||
<div class="">GitButler Cloud</div>
|
||||
<div class="text-zinc-400">backup your work and access advanced features</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-center space-x-2">
|
||||
<Login {user} {api} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="space-y-2">
|
||||
<div class="ml-1">Path</div>
|
||||
<div class="text-zinc-400 font-mono">
|
||||
{$project?.path}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="ml-1">Project Name</div>
|
||||
<!-- text box -->
|
||||
<input
|
||||
type="text"
|
||||
class="p-2 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
|
||||
value={$project?.title}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="ml-1">Project Description</div>
|
||||
<!-- text box -->
|
||||
<textarea
|
||||
rows="3"
|
||||
class="p-2 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
|
||||
value={$project?.api?.description}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,362 +1,321 @@
|
||||
<script lang="ts">
|
||||
import { Week } from "$lib/week";
|
||||
import type { PageData } from "./$types";
|
||||
import { WeekBlockEntry } from "$lib/components/week";
|
||||
import MdKeyboardArrowLeft from "svelte-icons/md/MdKeyboardArrowLeft.svelte";
|
||||
import MdKeyboardArrowRight from "svelte-icons/md/MdKeyboardArrowRight.svelte";
|
||||
import { derived } from "svelte/store";
|
||||
import { Week } from '$lib/week';
|
||||
import type { PageData } from './$types';
|
||||
import { WeekBlockEntry } from '$lib/components/week';
|
||||
import MdKeyboardArrowLeft from 'svelte-icons/md/MdKeyboardArrowLeft.svelte';
|
||||
import MdKeyboardArrowRight from 'svelte-icons/md/MdKeyboardArrowRight.svelte';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
export let data: PageData;
|
||||
const { project, sessions } = data;
|
||||
export let data: PageData;
|
||||
const { project, sessions } = data;
|
||||
|
||||
let week = Week.from(new Date());
|
||||
let week = Week.from(new Date());
|
||||
|
||||
$: canNavigateForwad = week.end.getTime() < new Date().getTime();
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat("default", {
|
||||
weekday: "short",
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
}).format(date);
|
||||
};
|
||||
$: canNavigateForwad = week.end.getTime() < new Date().getTime();
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('default', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short'
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
$: sessionsInWeek = derived([sessions], ([sessions]) => {
|
||||
return sessions.filter((session) => {
|
||||
return (
|
||||
week.start <= new Date(session.meta.startTimestampMs) &&
|
||||
new Date(session.meta.startTimestampMs) <= week.end
|
||||
);
|
||||
});
|
||||
});
|
||||
$: sessionsInWeek = derived([sessions], ([sessions]) => {
|
||||
return sessions.filter((session) => {
|
||||
return (
|
||||
week.start <= new Date(session.meta.startTimestampMs) &&
|
||||
new Date(session.meta.startTimestampMs) <= week.end
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full select-none text-zinc-400">
|
||||
<header
|
||||
class="flex items-center justify-between flex-none px-8 py-1.5 border-b border-zinc-700"
|
||||
>
|
||||
<div class="flex items-center justify-start w-64">
|
||||
<button
|
||||
class="-ml-2 w-8 h-8 hover:text-zinc-100"
|
||||
on:click={() => (week = Week.previous(week))}
|
||||
>
|
||||
<MdKeyboardArrowLeft />
|
||||
</button>
|
||||
<div class="flex-grow text-center cursor-default grid grid-cols-7">
|
||||
<span class="col-span-3"
|
||||
>{formatDate(Week.nThDay(week, 0))}</span
|
||||
>
|
||||
<span>—</span>
|
||||
<span class="col-span-3"
|
||||
>{formatDate(Week.nThDay(week, 6))}</span
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="-mr-2 w-8 h-8 hover:text-zinc-100 disabled:text-zinc-700"
|
||||
disabled={!canNavigateForwad}
|
||||
on:click={() => {
|
||||
if (canNavigateForwad) {
|
||||
week = Week.next(week);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MdKeyboardArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="isolate flex flex-col flex-auto overflow-auto">
|
||||
<div class="h-4/5 overflow-auto flex flex-col flex-none max-w-full border-b border-zinc-700">
|
||||
<!-- sticky top -->
|
||||
<div
|
||||
class="overflow-hidden sticky top-0 z-30 bg-zinc-800 border-b border-zinc-700 flex-none px-8"
|
||||
>
|
||||
<div class="grid-cols-8 divide-x divide-zinc-700 grid">
|
||||
<div class="py-4" />
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Mon <span
|
||||
class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 0).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Tue <span
|
||||
class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 1).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Wed <span
|
||||
class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 2).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Thu <span
|
||||
class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 3).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Fri <span
|
||||
class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 4).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Sat <span
|
||||
class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 5).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Sun <span
|
||||
class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 6).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-auto ">
|
||||
<div class="grid flex-auto grid-cols-1 grid-rows-1">
|
||||
<!-- hours y lines-->
|
||||
<header class="flex items-center justify-between flex-none px-8 py-1.5 border-b border-zinc-700">
|
||||
<div class="flex items-center justify-start w-64">
|
||||
<button
|
||||
class="-ml-2 w-8 h-8 hover:text-zinc-100"
|
||||
on:click={() => (week = Week.previous(week))}
|
||||
>
|
||||
<MdKeyboardArrowLeft />
|
||||
</button>
|
||||
<div class="flex-grow text-center cursor-default grid grid-cols-7">
|
||||
<span class="col-span-3">{formatDate(Week.nThDay(week, 0))}</span>
|
||||
<span>—</span>
|
||||
<span class="col-span-3">{formatDate(Week.nThDay(week, 6))}</span>
|
||||
</div>
|
||||
<button
|
||||
class="-mr-2 w-8 h-8 hover:text-zinc-100 disabled:text-zinc-700"
|
||||
disabled={!canNavigateForwad}
|
||||
on:click={() => {
|
||||
if (canNavigateForwad) {
|
||||
week = Week.next(week);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MdKeyboardArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="isolate flex flex-col flex-auto overflow-auto">
|
||||
<div class="h-4/5 overflow-auto flex flex-col flex-none max-w-full border-b border-zinc-700">
|
||||
<!-- sticky top -->
|
||||
<div
|
||||
class="overflow-hidden sticky top-0 z-30 bg-zinc-800 border-b border-zinc-700 flex-none px-8"
|
||||
>
|
||||
<div class="grid-cols-8 divide-x divide-zinc-700 grid">
|
||||
<div class="py-4" />
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Mon <span class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 0).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Tue <span class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 1).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Wed <span class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 2).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Thu <span class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 3).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Fri <span class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 4).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Sat <span class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 5).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
>Sun <span class="items-center justify-center font-semibold"
|
||||
>{Week.nThDay(week, 6).getDate()}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-auto ">
|
||||
<div class="grid flex-auto grid-cols-1 grid-rows-1">
|
||||
<!-- hours y lines-->
|
||||
|
||||
<div
|
||||
class="text-zinc-500 col-start-1 col-end-2 row-start-1 grid-rows-1 grid grid-cols-8 px-8"
|
||||
>
|
||||
<div
|
||||
class="col-start-1 col-end-2 row-start-1 grid justify-end"
|
||||
style="grid-template-rows: repeat(24, minmax(1.5rem, 1fr));"
|
||||
>
|
||||
<div class="row-end-1 h-7" />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
12 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
2 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
4 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
6 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
8 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
10 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
12 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
2 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
4 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
6 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
8 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500"
|
||||
>
|
||||
10 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-zinc-500 col-start-1 col-end-2 row-start-1 grid-rows-1 grid grid-cols-8 px-8"
|
||||
>
|
||||
<div
|
||||
class="col-start-1 col-end-2 row-start-1 grid justify-end"
|
||||
style="grid-template-rows: repeat(24, minmax(1.5rem, 1fr));"
|
||||
>
|
||||
<div class="row-end-1 h-7" />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
12 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
2 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
4 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
6 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
8 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
10 AM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
12 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
2 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
4 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
6 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
8 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
|
||||
10 PM
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-zinc-500 col-start-1 col-end-2 row-start-1 grid-rows-1 grid grid-cols-8 ml-8 "
|
||||
>
|
||||
<div
|
||||
class="col-start-2 col-end-9 row-start-1 grid divide-y divide-zinc-700/20"
|
||||
style="grid-template-rows: repeat(24, minmax(1.5rem, 1fr));"
|
||||
>
|
||||
<div class="row-end-1 h-7" />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="text-zinc-500 col-start-1 col-end-2 row-start-1 grid-rows-1 grid grid-cols-8 ml-8 "
|
||||
>
|
||||
<div
|
||||
class="col-start-2 col-end-9 row-start-1 grid divide-y divide-zinc-700/20"
|
||||
style="grid-template-rows: repeat(24, minmax(1.5rem, 1fr));"
|
||||
>
|
||||
<div class="row-end-1 h-7" />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<div>
|
||||
<div
|
||||
class="h-7 left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right leading-5 text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<!-- day x lines -->
|
||||
<div
|
||||
class="col-start-1 col-end-2 row-start-1 grid-rows-1 divide-x divide-zinc-700/50 grid grid-cols-8 px-8"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<!-- day x lines -->
|
||||
<div
|
||||
class="col-start-1 col-end-2 row-start-1 grid-rows-1 divide-x divide-zinc-700/50 grid grid-cols-8 px-8"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<!-- actual entries -->
|
||||
<ol
|
||||
class="col-start-1 col-end-2 row-start-1 grid grid-cols-8 px-8"
|
||||
style="grid-template-rows: 1.75rem repeat(96, minmax(0px, 1fr)) auto;"
|
||||
>
|
||||
{#each $sessionsInWeek as session}
|
||||
<WeekBlockEntry
|
||||
startTime={new Date(
|
||||
session.meta.startTimestampMs
|
||||
)}
|
||||
endTime={new Date(
|
||||
session.meta.startTimestampMs
|
||||
)}
|
||||
label={session.meta.branch}
|
||||
href="/projects/{$project?.id}/sessions/{session.id}/"
|
||||
/>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- actual entries -->
|
||||
<ol
|
||||
class="col-start-1 col-end-2 row-start-1 grid grid-cols-8 px-8"
|
||||
style="grid-template-rows: 1.75rem repeat(96, minmax(0px, 1fr)) auto;"
|
||||
>
|
||||
{#each $sessionsInWeek as session}
|
||||
<WeekBlockEntry
|
||||
startTime={new Date(session.meta.startTimestampMs)}
|
||||
endTime={new Date(session.meta.startTimestampMs)}
|
||||
label={session.meta.branch}
|
||||
href="/projects/{$project?.id}/sessions/{session.id}/"
|
||||
/>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,224 +1,208 @@
|
||||
<script lang="ts">
|
||||
import { Login } from "$lib/components";
|
||||
import type { PageData } from "./$types";
|
||||
import MdAutorenew from "svelte-icons/md/MdAutorenew.svelte";
|
||||
import { log, toasts } from "$lib";
|
||||
import { Login } from '$lib/components';
|
||||
import type { PageData } from './$types';
|
||||
import MdAutorenew from 'svelte-icons/md/MdAutorenew.svelte';
|
||||
import { log, toasts } from '$lib';
|
||||
|
||||
export let data: PageData;
|
||||
const { user, api } = data;
|
||||
export let data: PageData;
|
||||
const { user, api } = data;
|
||||
|
||||
$: saving = false;
|
||||
$: saving = false;
|
||||
|
||||
let userName = $user?.name;
|
||||
let userPicture = $user?.picture;
|
||||
let userName = $user?.name;
|
||||
let userPicture = $user?.picture;
|
||||
|
||||
const fileTypes = ["image/jpeg", "image/png"];
|
||||
const fileTypes = ['image/jpeg', 'image/png'];
|
||||
|
||||
const validFileType = (file: File) => {
|
||||
return fileTypes.includes(file.type);
|
||||
};
|
||||
const validFileType = (file: File) => {
|
||||
return fileTypes.includes(file.type);
|
||||
};
|
||||
|
||||
const onPictureChange = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
const onPictureChange = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
|
||||
if (file && validFileType(file)) {
|
||||
userPicture = URL.createObjectURL(file);
|
||||
} else {
|
||||
userPicture = $user?.picture;
|
||||
toasts.error("Please use a valid image file");
|
||||
}
|
||||
};
|
||||
if (file && validFileType(file)) {
|
||||
userPicture = URL.createObjectURL(file);
|
||||
} else {
|
||||
userPicture = $user?.picture;
|
||||
toasts.error('Please use a valid image file');
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (e: SubmitEvent) => {
|
||||
if (!$user) return;
|
||||
saving = true;
|
||||
const onSubmit = async (e: SubmitEvent) => {
|
||||
if (!$user) return;
|
||||
saving = true;
|
||||
|
||||
const target = e.target as HTMLFormElement;
|
||||
const formData = new FormData(target);
|
||||
const name = formData.get("name") as string | undefined;
|
||||
const picture = formData.get("picture") as File | undefined;
|
||||
const target = e.target as HTMLFormElement;
|
||||
const formData = new FormData(target);
|
||||
const name = formData.get('name') as string | undefined;
|
||||
const picture = formData.get('picture') as File | undefined;
|
||||
|
||||
try {
|
||||
$user = await api.user.update($user.access_token, {
|
||||
name,
|
||||
picture: picture,
|
||||
});
|
||||
toasts.success("Profile updated");
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
toasts.error("Failed to update user");
|
||||
}
|
||||
try {
|
||||
$user = await api.user.update($user.access_token, {
|
||||
name,
|
||||
picture: picture
|
||||
});
|
||||
toasts.success('Profile updated');
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
toasts.error('Failed to update user');
|
||||
}
|
||||
|
||||
saving = false;
|
||||
};
|
||||
saving = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="p-4 mx-auto">
|
||||
<div class="max-w-xl mx-auto p-4">
|
||||
{#if $user}
|
||||
<div class="flex flex-col gap-6 text-zinc-100">
|
||||
<header class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-medium">
|
||||
GitButler Cloud Account
|
||||
</h2>
|
||||
<Login {user} {api} />
|
||||
</header>
|
||||
<div class="max-w-xl mx-auto p-4">
|
||||
{#if $user}
|
||||
<div class="flex flex-col gap-6 text-zinc-100">
|
||||
<header class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-medium">GitButler Cloud Account</h2>
|
||||
<Login {user} {api} />
|
||||
</header>
|
||||
|
||||
<form
|
||||
on:submit={onSubmit}
|
||||
class="flex flex-row gap-12 justify-between rounded-lg p-2 items-start"
|
||||
>
|
||||
<fields id="left" class="flex flex-1 flex-col gap-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="name" class="text-zinc-400">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
bind:value={userName}
|
||||
type="text"
|
||||
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
on:submit={onSubmit}
|
||||
class="flex flex-row gap-12 justify-between rounded-lg p-2 items-start"
|
||||
>
|
||||
<fields id="left" class="flex flex-1 flex-col gap-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="name" class="text-zinc-400">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
bind:value={userName}
|
||||
type="text"
|
||||
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="email" class="text-zinc-400"
|
||||
>Email</label
|
||||
>
|
||||
<input
|
||||
disabled
|
||||
id="email"
|
||||
name="email"
|
||||
bind:value={$user.email}
|
||||
type="text"
|
||||
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="email" class="text-zinc-400">Email</label>
|
||||
<input
|
||||
disabled
|
||||
id="email"
|
||||
name="email"
|
||||
bind:value={$user.email}
|
||||
type="text"
|
||||
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<footer class="pt-4">
|
||||
{#if saving}
|
||||
<div
|
||||
class="flex w-32 flex-row w-content items-center gap-1 justify-center py-1 px-3 rounded text-white bg-blue-400"
|
||||
>
|
||||
<div class="animate-spin w-5 h-5">
|
||||
<MdAutorenew />
|
||||
</div>
|
||||
<span>Updating...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
type="submit"
|
||||
class="py-1 px-3 rounded text-white bg-blue-400"
|
||||
>Update profile</button
|
||||
>
|
||||
{/if}
|
||||
</footer>
|
||||
</fields>
|
||||
<footer class="pt-4">
|
||||
{#if saving}
|
||||
<div
|
||||
class="flex w-32 flex-row w-content items-center gap-1 justify-center py-1 px-3 rounded text-white bg-blue-400"
|
||||
>
|
||||
<div class="animate-spin w-5 h-5">
|
||||
<MdAutorenew />
|
||||
</div>
|
||||
<span>Updating...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="py-1 px-3 rounded text-white bg-blue-400"
|
||||
>Update profile</button
|
||||
>
|
||||
{/if}
|
||||
</footer>
|
||||
</fields>
|
||||
|
||||
<fields id="right" class="flex flex-col gap-2 items-center">
|
||||
{#if $user.picture}
|
||||
<img
|
||||
class="h-28 w-28 rounded-full border-zinc-300"
|
||||
src={userPicture}
|
||||
alt="Your avatar"
|
||||
required
|
||||
/>
|
||||
{/if}
|
||||
<fields id="right" class="flex flex-col gap-2 items-center">
|
||||
{#if $user.picture}
|
||||
<img
|
||||
class="h-28 w-28 rounded-full border-zinc-300"
|
||||
src={userPicture}
|
||||
alt="Your avatar"
|
||||
required
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<label
|
||||
for="picture"
|
||||
class="px-2 -mt-6 -ml-16 cursor-pointer text-center font-sm text-zinc-300 bg-zinc-800 border border-zinc-600 rounded-lg"
|
||||
>
|
||||
Edit
|
||||
<input
|
||||
on:change={onPictureChange}
|
||||
type="file"
|
||||
id="picture"
|
||||
name="picture"
|
||||
accept={fileTypes.join("")}
|
||||
class="hidden"
|
||||
/>
|
||||
</label>
|
||||
</fields>
|
||||
</form>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col text-white space-y-6 items-center justify-items-center"
|
||||
>
|
||||
<div class="text-3xl font-bold text-white">
|
||||
Connect to GitButler Cloud
|
||||
</div>
|
||||
<div>
|
||||
Sign up or log in to GitButler Cloud for more tools and
|
||||
features:
|
||||
</div>
|
||||
<ul class="text-zinc-400 pb-4 space-y-2">
|
||||
<li class="flex flex-row space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
>Backup everything you do in any of your projects</span
|
||||
>
|
||||
</li>
|
||||
<li class="flex flex-row space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
|
||||
/>
|
||||
</svg>
|
||||
<label
|
||||
for="picture"
|
||||
class="px-2 -mt-6 -ml-16 cursor-pointer text-center font-sm text-zinc-300 bg-zinc-800 border border-zinc-600 rounded-lg"
|
||||
>
|
||||
Edit
|
||||
<input
|
||||
on:change={onPictureChange}
|
||||
type="file"
|
||||
id="picture"
|
||||
name="picture"
|
||||
accept={fileTypes.join('')}
|
||||
class="hidden"
|
||||
/>
|
||||
</label>
|
||||
</fields>
|
||||
</form>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col text-white space-y-6 items-center justify-items-center">
|
||||
<div class="text-3xl font-bold text-white">Connect to GitButler Cloud</div>
|
||||
<div>Sign up or log in to GitButler Cloud for more tools and features:</div>
|
||||
<ul class="text-zinc-400 pb-4 space-y-2">
|
||||
<li class="flex flex-row space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Backup everything you do in any of your projects</span>
|
||||
</li>
|
||||
<li class="flex flex-row space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span>Sync your data across devices</span>
|
||||
</li>
|
||||
<li class="flex flex-row space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<span>AI commit message automated suggestions</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-8 text-center">
|
||||
<Login {user} {api} />
|
||||
</div>
|
||||
<div class="text-zinc-300 text-center">
|
||||
You will still need to give us permission for each project
|
||||
before we transfer any data to our servers. You can revoke
|
||||
this permission at any time.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span>Sync your data across devices</span>
|
||||
</li>
|
||||
<li class="flex flex-row space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="white"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<span>AI commit message automated suggestions</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-8 text-center">
|
||||
<Login {user} {api} />
|
||||
</div>
|
||||
<div class="text-zinc-300 text-center">
|
||||
You will still need to give us permission for each project before we transfer any data to
|
||||
our servers. You can revoke this permission at any time.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,22 +1,15 @@
|
||||
import preprocess from "svelte-preprocess";
|
||||
import staticAdapter from "@sveltejs/adapter-static";
|
||||
import { vitePreprocess } from "@sveltejs/kit/vite";
|
||||
import staticAdapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: [
|
||||
vitePreprocess(),
|
||||
preprocess({
|
||||
postcss: true,
|
||||
typescript: true,
|
||||
}),
|
||||
],
|
||||
kit: {
|
||||
adapter: staticAdapter({
|
||||
precompress: true,
|
||||
strict: false,
|
||||
}),
|
||||
},
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: staticAdapter({
|
||||
precompress: true,
|
||||
strict: false
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -1,22 +1,22 @@
|
||||
const config = {
|
||||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
darkMode: "class",
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
darkMode: 'class',
|
||||
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ["Inter", "SF Pro", "-apple-system", "system-ui"],
|
||||
},
|
||||
fontSize: {
|
||||
xs: "10px",
|
||||
sm: "12px",
|
||||
base: "13px",
|
||||
lg: "15px",
|
||||
xl: "22px",
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'SF Pro', '-apple-system', 'system-ui']
|
||||
},
|
||||
fontSize: {
|
||||
xs: '10px',
|
||||
sm: '12px',
|
||||
base: '13px',
|
||||
lg: '15px',
|
||||
xl: '22px'
|
||||
},
|
||||
extend: {}
|
||||
},
|
||||
|
||||
plugins: [],
|
||||
plugins: []
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
@ -1,17 +1,17 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from 'vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
plugins: [sveltekit()],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
// prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
},
|
||||
// to make use of `TAURI_DEBUG` and other env variables
|
||||
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
build: {
|
||||
// Tauri supports es2021
|
||||
target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13",
|
||||
// don't minify for debug builds
|
||||
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
|
||||
// produce sourcemaps for debug builds
|
||||
sourcemap: !!process.env.TAURI_DEBUG,
|
||||
},
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
// prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true
|
||||
},
|
||||
// to make use of `TAURI_DEBUG` and other env variables
|
||||
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||
envPrefix: ['VITE_', 'TAURI_'],
|
||||
build: {
|
||||
// Tauri supports es2021
|
||||
target: process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13',
|
||||
// don't minify for debug builds
|
||||
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
|
||||
// produce sourcemaps for debug builds
|
||||
sourcemap: !!process.env.TAURI_DEBUG
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user