sort out lint/format setup

This commit is contained in:
Nikita Galaiko 2023-02-24 10:46:41 +01:00
parent 894e31881d
commit d80c96ce80
No known key found for this signature in database
GPG Key ID: EBAB54E845BA519D
49 changed files with 2299 additions and 2747 deletions

18
.eslintignore Normal file
View 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
View 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
}
};

View File

@ -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
View 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
View File

@ -0,0 +1,9 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@ -1,8 +0,0 @@
{
"tabWidth": 4,
"plugins": [
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss"
],
"pluginSearchDirs": false
}

View File

@ -1,56 +1,55 @@
{ {
"name": "git-butler-tauri", "name": "git-butler-tauri",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", "lint": "prettier --plugin-search-dir . --check . && eslint .",
"tauri": "tauri" "format": "prettier --plugin-search-dir . --write .",
}, "tauri": "tauri"
"dependencies": { },
"@codemirror/commands": "^6.2.0", "dependencies": {
"@codemirror/merge": "^0.1.3", "@codemirror/commands": "^6.2.0",
"@codemirror/state": "^6.2.0", "@codemirror/merge": "^0.1.3",
"@codemirror/view": "^6.7.3", "@codemirror/state": "^6.2.0",
"@tauri-apps/api": "^1.2.0", "@codemirror/view": "^6.7.3",
"date-fns": "^2.29.3", "@tauri-apps/api": "^1.2.0",
"fluent-svelte": "^1.6.0", "date-fns": "^2.29.3",
"idb-keyval": "^6.2.0", "fluent-svelte": "^1.6.0",
"inter-ui": "^3.19.3", "idb-keyval": "^6.2.0",
"mm-jsr": "^3.0.2", "inter-ui": "^3.19.3",
"nanoid": "^4.0.1", "mm-jsr": "^3.0.2",
"posthog-js": "^1.46.1", "nanoid": "^4.0.1",
"seti-icons": "^0.0.4", "posthog-js": "^1.46.1",
"svelte-french-toast": "^1.0.3", "seti-icons": "^0.0.4",
"svelte-icons": "^2.1.0", "svelte-french-toast": "^1.0.3",
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log" "svelte-icons": "^2.1.0",
}, "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log"
"devDependencies": { },
"@sveltejs/adapter-static": "next", "devDependencies": {
"@sveltejs/kit": "next", "@sveltejs/adapter-static": "next",
"@tauri-apps/cli": "^1.2.2", "@sveltejs/kit": "next",
"@types/diff": "^5.0.2", "@tauri-apps/cli": "^1.2.2",
"autoprefixer": "^10.4.7", "@types/diff": "^5.0.2",
"eslint": "^8.34.0", "autoprefixer": "^10.4.7",
"eslint-config-prettier": "^8.6.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"eslint-plugin-prettier": "^4.2.1", "@typescript-eslint/parser": "^5.45.0",
"eslint-plugin-svelte": "^2.18.0", "eslint": "^8.28.0",
"eslint-plugin-unicorn": "^45.0.2", "eslint-config-prettier": "^8.5.0",
"postcss": "^8.4.14", "eslint-plugin-svelte3": "^4.0.0",
"postcss-load-config": "^4.0.1", "prettier": "^2.8.0",
"prettier": "^2.8.4", "prettier-plugin-svelte": "^2.8.1",
"prettier-plugin-svelte": "^2.9.0", "postcss": "^8.4.14",
"prettier-plugin-tailwindcss": "^0.2.2", "postcss-load-config": "^4.0.1",
"svelte": "^3.55.1", "svelte": "^3.55.1",
"svelte-check": "^3.0.0", "svelte-check": "^3.0.1",
"svelte-preprocess": "^4.10.7", "tailwindcss": "^3.1.5",
"tailwindcss": "^3.1.5", "tslib": "^2.4.1",
"tslib": "^2.4.1", "typescript": "^4.8.4",
"typescript": "^4.8.4", "vite": "^4.0.0"
"vite": "^4.0.0" }
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,13 @@
const tailwindcss = require("tailwindcss"); const tailwindcss = require('tailwindcss');
const autoprefixer = require("autoprefixer"); const autoprefixer = require('autoprefixer');
const config = { const config = {
plugins: [ plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind, //Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(), tailwindcss(),
//But others, like autoprefixer, need to run after, //But others, like autoprefixer, need to run after,
autoprefixer, autoprefixer
], ]
}; };
module.exports = config; module.exports = config;

View File

@ -1,13 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="dark"> <html lang="en" class="dark">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body class="fixed h-full w-full overflow-hidden font-sans antialiased text-base"> <body class="fixed h-full w-full overflow-hidden font-sans antialiased text-base">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

@ -1,4 +1,4 @@
@import "inter-ui/inter.css"; @import 'inter-ui/inter.css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@ -1,174 +1,164 @@
import { PUBLIC_API_BASE_URL } from "$env/static/public"; import { PUBLIC_API_BASE_URL } from '$env/static/public';
import * as log from "$lib/log"; import * as log from '$lib/log';
import { nanoid } from "nanoid"; 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(); const getUrl = (path: string) => new URL(path, apiUrl).toString();
export type LoginToken = { export type LoginToken = {
token: string; token: string;
expires: string; expires: string;
url: string; url: string;
}; };
export type User = { export type User = {
id: number; id: number;
name: string; name: string;
email: string; email: string;
picture: string; picture: string;
locale: string; locale: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
access_token: string; access_token: string;
}; };
export type Project = { export type Project = {
name: string; name: string;
description: string | null; description: string | null;
repository_id: string; repository_id: string;
git_url: string; git_url: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
}; };
const parseResponseJSON = async (response: Response) => { const parseResponseJSON = async (response: Response) => {
if (response.status === 204 || response.status === 205) { if (response.status === 204 || response.status === 205) {
return null; return null;
} else if (response.status >= 400) { } else if (response.status >= 400) {
throw new Error( throw new Error(`HTTP Error ${response.statusText}: ${await response.text()}`);
`HTTP Error ${response.statusText}: ${await response.text()}` } else {
); return await response.json();
} else { }
return await response.json();
}
}; };
type FetchMiddleware = (f: typeof fetch) => typeof fetch; type FetchMiddleware = (f: typeof fetch) => typeof fetch;
const fetchWith = ( const fetchWith = (fetch: typeof window.fetch, ...middlewares: FetchMiddleware[]) =>
fetch: typeof window.fetch, middlewares.reduce((f, middleware) => middleware(f), fetch);
...middlewares: FetchMiddleware[]
) => middlewares.reduce((f, middleware) => middleware(f), fetch);
const withRequestId: FetchMiddleware = (fetch) => async (url, options) => { const withRequestId: FetchMiddleware = (fetch) => async (url, options) => {
const requestId = nanoid(); const requestId = nanoid();
if (!options) options = {}; if (!options) options = {};
options.headers = { options.headers = {
...options?.headers, ...options?.headers,
"X-Request-Id": requestId, 'X-Request-Id': requestId
}; };
return fetch(url, options); return fetch(url, options);
}; };
const withLog: FetchMiddleware = (fetch) => async (url, options) => { const withLog: FetchMiddleware = (fetch) => async (url, options) => {
log.info("fetch", url, options); log.info('fetch', url, options);
try { try {
const resp = await fetch(url, options); const resp = await fetch(url, options);
log.info(resp); log.info(resp);
return resp; return resp;
} catch (e: any) { } catch (e: any) {
log.error("fetch", e); log.error('fetch', e);
throw e; throw e;
} }
}; };
export default ( export default (
{ fetch: realFetch }: { fetch: typeof window.fetch } = { { fetch: realFetch }: { fetch: typeof window.fetch } = {
fetch: window.fetch, fetch: window.fetch
} }
) => { ) => {
const fetch = fetchWith(realFetch, withRequestId, withLog); const fetch = fetchWith(realFetch, withRequestId, withLog);
return { return {
login: { login: {
token: { token: {
create: (params: {} = {}): Promise<LoginToken> => create: (params: {} = {}): Promise<LoginToken> =>
fetch(getUrl("login/token.json"), { fetch(getUrl('login/token.json'), {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json'
}, },
body: JSON.stringify(params), body: JSON.stringify(params)
}) })
.then(parseResponseJSON) .then(parseResponseJSON)
.then((token) => { .then((token) => {
const url = new URL(token.url); const url = new URL(token.url);
url.host = apiUrl.host; url.host = apiUrl.host;
return { return {
...token, ...token,
url: url.toString(), url: url.toString()
}; };
}), })
}, },
user: { user: {
get: (token: string): Promise<User> => get: (token: string): Promise<User> =>
fetch(getUrl(`login/user/${token}.json`), { fetch(getUrl(`login/user/${token}.json`), {
method: "GET", method: 'GET'
}).then(parseResponseJSON), }).then(parseResponseJSON)
}, }
}, },
user: { user: {
get: async (token: string): Promise<User> => get: async (token: string): Promise<User> =>
fetch(getUrl(`user.json`), { fetch(getUrl(`user.json`), {
method: "GET", method: 'GET',
headers: { headers: {
"X-Auth-Token": token, 'X-Auth-Token': token
}, }
}).then(parseResponseJSON), }).then(parseResponseJSON),
update: async ( update: async (token: string, params: { name?: string; picture?: File }) => {
token: string, const formData = new FormData();
params: { name?: string; picture?: File } if (params.name) {
) => { formData.append('name', params.name);
const formData = new FormData(); }
if (params.name) { if (params.picture) {
formData.append("name", params.name); formData.append('avatar', params.picture);
} }
if (params.picture) { return fetch(getUrl(`user.json`), {
formData.append("avatar", params.picture); method: 'PUT',
} headers: {
return fetch(getUrl(`user.json`), { 'X-Auth-Token': token
method: "PUT", },
headers: { body: formData
"X-Auth-Token": token, }).then(parseResponseJSON);
}, }
body: formData, },
}).then(parseResponseJSON); projects: {
}, create: (token: string, params: { name: string; uid?: string }): Promise<Project> =>
}, fetch(getUrl('projects.json'), {
projects: { method: 'POST',
create: ( headers: {
token: string, 'Content-Type': 'application/json',
params: { name: string; uid?: string } 'X-Auth-Token': token
): Promise<Project> => },
fetch(getUrl("projects.json"), { body: JSON.stringify(params)
method: "POST", }).then(parseResponseJSON),
headers: { list: (token: string): Promise<Project[]> =>
"Content-Type": "application/json", fetch(getUrl('projects.json'), {
"X-Auth-Token": token, method: 'GET',
}, headers: {
body: JSON.stringify(params), 'X-Auth-Token': token
}).then(parseResponseJSON), }
list: (token: string): Promise<Project[]> => }).then(parseResponseJSON),
fetch(getUrl("projects.json"), { get: (token: string, repositoryId: string): Promise<Project> =>
method: "GET", fetch(getUrl(`projects/${repositoryId}.json`), {
headers: { method: 'GET',
"X-Auth-Token": token, headers: {
}, 'X-Auth-Token': token
}).then(parseResponseJSON), }
get: (token: string, repositoryId: string): Promise<Project> => }).then(parseResponseJSON),
fetch(getUrl(`projects/${repositoryId}.json`), { delete: (token: string, repositoryId: string): Promise<void> =>
method: "GET", fetch(getUrl(`projects/${repositoryId}.json`), {
headers: { method: 'DELETE',
"X-Auth-Token": token, headers: {
}, 'X-Auth-Token': token
}).then(parseResponseJSON), }
delete: (token: string, repositoryId: string): Promise<void> => }).then(parseResponseJSON)
fetch(getUrl(`projects/${repositoryId}.json`), { }
method: "DELETE", };
headers: {
"X-Auth-Token": token,
},
}).then(parseResponseJSON),
},
};
}; };

View File

@ -1,3 +1,3 @@
export const toHumanBranchName = (branch: string) => { export const toHumanBranchName = (branch: string) => {
return branch.replace("refs/heads/", ""); return branch.replace('refs/heads/', '');
}; };

View File

@ -1,18 +1,14 @@
<script> <script>
import FaArrowLeft from "svelte-icons/fa/FaArrowLeft.svelte"; import FaArrowLeft from 'svelte-icons/fa/FaArrowLeft.svelte';
import FaArrowRight from "svelte-icons/fa/FaArrowRight.svelte"; import FaArrowRight from 'svelte-icons/fa/FaArrowRight.svelte';
let history = window.history; let history = window.history;
</script> </script>
<div class="flex items-center justify-center space-x-3 text-zinc-400"> <div class="flex items-center justify-center space-x-3 text-zinc-400">
<button <button class="w-4 h-4 hover:text-zinc-200" title="Go back" on:click={() => history.back()}
class="w-4 h-4 hover:text-zinc-200" ><FaArrowLeft /></button
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 forward"
on:click={() => history.forward()}><FaArrowRight /></button
>
</div> </div>

View File

@ -1,42 +1,35 @@
<script lang="ts"> <script lang="ts">
import type { Project } from "$lib/projects"; import type { Project } from '$lib/projects';
import type { Session } from "$lib/sessions"; import type { Session } from '$lib/sessions';
import { toHumanReadableTime } from "$lib/time"; import { toHumanReadableTime } from '$lib/time';
import { getContext } from "svelte"; import { getContext } from 'svelte';
import type { Writable } from "svelte/store"; import type { Writable } from 'svelte/store';
import IoIosBowtie from "svelte-icons/io/IoIosBowtie.svelte"; import IoIosBowtie from 'svelte-icons/io/IoIosBowtie.svelte';
import MdKeyboardArrowRight from "svelte-icons/md/MdKeyboardArrowRight.svelte"; import MdKeyboardArrowRight from 'svelte-icons/md/MdKeyboardArrowRight.svelte';
let project: Writable<Project | null | undefined> = getContext("project"); let project: Writable<Project | null | undefined> = getContext('project');
let session: Writable<Session | null | undefined> = getContext("session"); let session: Writable<Session | null | undefined> = getContext('session');
</script> </script>
<div <div class="flex flex-row items-center space-x-1 bg-zinc-900 text-zinc-400 h-8">
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">
<a class="hover:text-zinc-200" href="/"> <IoIosBowtie />
<div class="w-6 h-6"> </div>
<IoIosBowtie /> </a>
</div> {#if $project}
</a> <div class="w-8 h-8 text-zinc-700">
{#if $project} <MdKeyboardArrowRight />
<div class="w-8 h-8 text-zinc-700"> </div>
<MdKeyboardArrowRight /> <a class="hover:text-zinc-200" href="/projects/{$project.id}">{$project.title}</a>
</div> {/if}
<a class="hover:text-zinc-200" href="/projects/{$project.id}" {#if $project && $session}
>{$project.title}</a <div class="w-8 h-8 text-zinc-700">
> <MdKeyboardArrowRight />
{/if} </div>
{#if $project && $session} <a class="hover:text-zinc-200" href="/projects/{$project.id}/sessions/{$session.id}">
<div class="w-8 h-8 text-zinc-700"> {toHumanReadableTime($session.meta.startTimestampMs)}
<MdKeyboardArrowRight /> {toHumanReadableTime($session.meta.lastTimestampMs)}
</div> </a>
<a {/if}
class="hover:text-zinc-200"
href="/projects/{$project.id}/sessions/{$session.id}"
>
{toHumanReadableTime($session.meta.startTimestampMs)}
{toHumanReadableTime($session.meta.lastTimestampMs)}
</a>
{/if}
</div> </div>

View File

@ -1,77 +1,75 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from 'svelte';
import { EditorState } from "@codemirror/state"; import { EditorState } from '@codemirror/state';
import { EditorView, lineNumbers } from "@codemirror/view"; import { EditorView, lineNumbers } from '@codemirror/view';
let editorTheme = EditorView.theme( let editorTheme = EditorView.theme(
{ {
"&": { '&': {
color: "#d4d4d8", color: '#d4d4d8',
backgroundColor: "#18181b", backgroundColor: '#18181b'
}, },
".cm-content": { '.cm-content': {
caretColor: "#0e9", caretColor: '#0e9'
}, },
"&.cm-focused .cm-cursor": { '&.cm-focused .cm-cursor': {
borderLeftColor: "#0e9", borderLeftColor: '#0e9'
}, },
"&.cm-focused .cm-selectionBackground, ::selection": { '&.cm-focused .cm-selectionBackground, ::selection': {
backgroundColor: "#0284c7", backgroundColor: '#0284c7'
}, },
".cm-gutters": { '.cm-gutters': {
backgroundColor: "#18181b", backgroundColor: '#18181b',
color: "#3f3f46", color: '#3f3f46',
border: "none", border: 'none'
}, }
}, },
{ dark: true } { dark: true }
); );
const fixedHeightEditor = EditorView.theme({ const fixedHeightEditor = EditorView.theme({
"&": { height: "600px" }, '&': { height: '600px' },
".cm-scroller": { overflow: "auto" }, '.cm-scroller': { overflow: 'auto' }
}); });
export let value: string; export let value: string;
let element: HTMLDivElement; let element: HTMLDivElement;
let editorView: EditorView; let editorView: EditorView;
onMount(() => (editorView = create_editor_view(value))); onMount(() => (editorView = create_editor_view(value)));
onDestroy(() => editorView?.destroy()); onDestroy(() => editorView?.destroy());
$: editorView && update(value); $: editorView && update(value);
// There may be a more graceful way to update the two editors // There may be a more graceful way to update the two editors
function update(value: string): void { function update(value: string): void {
editorView?.destroy(); editorView?.destroy();
editorView = create_editor_view(value); editorView = create_editor_view(value);
} }
function create_editor_state( function create_editor_state(value: string | null | undefined): EditorState {
value: string | null | undefined return EditorState.create({
): EditorState { doc: value ?? undefined,
return EditorState.create({ extensions: state_extensions
doc: value ?? undefined, });
extensions: state_extensions, }
});
}
function create_editor_view(value: string): EditorView { function create_editor_view(value: string): EditorView {
return new EditorView({ return new EditorView({
state: create_editor_state(value), state: create_editor_state(value),
parent: element, parent: element
}); });
} }
let state_extensions = [ let state_extensions = [
EditorView.editable.of(false), EditorView.editable.of(false),
EditorView.lineWrapping, EditorView.lineWrapping,
lineNumbers(), lineNumbers(),
editorTheme, editorTheme,
fixedHeightEditor, fixedHeightEditor
]; ];
</script> </script>
<code> <code>
<div bind:this={element} /> <div bind:this={element} />
</code> </code>

View File

@ -1,51 +1,45 @@
<script lang="ts"> <script lang="ts">
import type Users from "$lib/users"; import type Users from '$lib/users';
import type Api from "$lib/api"; import type Api from '$lib/api';
import type { LoginToken } from "$lib/api"; import type { LoginToken } from '$lib/api';
import { derived, writable } from "svelte/store"; import { derived, writable } from 'svelte/store';
import { open } from "@tauri-apps/api/shell"; import { open } from '@tauri-apps/api/shell';
export let user: Awaited<ReturnType<typeof Users>>; export let user: Awaited<ReturnType<typeof Users>>;
export let api: Awaited<ReturnType<typeof Api>>; export let api: Awaited<ReturnType<typeof Api>>;
const pollForUser = async (token: string) => { const pollForUser = async (token: string) => {
const apiUser = await api.login.user.get(token).catch(() => null); const apiUser = await api.login.user.get(token).catch(() => null);
if (apiUser) { if (apiUser) {
user.set(apiUser); user.set(apiUser);
return apiUser; return apiUser;
} }
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(async () => { setTimeout(async () => {
resolve(await pollForUser(token)); resolve(await pollForUser(token));
}, 1000); }, 1000);
}); });
}; };
const token = writable<LoginToken | null>(null); const token = writable<LoginToken | null>(null);
const authUrl = derived(token, ($token) => $token?.url as string); const authUrl = derived(token, ($token) => $token?.url as string);
</script> </script>
<div> <div>
{#if $user} {#if $user}
<button <button class="text-zinc-400 hover:underline" on:click={() => user.delete()}>Log out</button>
class="text-zinc-400 hover:underline" {:else if $token !== null}
on:click={() => user.delete()}>Log out</button {#await Promise.all([open($token.url), pollForUser($token.token)])}
> <div>Log in in your system browser</div>
{:else if $token !== null} {/await}
{#await Promise.all([open($token.url), pollForUser($token.token)])} <p>
<div>Log in in your system browser</div> <button class="underline" on:click={() => open($authUrl)}>Click here</button>
{/await} if you are not redirected automatically, you can
<p> </p>
<button class="underline" on:click={() => open($authUrl)} {:else}
>Click here</button <button
> class="py-1 px-3 rounded text-white bg-blue-400"
if you are not redirected automatically, you can on:click={() => api.login.token.create().then(token.set)}>Sign up or Log in</button
</p> >
{:else} {/if}
<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> </div>

View File

@ -1,4 +1,4 @@
export { default as CodeViewer } from "./CodeViewer.svelte"; export { default as CodeViewer } from './CodeViewer.svelte';
export { default as BackForwardButtons } from "./BackForwardButtons.svelte"; export { default as BackForwardButtons } from './BackForwardButtons.svelte';
export { default as Login } from "./Login.svelte"; export { default as Login } from './Login.svelte';
export { default as Breadcrumbs } from "./Breadcrumbs.svelte"; export { default as Breadcrumbs } from './Breadcrumbs.svelte';

View File

@ -1,111 +1,104 @@
<script lang="ts"> <script lang="ts">
import { themeIcons } from "seti-icons"; import { themeIcons } from 'seti-icons';
import type { Session } from "$lib/sessions"; import type { Session } from '$lib/sessions';
import { toHumanReadableTime, toHumanReadableDate } from "$lib/time"; import { toHumanReadableTime, toHumanReadableDate } from '$lib/time';
import { toHumanBranchName } from "$lib/branch"; import { toHumanBranchName } from '$lib/branch';
import TimelineDaySessionActivities from "./TimelineDaySessionActivities.svelte"; import TimelineDaySessionActivities from './TimelineDaySessionActivities.svelte';
import { list } from "$lib/deltas"; import { list } from '$lib/deltas';
export let session: Session; export let session: Session;
export let projectId: string; export let projectId: string;
const getIcon = themeIcons({ const getIcon = themeIcons({
blue: "#268bd2", blue: '#268bd2',
grey: "#657b83", grey: '#657b83',
"grey-light": "#839496", 'grey-light': '#839496',
green: "#859900", green: '#859900',
orange: "#cb4b16", orange: '#cb4b16',
pink: "#d33682", pink: '#d33682',
purple: "#6c71c4", purple: '#6c71c4',
red: "#dc322f", red: '#dc322f',
white: "#fdf6e3", white: '#fdf6e3',
yellow: "#b58900", yellow: '#b58900',
ignore: "#586e75", ignore: '#586e75'
}); });
function pathToName(path: string) { function pathToName(path: string) {
return path.split("/").slice(-1)[0]; return path.split('/').slice(-1)[0];
} }
function pathToIconSvg(path: string) { function pathToIconSvg(path: string) {
let name: string = pathToName(path); let name: string = pathToName(path);
let { svg, color } = getIcon(name); let { svg, color } = getIcon(name);
return svg; return svg;
} }
const colorFromBranchName = (branchName: string) => { const colorFromBranchName = (branchName: string) => {
const colors = [ const colors = [
"bg-red-500 border-red-700", 'bg-red-500 border-red-700',
"bg-green-500 border-green-700", 'bg-green-500 border-green-700',
"bg-blue-500 border-blue-700", 'bg-blue-500 border-blue-700',
"bg-yellow-500 border-yellow-700", 'bg-yellow-500 border-yellow-700',
"bg-purple-500 border-purple-700", 'bg-purple-500 border-purple-700',
"bg-pink-500 border-pink-700", 'bg-pink-500 border-pink-700',
"bg-indigo-500 border-indigo-700", 'bg-indigo-500 border-indigo-700',
"bg-orange-500 border-orange-700", 'bg-orange-500 border-orange-700'
]; ];
const hash = branchName.split("").reduce((acc, char) => { const hash = branchName.split('').reduce((acc, char) => {
return acc + char.charCodeAt(0); return acc + char.charCodeAt(0);
}, 0); }, 0);
return colors[hash % colors.length]; return colors[hash % colors.length];
}; };
</script> </script>
<div class="flex flex-col space-y-2"> <div class="flex flex-col space-y-2">
<span class="relative inline-flex"> <span class="relative inline-flex">
<a <a
id="block" 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( 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 session.meta.branch
)}" )}"
title={session.meta.branch} title={session.meta.branch}
href="/projects/{projectId}/sessions/{session.id}/" href="/projects/{projectId}/sessions/{session.id}/"
> >
{toHumanBranchName(session.meta.branch)} {toHumanBranchName(session.meta.branch)}
</a> </a>
{#if !session.hash} {#if !session.hash}
<span <span class="flex absolute h-3 w-3 top-0 right-0 -mt-1 -mr-1" title="Current session">
class="flex absolute h-3 w-3 top-0 right-0 -mt-1 -mr-1" <span
title="Current session" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-200 opacity-75"
> />
<span <span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-200 opacity-75" class="relative inline-flex rounded-full h-3 w-3 bg-zinc-200 border border-orange-200"
/> />
<span </span>
class="relative inline-flex rounded-full h-3 w-3 bg-zinc-200 border border-orange-200" {/if}
/> </span>
</span> <div id="activities">
{/if} <div class="my-2 mx-1">
</span> <TimelineDaySessionActivities
<div id="activities"> activities={session.activity}
<div class="my-2 mx-1"> sessionStart={session.meta.startTimestampMs}
<TimelineDaySessionActivities sessionEnd={session.meta.lastTimestampMs}
activities={session.activity} />
sessionStart={session.meta.startTimestampMs} </div>
sessionEnd={session.meta.lastTimestampMs} </div>
/> <div id="time-range" class="">
</div> {toHumanReadableDate(session.meta.startTimestampMs)},
</div> {toHumanReadableTime(session.meta.startTimestampMs)}
<div id="time-range" class=""> <div class=" text-zinc-600">
{toHumanReadableDate(session.meta.startTimestampMs)}, {Math.round((session.meta.lastTimestampMs - session.meta.startTimestampMs) / 60 / 1000)} min
{toHumanReadableTime(session.meta.startTimestampMs)} </div>
<div class=" text-zinc-600"> </div>
{Math.round( <div id="files">
(session.meta.lastTimestampMs - session.meta.startTimestampMs) / {#await list({ projectId: projectId, sessionId: session.id }) then deltas}
60 / {#each Object.keys(deltas) as delta}
1000 <div class="flex flex-row w-32 items-center">
)} min <div class="w-6 h-6 text-white fill-blue-400">
</div> {@html pathToIconSvg(delta)}
</div> </div>
<div id="files"> <div class="text-white w-24 truncate">
{#await list( { projectId: projectId, sessionId: session.id } ) then deltas} {pathToName(delta)}
{#each Object.keys(deltas) as delta} </div>
<div class="flex flex-row w-32 items-center"> </div>
<div class="w-6 h-6 text-white fill-blue-400"> {/each}
{@html pathToIconSvg(delta)} {/await}
</div> </div>
<div class="text-white w-24 truncate">
{pathToName(delta)}
</div>
</div>
{/each}
{/await}
</div>
</div> </div>

View File

@ -1,63 +1,59 @@
<script lang="ts"> <script lang="ts">
import type { Activity } from "$lib/sessions"; import type { Activity } from '$lib/sessions';
import FaSquare from "svelte-icons/fa/FaSquare.svelte"; import FaSquare from 'svelte-icons/fa/FaSquare.svelte';
import FaCircle from "svelte-icons/fa/FaCircle.svelte"; import FaCircle from 'svelte-icons/fa/FaCircle.svelte';
import FaAdjust from "svelte-icons/fa/FaAdjust.svelte"; import FaAdjust from 'svelte-icons/fa/FaAdjust.svelte';
import FaMapMarker from "svelte-icons/fa/FaMapMarker.svelte"; import FaMapMarker from 'svelte-icons/fa/FaMapMarker.svelte';
export let activities: Activity[]; export let activities: Activity[];
export let sessionStart: number; export let sessionStart: number;
export let sessionEnd: number; export let sessionEnd: number;
$: sessionDuration = sessionEnd - sessionStart; $: sessionDuration = sessionEnd - sessionStart;
let proportionOfTime = (time: number) => { let proportionOfTime = (time: number) => {
return ((time - sessionStart) / sessionDuration) * 100; return ((time - sessionStart) / sessionDuration) * 100;
}; };
const toHumanReadableTime = (timestamp: number) => { const toHumanReadableTime = (timestamp: number) => {
return new Date(timestamp).toLocaleTimeString("en-US", { return new Date(timestamp).toLocaleTimeString('en-US', {
hour: "numeric", hour: 'numeric',
minute: "numeric", minute: 'numeric'
}); });
}; };
</script> </script>
<div class="relative"> <div class="relative">
<hr class="h-px bg-slate-400 border-0 z-0" /> <hr class="h-px bg-slate-400 border-0 z-0" />
<div class="absolute inset-0 -mt-1.5"> <div class="absolute inset-0 -mt-1.5">
{#each activities as activity} {#each activities as activity}
<div <div
class="flex -mx-1.5" class="flex -mx-1.5"
style="position:relative; left: {proportionOfTime( style="position:relative; left: {proportionOfTime(activity.timestampMs)}%;"
activity.timestampMs >
)}%;" <div
> class="w-3 h-3 text-slate-700 z-50 absolute inset-0"
<div style=""
class="w-3 h-3 text-slate-700 z-50 absolute inset-0" title="{activity.type}: {activity.message} at {toHumanReadableTime(activity.timestampMs)}"
style="" >
title="{activity.type}: {activity.message} at {toHumanReadableTime( {#if activity.type === 'commit'}
activity.timestampMs <div class="text-sky-500 hover:text-sky-600">
)}" <FaSquare />
> </div>
{#if activity.type === "commit"} {:else if activity.type === 'merge'}
<div class="text-sky-500 hover:text-sky-600"> <div class="text-green-500 hover:text-green-600">
<FaSquare /> <FaMapMarker />
</div> </div>
{:else if activity.type === "merge"} {:else if activity.type === 'rebase'}
<div class="text-green-500 hover:text-green-600"> <div class="text-orange-500 hover:text-orange-600">
<FaMapMarker /> <FaAdjust />
</div> </div>
{:else if activity.type === "rebase"} {:else if activity.type === 'push'}
<div class="text-orange-500 hover:text-orange-600"> <div class="text-purple-500 hover:text-purple-600">
<FaAdjust /> <FaCircle />
</div> </div>
{:else if activity.type === "push"} {/if}
<div class="text-purple-500 hover:text-purple-600"> </div>
<FaCircle /> </div>
</div> {/each}
{/if} </div>
</div>
</div>
{/each}
</div>
</div> </div>

View File

@ -1 +1 @@
export { default as TimelineDaySession } from "./TimelineDaySession.svelte"; export { default as TimelineDaySession } from './TimelineDaySession.svelte';

View File

@ -1,50 +1,47 @@
<script lang="ts"> <script lang="ts">
import { toHumanBranchName } from "$lib/branch"; import { toHumanBranchName } from '$lib/branch';
export let startTime: Date; export let startTime: Date;
export let endTime: Date; export let endTime: Date;
export let label: string; export let label: string;
export let href: string; export let href: string;
const timeToGridRow = (time: Date) => { const timeToGridRow = (time: Date) => {
const hours = time.getHours(); const hours = time.getHours();
const minutes = time.getMinutes(); const minutes = time.getMinutes();
const totalMinutes = hours * 60 + minutes; const totalMinutes = hours * 60 + minutes;
const totalMinutesPerDay = 24 * 60; const totalMinutesPerDay = 24 * 60;
const gridRow = Math.floor((totalMinutes / totalMinutesPerDay) * 96); const gridRow = Math.floor((totalMinutes / totalMinutesPerDay) * 96);
return gridRow + 1; // offset the first row return gridRow + 1; // offset the first row
}; };
const dateToGridCol = (date: Date) => { const dateToGridCol = (date: Date) => {
return date.getDay(); return date.getDay();
}; };
const timeToSpan = (startTime: Date, endTime: Date) => { const timeToSpan = (startTime: Date, endTime: Date) => {
const startMinutes = startTime.getHours() * 60 + startTime.getMinutes(); const startMinutes = startTime.getHours() * 60 + startTime.getMinutes();
const endMinutes = endTime.getHours() * 60 + endTime.getMinutes(); const endMinutes = endTime.getHours() * 60 + endTime.getMinutes();
const span = Math.round((endMinutes - startMinutes) / 15); // 4 spans per hour const span = Math.round((endMinutes - startMinutes) / 15); // 4 spans per hour
if (span < 1) { if (span < 1) {
return 1; return 1;
} else { } else {
return span; return span;
} }
}; };
</script> </script>
<li <li
class="relative mt-px flex col-start-{dateToGridCol(startTime)}" class="relative mt-px flex col-start-{dateToGridCol(startTime)}"
style="grid-row: {timeToGridRow(startTime)} / span {timeToSpan( style="grid-row: {timeToGridRow(startTime)} / span {timeToSpan(startTime, endTime)};"
startTime,
endTime
)};"
> >
<a <a
{href} {href}
title={startTime.toLocaleTimeString()} 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" 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"> <p class="order-1 font-semibold text-zinc-800">
{toHumanBranchName(label)} {toHumanBranchName(label)}
</p> </p>
</a> </a>
</li> </li>

View File

@ -1 +1 @@
export { default as WeekBlockEntry } from "./WeekBlockEntry.svelte"; export { default as WeekBlockEntry } from './WeekBlockEntry.svelte';

View File

@ -1,7 +1,7 @@
import { log } from "$lib"; import { log } from '$lib';
import { invoke } from "@tauri-apps/api"; import { invoke } from '@tauri-apps/api';
import { appWindow } from "@tauri-apps/api/window"; import { appWindow } from '@tauri-apps/api/window';
import { writable, type Readable } from "svelte/store"; import { writable, type Readable } from 'svelte/store';
export type OperationDelete = { delete: [number, number] }; export type OperationDelete = { delete: [number, number] };
export type OperationInsert = { insert: [number, string] }; export type OperationInsert = { insert: [number, string] };
@ -9,37 +9,35 @@ export type OperationInsert = { insert: [number, string] };
export type Operation = OperationDelete | OperationInsert; export type Operation = OperationDelete | OperationInsert;
export namespace Operation { export namespace Operation {
export const isDelete = ( export const isDelete = (operation: Operation): operation is OperationDelete =>
operation: Operation 'delete' in operation;
): operation is OperationDelete => "delete" in operation;
export const isInsert = ( export const isInsert = (operation: Operation): operation is OperationInsert =>
operation: Operation 'insert' in operation;
): operation is OperationInsert => "insert" in operation;
} }
export type Delta = { timestampMs: number; operations: Operation[] }; export type Delta = { timestampMs: number; operations: Operation[] };
type DeltasEvent = { type DeltasEvent = {
deltas: Delta[]; deltas: Delta[];
filePath: string; filePath: string;
}; };
export const list = (params: { projectId: string; sessionId: 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 }) => { 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 store = writable<Record<string, Delta[]>>(init);
const eventName = `project://${params.projectId}/sessions/${params.sessionId}/deltas`; const eventName = `project://${params.projectId}/sessions/${params.sessionId}/deltas`;
await appWindow.listen<DeltasEvent>(eventName, (event) => { await appWindow.listen<DeltasEvent>(eventName, (event) => {
log.info(`Received deltas event ${eventName}`); log.info(`Received deltas event ${eventName}`);
store.update((deltas) => ({ store.update((deltas) => ({
...deltas, ...deltas,
[event.payload.filePath]: event.payload.deltas, [event.payload.filePath]: event.payload.deltas
})); }));
}); });
return store as Readable<Record<string, Delta[]>>; return store as Readable<Record<string, Delta[]>>;
}; };

View File

@ -1,6 +1,6 @@
export * as deltas from "./deltas"; export * as deltas from './deltas';
export * as projects from "./projects"; export * as projects from './projects';
export * as log from "./log"; export * as log from './log';
export * as toasts from "./toasts"; export * as toasts from './toasts';
export * as sessions from "./sessions"; export * as sessions from './sessions';
export * as week from "./week"; export * as week from './week';

View File

@ -1,39 +1,36 @@
import { building } from "$app/environment"; import { building } from '$app/environment';
export const setup = async () => { export const setup = async () => {
if (!building) { if (!building) {
await (await import("tauri-plugin-log-api")).attachConsole(); await (await import('tauri-plugin-log-api')).attachConsole();
} }
}; };
const logger = async () => const logger = async () =>
building building
? { ? {
debug: (..._: any[]) => { }, debug: (..._: any[]) => {},
info: (..._: any[]) => { }, info: (..._: any[]) => {},
error: (..._: any[]) => { }, error: (..._: any[]) => {}
} }
: import("tauri-plugin-log-api").then((tauri) => ({ : import('tauri-plugin-log-api').then((tauri) => ({
debug: tauri.debug, debug: tauri.debug,
info: tauri.info, info: tauri.info,
error: tauri.error, error: tauri.error
})); }));
const toString = (value: any) => { const toString = (value: any) => {
if (value instanceof Error) { if (value instanceof Error) {
return value.message; return value.message;
} else if (typeof value === "object") { } else if (typeof value === 'object') {
return JSON.stringify(value); return JSON.stringify(value);
} else { } else {
return value.toString(); return value.toString();
} }
}; };
export const debug = async (...args: any[]) => export const debug = async (...args: any[]) => (await logger()).debug(args.map(toString).join(' '));
(await logger()).debug(args.map(toString).join(" "));
export const info = async (...args: any[]) => export const info = async (...args: any[]) => (await logger()).info(args.map(toString).join(' '));
(await logger()).info(args.map(toString).join(" "));
export const error = async (...args: any[]) => export const error = async (...args: any[]) => (await logger()).error(args.map(toString).join(' '));
(await logger()).error(args.map(toString).join(" "));

View File

@ -1,31 +1,31 @@
import posthog from "posthog-js"; import posthog from 'posthog-js';
import { PUBLIC_POSTHOG_API_KEY } from "$env/static/public"; import { PUBLIC_POSTHOG_API_KEY } from '$env/static/public';
import type { User } from "$lib/api"; import type { User } from '$lib/api';
import * as log from "$lib/log"; import * as log from '$lib/log';
export default () => { export default () => {
const instance = posthog.init(PUBLIC_POSTHOG_API_KEY, { const instance = posthog.init(PUBLIC_POSTHOG_API_KEY, {
api_host: "https://eu.posthog.com", api_host: 'https://eu.posthog.com',
capture_performance: false, capture_performance: false
}); });
log.info("posthog initialized"); log.info('posthog initialized');
return { return {
identify: (user: User | undefined) => { identify: (user: User | undefined) => {
if (user) { if (user) {
log.info("posthog identify", { log.info('posthog identify', {
id: user.id, id: user.id,
name: user.name, name: user.name,
email: user.email, email: user.email
}); });
instance?.identify(`user_${user.id}`, { instance?.identify(`user_${user.id}`, {
email: user.email, email: user.email,
name: user.name, name: user.name
}); });
} else { } else {
log.info("posthog reset"); log.info('posthog reset');
instance?.capture("log-out"); instance?.capture('log-out');
instance?.reset(); instance?.reset();
} }
}, }
}; };
}; };

View File

@ -1,67 +1,58 @@
import { invoke } from "@tauri-apps/api"; import { invoke } from '@tauri-apps/api';
import { derived, writable } from "svelte/store"; import { derived, writable } from 'svelte/store';
import type { Project as ApiProject } from "$lib/api"; import type { Project as ApiProject } from '$lib/api';
export type Project = { export type Project = {
id: string; id: string;
title: string; title: string;
path: string; path: string;
api: ApiProject & { sync: boolean }; api: ApiProject & { sync: boolean };
}; };
const list = () => invoke<Project[]>("list_projects"); const list = () => invoke<Project[]>('list_projects');
const update = (params: { const update = (params: {
project: { project: {
id: string; id: string;
title?: string; title?: string;
api?: ApiProject & { sync: boolean }; api?: ApiProject & { sync: boolean };
}; };
}) => invoke<Project>("update_project", params); }) => invoke<Project>('update_project', params);
const add = (params: { path: string }) => const add = (params: { path: string }) => invoke<Project>('add_project', params);
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 () => { export default async () => {
const init = await list(); const init = await list();
const store = writable<Project[]>(init); const store = writable<Project[]>(init);
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
get: (id: string) => { get: (id: string) => {
const project = derived(store, (store) => const project = derived(store, (store) => store.find((p) => p.id === id));
store.find((p) => p.id === id) return {
); subscribe: project.subscribe,
return { update: (params: { title?: string; api?: Project['api'] }) =>
subscribe: project.subscribe, update({
update: (params: { title?: string; api?: Project["api"] }) => project: {
update({ id,
project: { ...params
id, }
...params, }).then((project) => {
}, store.update((projects) => projects.map((p) => (p.id === project.id ? project : p)));
}).then((project) => { return project;
store.update((projects) => })
projects.map((p) => };
p.id === project.id ? project : p },
) add: (params: { path: string }) =>
); add(params).then((project) => {
return project; store.update((projects) => [...projects, project]);
}), return project;
}; }),
}, delete: (params: { id: string }) =>
add: (params: { path: string }) => del(params).then(() => {
add(params).then((project) => { store.update((projects) => projects.filter((p) => p.id !== params.id));
store.update((projects) => [...projects, project]); })
return project; };
}),
delete: (params: { id: string }) =>
del(params).then(() => {
store.update((projects) =>
projects.filter((p) => p.id !== params.id)
);
}),
};
}; };

View File

@ -1,56 +1,49 @@
import { invoke } from "@tauri-apps/api"; import { invoke } from '@tauri-apps/api';
import { appWindow } from "@tauri-apps/api/window"; import { appWindow } from '@tauri-apps/api/window';
import { writable } from "svelte/store"; import { writable } from 'svelte/store';
import { log } from "$lib"; import { log } from '$lib';
export type Activity = { export type Activity = {
type: string; type: string;
timestampMs: number; timestampMs: number;
message: string; message: string;
}; };
export type Session = { export type Session = {
id: string; id: string;
hash?: string; hash?: string;
meta: { meta: {
startTimestampMs: number; startTimestampMs: number;
lastTimestampMs: number; lastTimestampMs: number;
branch: string; branch: string;
commit: string; commit: string;
}; };
activity: Activity[]; activity: Activity[];
}; };
export const listFiles = (params: { projectId: string; sessionId: string }) => 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 }) => const list = (params: { projectId: string }) => invoke<Session[]>('list_sessions', params);
invoke<Session[]>("list_sessions", params);
export default async (params: { projectId: string }) => { export default async (params: { projectId: string }) => {
const init = await list(params); const init = await list(params);
const store = writable(init); const store = writable(init);
const eventName = `project://${params.projectId}/sessions`; const eventName = `project://${params.projectId}/sessions`;
await appWindow.listen<Session>(eventName, (event) => { await appWindow.listen<Session>(eventName, (event) => {
log.info(`Received sessions event ${eventName}`); log.info(`Received sessions event ${eventName}`);
store.update((sessions) => { store.update((sessions) => {
const index = sessions.findIndex( const index = sessions.findIndex((session) => session.id === event.payload.id);
(session) => session.id === event.payload.id if (index === -1) {
); return [...sessions, event.payload];
if (index === -1) { } else {
return [...sessions, event.payload]; return [...sessions.slice(0, index), event.payload, ...sessions.slice(index + 1)];
} else { }
return [ });
...sessions.slice(0, index), });
event.payload,
...sessions.slice(index + 1),
];
}
});
});
return { return {
subscribe: store.subscribe, subscribe: store.subscribe
}; };
}; };

View File

@ -1,12 +1,12 @@
export const toHumanReadableTime = (timestamp: number) => { export const toHumanReadableTime = (timestamp: number) => {
return new Date(timestamp).toLocaleTimeString("en-US", { return new Date(timestamp).toLocaleTimeString('en-US', {
hour: "numeric", hour: 'numeric',
minute: "numeric", minute: 'numeric'
}); });
}; };
export const toHumanReadableDate = (timestamp: number) => { export const toHumanReadableDate = (timestamp: number) => {
return new Date(timestamp).toLocaleDateString("en-US", { return new Date(timestamp).toLocaleDateString('en-US', {
dateStyle: "short", dateStyle: 'short'
}); });
}; };

View File

@ -1,15 +1,12 @@
import toast, { import toast, { type ToastOptions, type ToastPosition } from 'svelte-french-toast';
type ToastOptions,
type ToastPosition,
} from "svelte-french-toast";
const defaultOptions = { const defaultOptions = {
position: "bottom-center" as ToastPosition, position: 'bottom-center' as ToastPosition,
style: "border-radius: 200px; background: #333; color: #fff;", style: 'border-radius: 200px; background: #333; color: #fff;'
}; };
export const error = (msg: string, options: ToastOptions = {}) => export const error = (msg: string, options: ToastOptions = {}) =>
toast.error(msg, { ...defaultOptions, ...options }); toast.error(msg, { ...defaultOptions, ...options });
export const success = (msg: string, options: ToastOptions = {}) => export const success = (msg: string, options: ToastOptions = {}) =>
toast.success(msg, { ...defaultOptions, ...options }); toast.success(msg, { ...defaultOptions, ...options });

View File

@ -1,27 +1,27 @@
import type { User } from "$lib/api"; import type { User } from '$lib/api';
import { writable } from "svelte/store"; import { writable } from 'svelte/store';
import { invoke } from "@tauri-apps/api"; 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 () => { export default async () => {
const store = writable<User | undefined>(undefined); const store = writable<User | undefined>(undefined);
const init = await get(); const init = await get();
store.set(init); store.set(init);
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
set: async (user: User) => { set: async (user: User) => {
await set({ user }); await set({ user });
store.set(user); store.set(user);
}, },
delete: async () => { delete: async () => {
await del(); await del();
store.set(undefined); store.set(undefined);
}, }
}; };
}; };

View File

@ -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 = { export type Week = {
start: Date; start: Date;
end: Date; end: Date;
}; };
export namespace Week { export namespace Week {
export const from = (date: Date): Week => { export const from = (date: Date): Week => {
return { return {
start: startOfWeek(date, { weekStartsOn: 1 }), start: startOfWeek(date, { weekStartsOn: 1 }),
end: endOfWeek(date), end: endOfWeek(date)
}; };
}; };
export const next = (week: Week): Week => { export const next = (week: Week): Week => {
return { start: addWeeks(week.start, 1), end: addWeeks(week.end, 1) }; return { start: addWeeks(week.start, 1), end: addWeeks(week.end, 1) };
}; };
export const previous = (week: Week): Week => { export const previous = (week: Week): Week => {
return { start: subWeeks(week.start, 1), end: subWeeks(week.end, 1) }; return { start: subWeeks(week.start, 1), end: subWeeks(week.end, 1) };
}; };
export const nThDay = (week: Week, n: number): Date => { export const nThDay = (week: Week, n: number): Date => {
return addDays(week.start, n); return addDays(week.start, n);
}; };
} }

View File

@ -1,52 +1,45 @@
<script lang="ts"> <script lang="ts">
import "../app.postcss"; import '../app.postcss';
import { Toaster } from "svelte-french-toast"; import { Toaster } from 'svelte-french-toast';
import type { LayoutData } from "./$types"; import type { LayoutData } from './$types';
import { BackForwardButtons } from "$lib/components"; import { BackForwardButtons } from '$lib/components';
import { setContext } from "svelte"; import { setContext } from 'svelte';
import { writable } from "svelte/store"; import { writable } from 'svelte/store';
import Breadcrumbs from "$lib/components/Breadcrumbs.svelte"; import Breadcrumbs from '$lib/components/Breadcrumbs.svelte';
export let data: LayoutData; export let data: LayoutData;
const { user, posthog } = data; const { user, posthog } = data;
setContext("project", writable(null)); setContext('project', writable(null));
setContext("session", writable(null)); setContext('session', writable(null));
user.subscribe(posthog.identify); user.subscribe(posthog.identify);
</script> </script>
<header <header
data-tauri-drag-region 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 " 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"> <div class="ml-24">
<BackForwardButtons /> <BackForwardButtons />
</div> </div>
<div class="ml-6"><Breadcrumbs /></div> <div class="ml-6"><Breadcrumbs /></div>
<div class="flex-grow" /> <div class="flex-grow" />
<a <a href="/users/" class="flex items-center gap-2 mr-4 font-medium hover:text-zinc-200">
href="/users/" {#if $user}
class="flex items-center gap-2 mr-4 font-medium hover:text-zinc-200" {#if $user.picture}
> <img class="inline-block w-6 h-6 rounded-full" src={$user.picture} alt="Avatar" />
{#if $user} {/if}
{#if $user.picture} <span>{$user.name}</span>
<img {:else}
class="inline-block w-6 h-6 rounded-full" <span>Connect to GitButler Cloud</span>
src={$user.picture} {/if}
alt="Avatar" </a>
/>
{/if}
<span>{$user.name}</span>
{:else}
<span>Connect to GitButler Cloud</span>
{/if}
</a>
</header> </header>
<div class="h-0 min-h-full bg-zinc-800 text-zinc-400"> <div class="h-0 min-h-full bg-zinc-800 text-zinc-400">
<slot /> <slot />
<Toaster /> <Toaster />
</div> </div>

View File

@ -1,43 +1,43 @@
import { readable } from "svelte/store"; import { readable } from 'svelte/store';
import type { LayoutLoad } from "./$types"; import type { LayoutLoad } from './$types';
import { building } from "$app/environment"; import { building } from '$app/environment';
import type { Project } from "$lib/projects"; import type { Project } from '$lib/projects';
import Api from "$lib/api"; import Api from '$lib/api';
import Posthog from "$lib/posthog"; import Posthog from '$lib/posthog';
import * as log from "$lib/log"; import * as log from '$lib/log';
export const ssr = false; export const ssr = false;
export const prerender = true; export const prerender = true;
export const csr = true; export const csr = true;
export const load: LayoutLoad = async ({ fetch }) => { export const load: LayoutLoad = async ({ fetch }) => {
const projects = building const projects = building
? { ? {
...readable<Project[]>([]), ...readable<Project[]>([]),
add: () => { add: () => {
throw new Error("not implemented"); throw new Error('not implemented');
}, },
get: () => { get: () => {
throw new Error("not implemented"); throw new Error('not implemented');
}, }
} }
: await (await import("$lib/projects")).default(); : await (await import('$lib/projects')).default();
const user = building const user = building
? { ? {
...readable<undefined>(undefined), ...readable<undefined>(undefined),
set: () => { set: () => {
throw new Error("not implemented"); throw new Error('not implemented');
}, },
delete: () => { delete: () => {
throw new Error("not implemented"); throw new Error('not implemented');
}, }
} }
: await (await import("$lib/users")).default(); : await (await import('$lib/users')).default();
await log.setup(); await log.setup();
return { return {
projects, projects,
user, user,
api: Api({ fetch }), api: Api({ fetch }),
posthog: Posthog(), posthog: Posthog()
}; };
}; };

View File

@ -1,59 +1,53 @@
<script lang="ts"> <script lang="ts">
import FaFolderOpen from "svelte-icons/fa/FaFolderOpen.svelte"; import FaFolderOpen from 'svelte-icons/fa/FaFolderOpen.svelte';
import { open } from "@tauri-apps/api/dialog"; import { open } from '@tauri-apps/api/dialog';
import type { LayoutData } from "./$types"; import type { LayoutData } from './$types';
export let data: LayoutData; export let data: LayoutData;
const { projects } = data; const { projects } = data;
const onAddLocalRepositoryClick = async () => { const onAddLocalRepositoryClick = async () => {
const selectedPath = await open({ const selectedPath = await open({
directory: true, directory: true,
recursive: true, recursive: true
}); });
if (selectedPath === null) return; if (selectedPath === null) return;
if (Array.isArray(selectedPath) && selectedPath.length !== 1) return; if (Array.isArray(selectedPath) && selectedPath.length !== 1) return;
const projectPath = Array.isArray(selectedPath) const projectPath = Array.isArray(selectedPath) ? selectedPath[0] : selectedPath;
? selectedPath[0]
: selectedPath;
const projectExists = $projects.some((p) => p.path === projectPath); const projectExists = $projects.some((p) => p.path === projectPath);
if (projectExists) return; if (projectExists) return;
await projects.add({ path: projectPath }); await projects.add({ path: projectPath });
}; };
</script> </script>
<div class="select-none p-16"> <div class="select-none p-16">
<div class="mb-6"> <div class="mb-6">
<h1 class="text-4xl text-zinc-200 mb-2">GitButler</h1> <h1 class="text-4xl text-zinc-200 mb-2">GitButler</h1>
<h2 class="text-2xl">Your Personal VCS Assistant</h2> <h2 class="text-2xl">Your Personal VCS Assistant</h2>
</div> </div>
<div class="flex flex-row"> <div class="flex flex-row">
<div class="w-1/2"> <div class="w-1/2">
<div class="text-xl text-zinc-200 mb-1">Start</div> <div class="text-xl text-zinc-200 mb-1">Start</div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<div class="w-4 h-4"><FaFolderOpen /></div> <div class="w-4 h-4"><FaFolderOpen /></div>
<button <button on:click={onAddLocalRepositoryClick} class="hover:text-zinc-200"
on:click={onAddLocalRepositoryClick} >Add a local repository...</button
class="hover:text-zinc-200">Add a local repository...</button >
> </div>
</div> </div>
</div> <div class="w-1/2">
<div class="w-1/2"> <div class="text-xl text-zinc-200 mb-1">Recent</div>
<div class="text-xl text-zinc-200 mb-1">Recent</div> <div class="flex flex-col space-y-1">
<div class="flex flex-col space-y-1"> {#each $projects as project}
{#each $projects as project} <div class="space-x-2">
<div class="space-x-2"> <a class="hover:text-zinc-200" href="/projects/{project.id}/">{project.title}</a>
<a <span class="text-zinc-500">{project.path}</span>
class="hover:text-zinc-200" </div>
href="/projects/{project.id}/">{project.title}</a {/each}
> </div>
<span class="text-zinc-500">{project.path}</span> </div>
</div> </div>
{/each}
</div>
</div>
</div>
</div> </div>

View File

@ -1,128 +1,113 @@
<script lang="ts"> <script lang="ts">
import type { LayoutData } from "./$types"; import type { LayoutData } from './$types';
import { getContext } from "svelte"; import { getContext } from 'svelte';
import type { Writable } from "svelte/store"; import type { Writable } from 'svelte/store';
import type { Project } from "$lib/projects"; import type { Project } from '$lib/projects';
import { onDestroy } from "svelte"; import { onDestroy } from 'svelte';
import { page } from "$app/stores"; import { page } from '$app/stores';
export let data: LayoutData; export let data: LayoutData;
$: project = data.project; $: project = data.project;
$: sessions = data.sessions; $: sessions = data.sessions;
$: lastSessionId = $sessions[$sessions.length - 1]?.id; $: lastSessionId = $sessions[$sessions.length - 1]?.id;
function projectUrl(project: Project) { function projectUrl(project: Project) {
const gitUrl = project.api?.git_url; const gitUrl = project.api?.git_url;
// get host from git url // get host from git url
const url = new URL(gitUrl); const url = new URL(gitUrl);
const host = url.origin; const host = url.origin;
const projectId = gitUrl.split("/").pop(); const projectId = gitUrl.split('/').pop();
return `${host}/projects/${projectId}`; return `${host}/projects/${projectId}`;
} }
const contextProjectStore: Writable<Project | null | undefined> = const contextProjectStore: Writable<Project | null | undefined> = getContext('project');
getContext("project"); $: contextProjectStore.set($project);
$: contextProjectStore.set($project); onDestroy(() => {
onDestroy(() => { contextProjectStore.set(null);
contextProjectStore.set(null); });
});
$: selection = $page?.route?.id?.split("/")?.[3]; $: selection = $page?.route?.id?.split('/')?.[3];
</script> </script>
<nav <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 <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" 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 <a
class=" class="
{selection === 'week' ? 'bg-zinc-600/70 text-zinc-100' : ''} {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" rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100"
href="/projects/{$project?.id}/week">Week</a href="/projects/{$project?.id}/week">Week</a
> >
<a <a
href="/projects/{$project?.id}/day" href="/projects/{$project?.id}/day"
class=" class="
{selection === 'day' ? 'bg-zinc-600/70 text-zinc-100' : ''} {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" rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100">Day</a
>Day</a >
> <a
<a href="/projects/{$project?.id}/sessions/{lastSessionId}"
href="/projects/{$project?.id}/sessions/{lastSessionId}" class="
class="
{selection === 'sessions' ? 'bg-zinc-600/70 text-zinc-100' : ''} {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" rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100"
title="go to current session">Session</a title="go to current session">Session</a
> >
</div> </div>
<ul> <ul>
<li> <li>
<a <a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
href="/projects/{$project?.id}/settings" <svg
class="text-zinc-400 hover:text-zinc-300" xmlns="http://www.w3.org/2000/svg"
> fill="none"
<svg viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" stroke-width="1.5"
fill="none" stroke="currentColor"
viewBox="0 0 24 24" class="w-6 h-6"
stroke-width="1.5" >
stroke="currentColor" <path
class="w-6 h-6" stroke-linecap="round"
> stroke-linejoin="round"
<path 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"
stroke-linecap="round" />
stroke-linejoin="round" <path
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" stroke-linecap="round"
/> stroke-linejoin="round"
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
stroke-linecap="round" />
stroke-linejoin="round" </svg>
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" </a>
/> </li>
</svg> </ul>
</a>
</li>
</ul>
</nav> </nav>
<slot /> <slot />
<div class="absolute bottom-0 left-0 w-full"> <div class="absolute bottom-0 left-0 w-full">
<div <div
class="flex items-center flex-shrink-0 h-6 border-t select-none border-zinc-700 bg-zinc-900 " class="flex items-center flex-shrink-0 h-6 border-t select-none border-zinc-700 bg-zinc-900 "
> >
<div <div class="flex flex-row mx-4 items-center space-x-2 justify-between w-full">
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">
{#if $project?.api?.sync} <div class="flex flex-row items-center space-x-2 ">
<a <div class="w-2 h-2 bg-green-700 rounded-full" />
href="/projects/{$project?.id}/settings" <div class="text-zinc-200">Syncing</div>
class="text-zinc-400 hover:text-zinc-300" </div>
> </a>
<div class="flex flex-row items-center space-x-2 "> <a target="_blank" rel="noreferrer" href={projectUrl($project)}>Open in GitButler Cloud</a>
<div class="w-2 h-2 bg-green-700 rounded-full" /> {:else}
<div class="text-zinc-200">Syncing</div> <a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
</div> <div class="flex flex-row items-center space-x-2 ">
</a> <div class="w-2 h-2 bg-red-700 rounded-full" />
<a target="_blank" rel="noreferrer" href={projectUrl($project)} <div class="text-zinc-200">Offline</div>
>Open in GitButler Cloud</a </div>
> </a>
{:else} {/if}
<a </div>
href="/projects/{$project?.id}/settings" </div>
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> </div>

View File

@ -1,24 +1,20 @@
import { readable, derived } from "svelte/store"; import { readable, derived } from 'svelte/store';
import type { LayoutLoad } from "./$types"; import type { LayoutLoad } from './$types';
import { building } from "$app/environment"; import { building } from '$app/environment';
import type { Session } from "$lib/sessions"; import type { Session } from '$lib/sessions';
export const prerender = false; export const prerender = false;
export const load: LayoutLoad = async ({ parent, params }) => { export const load: LayoutLoad = async ({ parent, params }) => {
const { projects } = await parent(); const { projects } = await parent();
const sessions = building const sessions = building
? readable<Session[]>([]) ? readable<Session[]>([])
: await ( : await (await import('$lib/sessions')).default({ projectId: params.projectId });
await import("$lib/sessions") const orderedSessions = derived(sessions, (sessions) => {
).default({ projectId: params.projectId }); return sessions.slice().sort((a, b) => a.meta.startTimestampMs - b.meta.startTimestampMs);
const orderedSessions = derived(sessions, (sessions) => { });
return sessions return {
.slice() project: projects.get(params.projectId),
.sort((a, b) => a.meta.startTimestampMs - b.meta.startTimestampMs); sessions: orderedSessions
}); };
return {
project: projects.get(params.projectId),
sessions: orderedSessions,
};
}; };

View File

@ -1,20 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from "./$types"; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
const { project } = data; const { project } = data;
</script> </script>
<div class="flex flex-col mt-12"> <div class="flex flex-col mt-12">
<h1 class="text-zinc-200 text-2xl flex justify-center"> <h1 class="text-zinc-200 text-2xl flex justify-center">
Overview of {$project?.title} Overview of {$project?.title}
</h1> </h1>
<div class="flex justify-center space-x-2"> <div class="flex justify-center space-x-2">
<a class="hover:text-zinc-200" href="/projects/{$project?.id}/week" <a class="hover:text-zinc-200" href="/projects/{$project?.id}/week">Week</a>
>Week</a <a href="/projects/{$project?.id}/day" class="hover:text-zinc-200">Day</a>
> </div>
<a href="/projects/{$project?.id}/day" class="hover:text-zinc-200"
>Day</a
>
</div>
</div> </div>

View File

@ -1,96 +1,85 @@
<script lang="ts"> <script lang="ts">
import MdKeyboardArrowLeft from "svelte-icons/md/MdKeyboardArrowLeft.svelte"; import MdKeyboardArrowLeft from 'svelte-icons/md/MdKeyboardArrowLeft.svelte';
import MdKeyboardArrowRight from "svelte-icons/md/MdKeyboardArrowRight.svelte"; import MdKeyboardArrowRight from 'svelte-icons/md/MdKeyboardArrowRight.svelte';
import { TimelineDaySession } from "$lib/components/timeline"; import { TimelineDaySession } from '$lib/components/timeline';
import type { PageData } from "./$types"; import type { PageData } from './$types';
import type { Session } from "$lib/sessions"; import type { Session } from '$lib/sessions';
import { derived } from "svelte/store"; import { derived } from 'svelte/store';
export let data: PageData; export let data: PageData;
const { project, sessions } = data; const { project, sessions } = data;
let date = new Date(); let date = new Date();
$: canNavigateForwad = $: canNavigateForwad = new Date(date.getTime() + 24 * 60 * 60 * 1000) < new Date();
new Date(date.getTime() + 24 * 60 * 60 * 1000) < new Date();
const formatDate = (date: Date) => { const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("default", { return new Intl.DateTimeFormat('default', {
weekday: "short", weekday: 'short',
day: "numeric", day: 'numeric',
month: "short", month: 'short'
}).format(date); }).format(date);
}; };
const sessionDisplayWidth = (session: Session) => { const sessionDisplayWidth = (session: Session) => {
let sessionDurationMinutes = let sessionDurationMinutes =
(session.meta.lastTimestampMs - session.meta.startTimestampMs) / 60; (session.meta.lastTimestampMs - session.meta.startTimestampMs) / 60;
if (sessionDurationMinutes <= 10) { if (sessionDurationMinutes <= 10) {
return "w-40 min-w-40"; return 'w-40 min-w-40';
} else { } else {
return "w-60 min-w-60"; return 'w-60 min-w-60';
} }
}; };
$: sessionsInDay = derived([sessions], ([sessions]) => { $: sessionsInDay = derived([sessions], ([sessions]) => {
const start = new Date( const start = new Date(date.getFullYear(), date.getMonth(), date.getDate());
date.getFullYear(), const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
date.getMonth(), return sessions.filter((session) => {
date.getDate() return (
); start <= new Date(session.meta.startTimestampMs) &&
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000); new Date(session.meta.startTimestampMs) <= end
return sessions.filter((session) => { );
return ( });
start <= new Date(session.meta.startTimestampMs) && });
new Date(session.meta.startTimestampMs) <= end
);
});
});
</script> </script>
<div class="flex flex-col h-full select-none text-zinc-400"> <div class="flex flex-col h-full select-none text-zinc-400">
<header <header class="flex items-center justify-between flex-none px-8 py-1.5 border-b border-zinc-700">
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
<div class="flex items-center justify-start w-64"> class="-ml-2 w-8 h-8 hover:text-zinc-100"
<button on:click={() => (date = new Date(date.getTime() - 24 * 60 * 60 * 1000))}
class="-ml-2 w-8 h-8 hover:text-zinc-100" >
on:click={() => <MdKeyboardArrowLeft />
(date = new Date(date.getTime() - 24 * 60 * 60 * 1000))} </button>
> <div class="flex-grow text-center">
<MdKeyboardArrowLeft /> {formatDate(date)}
</button> </div>
<div class="flex-grow text-center"> <button
{formatDate(date)} class="-mr-2 w-8 h-8 hover:text-zinc-100 disabled:text-zinc-700"
</div> disabled={!canNavigateForwad}
<button on:click={() => {
class="-mr-2 w-8 h-8 hover:text-zinc-100 disabled:text-zinc-700" if (canNavigateForwad) {
disabled={!canNavigateForwad} date = new Date(date.getTime() + 24 * 60 * 60 * 1000);
on:click={() => { }
if (canNavigateForwad) { }}
date = new Date(date.getTime() + 24 * 60 * 60 * 1000); >
} <MdKeyboardArrowRight />
}} </button>
> </div>
<MdKeyboardArrowRight /> </header>
</button>
</div>
</header>
<div class="w-full h-full overflow-scroll mx-2 flex"> <div class="w-full h-full overflow-scroll mx-2 flex">
{#if $project} {#if $project}
<div class="flex-grow items-center justify-center mt-4"> <div class="flex-grow items-center justify-center mt-4">
<div class="justify-center flex flex-row space-x-2 pt-2"> <div class="justify-center flex flex-row space-x-2 pt-2">
{#each $sessionsInDay as session} {#each $sessionsInDay as session}
<div class={sessionDisplayWidth(session)}> <div class={sessionDisplayWidth(session)}>
<TimelineDaySession <TimelineDaySession projectId={$project.id} {session} />
projectId={$project.id} </div>
{session} {/each}
/> </div>
</div> </div>
{/each} {:else}
</div> <p>Project not found</p>
</div> {/if}
{:else} </div>
<p>Project not found</p>
{/if}
</div>
</div> </div>

View File

@ -1,297 +1,267 @@
<script lang="ts"> <script lang="ts">
import MdKeyboardArrowLeft from "svelte-icons/md/MdKeyboardArrowLeft.svelte"; import MdKeyboardArrowLeft from 'svelte-icons/md/MdKeyboardArrowLeft.svelte';
import MdKeyboardArrowRight from "svelte-icons/md/MdKeyboardArrowRight.svelte"; import MdKeyboardArrowRight from 'svelte-icons/md/MdKeyboardArrowRight.svelte';
import type { PageData } from "./$types"; import type { PageData } from './$types';
import { add, format, differenceInSeconds, addSeconds } from "date-fns"; import { add, format, differenceInSeconds, addSeconds } from 'date-fns';
import { page } from "$app/stores"; import { page } from '$app/stores';
import { onMount } from "svelte"; import { onMount } from 'svelte';
import { derived } from "svelte/store"; import { derived } from 'svelte/store';
import { Operation } from "$lib/deltas"; import { Operation } from '$lib/deltas';
import { Slider } from "fluent-svelte"; import { Slider } from 'fluent-svelte';
import { CodeViewer } from "$lib/components"; import { CodeViewer } from '$lib/components';
import "fluent-svelte/theme.css"; import 'fluent-svelte/theme.css';
export let data: PageData; export let data: PageData;
$: session = data.session; $: session = data.session;
$: previousSesssion = data.previousSesssion; $: previousSesssion = data.previousSesssion;
$: nextSession = data.nextSession; $: nextSession = data.nextSession;
$: deltas = data.deltas; $: deltas = data.deltas;
let time = new Date(); let time = new Date();
$: start = new Date($session.meta.startTimestampMs); $: start = new Date($session.meta.startTimestampMs);
$: end = $session.hash $: 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.
? addSeconds(new Date($session.meta.lastTimestampMs), 10) // Also, if the session is current, the end time moves.
: 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(() => { onMount(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
time = new Date(); time = new Date();
}, 10000); }, 10000);
return () => { return () => {
clearInterval(interval); clearInterval(interval);
}; };
}); });
$: midpoint = add(start, { $: midpoint = add(start, {
seconds: differenceInSeconds(end, start) * 0.5, seconds: differenceInSeconds(end, start) * 0.5
}); });
$: quarter = add(start, { $: quarter = add(start, {
seconds: differenceInSeconds(end, start) * 0.25, seconds: differenceInSeconds(end, start) * 0.25
}); });
$: threequarters = add(start, { $: threequarters = add(start, {
seconds: differenceInSeconds(end, start) * 0.75, seconds: differenceInSeconds(end, start) * 0.75
}); });
const timeStampToCol = (deltaTimestamp: Date) => { const timeStampToCol = (deltaTimestamp: Date) => {
if (deltaTimestamp < start || deltaTimestamp > end) { if (deltaTimestamp < start || deltaTimestamp > end) {
console.error( console.error(
`Delta timestamp out of session range. Delta timestamp: ${deltaTimestamp}, Session start: ${start}, Session end: ${end}` `Delta timestamp out of session range. Delta timestamp: ${deltaTimestamp}, Session start: ${start}, Session end: ${end}`
); );
} }
// there are 88 columns // there are 88 columns
// start is column 17 // start is column 17
const totalDiff = differenceInSeconds(end, start); const totalDiff = differenceInSeconds(end, start);
const eventDiff = differenceInSeconds(deltaTimestamp, start); const eventDiff = differenceInSeconds(deltaTimestamp, start);
const rat = eventDiff / totalDiff; const rat = eventDiff / totalDiff;
const col = Math.floor(rat * 63 + 17); const col = Math.floor(rat * 63 + 17);
return col; return col;
}; };
const colToTimestamp = (col: number) => { const colToTimestamp = (col: number) => {
const totalDiff = differenceInSeconds(end, start); const totalDiff = differenceInSeconds(end, start);
const colDiff = col - 17; const colDiff = col - 17;
const rat = colDiff / 63; const rat = colDiff / 63;
const eventDiff = totalDiff * rat; const eventDiff = totalDiff * rat;
const timestamp = addSeconds(start, eventDiff); const timestamp = addSeconds(start, eventDiff);
return timestamp; return timestamp;
}; };
$: tickSizeMs = Math.floor((end.getTime() - start.getTime()) / 63); // how many ms each column represents $: tickSizeMs = Math.floor((end.getTime() - start.getTime()) / 63); // how many ms each column represents
let selectedFileIdx = 0; let selectedFileIdx = 0;
let value = 0; let value = 0;
$: doc = derived([data.deltas], ([allDeltas]) => { $: doc = derived([data.deltas], ([allDeltas]) => {
const filePath = Object.keys(allDeltas)[selectedFileIdx]; const filePath = Object.keys(allDeltas)[selectedFileIdx];
const deltas = allDeltas[filePath]; const deltas = allDeltas[filePath];
let text = data.files[filePath] || ""; let text = data.files[filePath] || '';
if (!deltas) return text; if (!deltas) return text;
const sliderValueTimestampMs = const sliderValueTimestampMs = colToTimestamp(value).getTime() + tickSizeMs; // Include the tick size so that the slider value is always in the future
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
// Filter operations based on the current slider value const operations = deltas
const operations = deltas .filter(
.filter( (delta) =>
(delta) => delta.timestampMs >= start.getTime() && delta.timestampMs <= sliderValueTimestampMs
delta.timestampMs >= start.getTime() && )
delta.timestampMs <= sliderValueTimestampMs .sort((a, b) => a.timestampMs - b.timestampMs)
) .flatMap((delta) => delta.operations);
.sort((a, b) => a.timestampMs - b.timestampMs)
.flatMap((delta) => delta.operations);
operations.forEach((operation) => { operations.forEach((operation) => {
if (Operation.isInsert(operation)) { if (Operation.isInsert(operation)) {
text = text =
text.slice(0, operation.insert[0]) + text.slice(0, operation.insert[0]) +
operation.insert[1] + operation.insert[1] +
text.slice(operation.insert[0]); text.slice(operation.insert[0]);
} else if (Operation.isDelete(operation)) { } else if (Operation.isDelete(operation)) {
text = text =
text.slice(0, operation.delete[0]) + text.slice(0, operation.delete[0]) +
text.slice(operation.delete[0] + operation.delete[1]); text.slice(operation.delete[0] + operation.delete[1]);
} }
}); });
return text; return text;
}); });
const formatDate = (date: Date) => { const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("default", { return new Intl.DateTimeFormat('default', {
weekday: "short", weekday: 'short',
day: "numeric", day: 'numeric',
hour: "numeric", hour: 'numeric',
minute: "numeric", minute: 'numeric'
}).format(date); }).format(date);
}; };
</script> </script>
<div class="flex flex-col h-full text-zinc-400 overflow-hidden"> <div class="flex flex-col h-full text-zinc-400 overflow-hidden">
<header <header class="flex items-center justify-between flex-none px-8 py-1.5 border-b border-zinc-700">
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
<div class="flex items-center justify-start w-64"> href="/projects/{$page.params.projectId}/sessions/{$previousSesssion?.id}"
<a class="-ml-2 w-8 h-8 hover:text-zinc-100 {$previousSesssion
href="/projects/{$page.params ? ''
.projectId}/sessions/{$previousSesssion?.id}" : 'opacity-50 pointer-events-none cursor-not-allowed'}"
class="-ml-2 w-8 h-8 hover:text-zinc-100 {$previousSesssion >
? '' <MdKeyboardArrowLeft />
: 'opacity-50 pointer-events-none cursor-not-allowed'}" </a>
> <div class="flex-grow text-center cursor-default grid grid-cols-7">
<MdKeyboardArrowLeft /> <span class="col-span-3">{formatDate(start)}</span>
</a> <span>&mdash;</span>
<div class="flex-grow text-center cursor-default grid grid-cols-7"> <span class="col-span-3">{formatDate(end)}</span>
<span class="col-span-3">{formatDate(start)}</span> </div>
<span>&mdash;</span> <a
<span class="col-span-3">{formatDate(end)}</span> href="/projects/{$page.params.projectId}/sessions/{$nextSession?.id}"
</div> class="-mr-2 w-8 h-8 hover:text-zinc-100 {$nextSession
<a ? ''
href="/projects/{$page.params : 'text-zinc-700 pointer-events-none cursor-not-allowed'}"
.projectId}/sessions/{$nextSession?.id}" >
class="-mr-2 w-8 h-8 hover:text-zinc-100 {$nextSession <MdKeyboardArrowRight />
? '' </a>
: 'text-zinc-700 pointer-events-none cursor-not-allowed'}" </div>
> </header>
<MdKeyboardArrowRight />
</a>
</div>
</header>
<!-- main part --> <!-- main part -->
<div <div
class="flex flex-col flex-none max-w-full select-none border-b border-zinc-700 h-full overflow-auto" 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"> <div class="flex flex-col flex-none max-w-full mb-40">
<!-- sticky header --> <!-- sticky header -->
<div <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" 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 class="grid-cols-11 -mr-px border-zinc-700 grid">
<div /> <div />
<div <div class="col-span-2 flex items-center justify-center py-2">
class="col-span-2 flex items-center justify-center py-2" <span>{format(start, 'hh:mm')}</span>
> </div>
<span>{format(start, "hh:mm")}</span> <div class="col-span-2 flex items-center justify-center py-2">
</div> <span>{format(quarter, 'hh:mm')}</span>
<div </div>
class="col-span-2 flex items-center justify-center py-2" <div class="col-span-2 flex items-center justify-center py-2">
> <span>{format(midpoint, 'hh:mm')}</span>
<span>{format(quarter, "hh:mm")}</span> </div>
</div> <div class="col-span-2 flex items-center justify-center py-2">
<div <span>{format(threequarters, 'hh:mm')}</span>
class="col-span-2 flex items-center justify-center py-2" </div>
> <div class="col-span-2 flex items-center justify-center py-2">
<span>{format(midpoint, "hh:mm")}</span> <span>{format(end, 'hh:mm')}</span>
</div> </div>
<div </div>
class="col-span-2 flex items-center justify-center py-2" <!-- needle -->
> <div class="grid grid-cols-11">
<span>{format(threequarters, "hh:mm")}</span> <div class="col-span-2 flex items-center justify-center" />
</div> <div class="-mx-1 col-span-8 flex items-center justify-center">
<div <Slider min={17} max={80} step={1} bind:value>
class="col-span-2 flex items-center justify-center py-2" <svelte:fragment slot="tooltip" let:value>
> {format(colToTimestamp(value), 'hh:mm')}
<span>{format(end, "hh:mm")}</span> </svelte:fragment>
</div> </Slider>
</div> </div>
<!-- needle --> <div class="col-span-1 flex items-center justify-center" />
<div class="grid grid-cols-11"> </div>
<div class="col-span-2 flex items-center justify-center" /> </div>
<div <div class="flex flex-auto mb-1">
class="-mx-1 col-span-8 flex items-center justify-center" <div class="grid flex-auto grid-cols-1 grid-rows-1">
> <!-- file names list -->
<Slider min={17} max={80} step={1} bind:value> <div
<svelte:fragment slot="tooltip" let:value> class="bg-col-start-1 col-end-2 row-start-1 grid divide-y divide-zinc-700/20"
{format(colToTimestamp(value), "hh:mm")} style="grid-template-rows: repeat({Object.keys($deltas).length}, minmax(2rem, 1fr));"
</svelte:fragment> >
</Slider> <!-- <div class="row-end-1 h-7" /> -->
</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} {#each Object.keys($deltas) as filePath, i}
<div <div class="flex {i == selectedFileIdx ? 'bg-zinc-500/70' : ''}">
class="flex {i == selectedFileIdx <button
? 'bg-zinc-500/70' class="z-20 flex justify-end items-center overflow-hidden sticky left-0 w-1/6 leading-5
: ''}"
>
<button
class="z-20 flex justify-end items-center overflow-hidden sticky left-0 w-1/6 leading-5
{selectedFileIdx == i {selectedFileIdx == i
? 'text-zinc-200 cursor-default' ? 'text-zinc-200 cursor-default'
: 'text-zinc-400 hover:text-zinc-200 cursor-pointer'}" : 'text-zinc-400 hover:text-zinc-200 cursor-pointer'}"
on:click={() => (selectedFileIdx = i)} on:click={() => (selectedFileIdx = i)}
title={filePath} title={filePath}
> >
{filePath.split("/").pop()} {filePath.split('/').pop()}
</button> </button>
</div> </div>
{/each} {/each}
</div> </div>
<!-- col selection --> <!-- col selection -->
<div <div
class="col-start-1 col-end-2 row-start-1 grid" class="col-start-1 col-end-2 row-start-1 grid"
style="grid-template-columns: repeat(88, minmax(0, 1fr));" style="grid-template-columns: repeat(88, minmax(0, 1fr));"
> >
<div <div class="bg-sky-400/60 " style=" grid-column: {value};" />
class="bg-sky-400/60 " </div>
style=" grid-column: {value};" <!-- time vertical lines -->
/> <div
</div> class="col-start-1 col-end-2 row-start-1 grid-rows-1 divide-x divide-zinc-700/50 grid grid-cols-11"
<!-- time vertical lines --> >
<div <div class="col-span-2 row-span-full" />
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 class="col-span-2 row-span-full" /> <div class="col-span-2 row-span-full" />
<div class="col-span-2 row-span-full" /> </div>
<div class="col-span-2 row-span-full" />
<div class="col-span-2 row-span-full" />
</div>
<!-- actual entries --> <!-- actual entries -->
<ol <ol
class="col-start-1 col-end-2 row-start-1 grid" class="col-start-1 col-end-2 row-start-1 grid"
style=" style="
grid-template-columns: repeat(88, minmax(0, 1fr)); grid-template-columns: repeat(88, minmax(0, 1fr));
grid-template-rows: repeat({Object.keys($deltas) grid-template-rows: repeat({Object.keys($deltas)
.length}, minmax(0px, 1fr)) auto;" .length}, minmax(0px, 1fr)) auto;"
> >
{#each Object.entries($deltas) as [filePath, fileDeltas], idx} {#each Object.entries($deltas) as [filePath, fileDeltas], idx}
{#each fileDeltas as delta} {#each fileDeltas as delta}
<li <li
class="relative flex items-center bg-zinc-300 hover:bg-zinc-100 rounded m-0.5 cursor-pointer" class="relative flex items-center bg-zinc-300 hover:bg-zinc-100 rounded m-0.5 cursor-pointer"
style=" style="
grid-row: {idx + 1} / span 1; grid-row: {idx + 1} / span 1;
grid-column: {timeStampToCol( grid-column: {timeStampToCol(
new Date(delta.timestampMs) new Date(delta.timestampMs)
)} / span 1;" )} / span 1;"
> >
<button <button
class="z-20 h-full flex flex-col w-full items-center justify-center" class="z-20 h-full flex flex-col w-full items-center justify-center"
on:click={() => { on:click={() => {
value = timeStampToCol( value = timeStampToCol(new Date(delta.timestampMs));
new Date(delta.timestampMs) selectedFileIdx = idx;
); }}
selectedFileIdx = idx; />
}} </li>
/> {/each}
</li> {/each}
{/each} </ol>
{/each} </div>
</ol> </div>
</div> <div class="grid grid-cols-11 mt-6">
</div> <div class="col-span-2" />
<div class="grid grid-cols-11 mt-6"> <div class="col-span-8 p-1 bg-zinc-500/70 rounded select-text">
<div class="col-span-2" /> {#if $doc}
<div class="col-span-8 p-1 bg-zinc-500/70 rounded select-text"> <CodeViewer value={$doc} />
{#if $doc} {/if}
<CodeViewer value={$doc} /> </div>
{/if} <div class="" />
</div> </div>
<div class="" /> </div>
</div> </div>
</div>
</div>
</div> </div>

View File

@ -1,51 +1,45 @@
import type { PageLoad } from "./$types"; import type { PageLoad } from './$types';
import { derived, readable } from "svelte/store"; import { derived, readable } from 'svelte/store';
import { building } from "$app/environment"; import { building } from '$app/environment';
import type { Delta } from "$lib/deltas"; import type { Delta } from '$lib/deltas';
export const prerender = false; export const prerender = false;
export const load: PageLoad = async ({ parent, params }) => { export const load: PageLoad = async ({ parent, params }) => {
const { sessions } = await parent(); const { sessions } = await parent();
const deltas = building const deltas = building
? readable({} as Record<string, Delta[]>) ? readable({} as Record<string, Delta[]>)
: (await import("$lib/deltas")).default({ : (await import('$lib/deltas')).default({
projectId: params.projectId, projectId: params.projectId,
sessionId: params.sessionId, sessionId: params.sessionId
}); });
const files = building const files = building
? ({} as Record<string, string>) ? ({} as Record<string, string>)
: (await import("$lib/sessions")).listFiles({ : (await import('$lib/sessions')).listFiles({
projectId: params.projectId, projectId: params.projectId,
sessionId: params.sessionId, sessionId: params.sessionId
}); });
return { return {
session: derived(sessions, (sessions) => { session: derived(sessions, (sessions) => {
const result = sessions.find( const result = sessions.find((session) => session.id === params.sessionId);
(session) => session.id === params.sessionId return result ? result : sessions[0];
); }),
return result ? result : sessions[0]; previousSesssion: derived(sessions, (sessions) => {
}), const currentSessionIndex = sessions.findIndex((session) => session.id === params.sessionId);
previousSesssion: derived(sessions, (sessions) => { if (currentSessionIndex - 1 < sessions.length) {
const currentSessionIndex = sessions.findIndex( return sessions[currentSessionIndex - 1];
(session) => session.id === params.sessionId } else {
); return undefined;
if (currentSessionIndex - 1 < sessions.length) { }
return sessions[currentSessionIndex - 1]; }),
} else { nextSession: derived(sessions, (sessions) => {
return undefined; const currentSessionIndex = sessions.findIndex((session) => session.id === params.sessionId);
} if (currentSessionIndex + 1 < sessions.length) {
}), return sessions[currentSessionIndex + 1];
nextSession: derived(sessions, (sessions) => { } else {
const currentSessionIndex = sessions.findIndex( return undefined;
(session) => session.id === params.sessionId }
); }),
if (currentSessionIndex + 1 < sessions.length) { files: files,
return sessions[currentSessionIndex + 1]; deltas: deltas
} else { };
return undefined; };
}
}),
files: files,
deltas: deltas,
}
}

View File

@ -1,148 +1,138 @@
<script lang="ts"> <script lang="ts">
import { derived } from "svelte/store"; import { derived } from 'svelte/store';
import { Login } from "$lib/components"; import { Login } from '$lib/components';
import type { PageData } from "./$types"; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
const { project, user, api } = data; const { project, user, api } = data;
function repo_id(url: string) { function repo_id(url: string) {
const hurl = new URL(url); const hurl = new URL(url);
const path = hurl.pathname.split("/"); const path = hurl.pathname.split('/');
return path[path.length - 1]; return path[path.length - 1];
} }
function hostname(url: string) { function hostname(url: string) {
const hurl = new URL(url); const hurl = new URL(url);
return hurl.hostname; return hurl.hostname;
} }
const isSyncing = derived(project, (project) => project?.api?.sync); const isSyncing = derived(project, (project) => project?.api?.sync);
const onSyncChange = async (event: Event) => { const onSyncChange = async (event: Event) => {
if ($project === undefined) return; if ($project === undefined) return;
if ($user === undefined) return; if ($user === undefined) return;
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const sync = target.checked; const sync = target.checked;
if (!$project.api) { if (!$project.api) {
const apiProject = await api.projects.create($user.access_token, { const apiProject = await api.projects.create($user.access_token, {
name: $project.title, name: $project.title,
uid: $project.id, uid: $project.id
}); });
await project.update({ api: { ...apiProject, sync } }); await project.update({ api: { ...apiProject, sync } });
} else { } else {
await project.update({ api: { ...$project.api, sync } }); await project.update({ api: { ...$project.api, sync } });
} }
}; };
</script> </script>
<div class="p-4 mx-auto h-full overflow-auto"> <div class="p-4 mx-auto h-full overflow-auto">
<div class="max-w-2xl mx-auto p-4"> <div class="max-w-2xl mx-auto p-4">
<div class="flex flex-col text-zinc-100 space-y-6"> <div class="flex flex-col text-zinc-100 space-y-6">
<div class="space-y-0"> <div class="space-y-0">
<div class="text-lg font-medium">Project Settings</div> <div class="text-lg font-medium">Project Settings</div>
<div class="text-zinc-400"> <div class="text-zinc-400">
Manage your project settings for <strong Manage your project settings for <strong>{$project?.title}</strong>
>{$project?.title}</strong </div>
> </div>
</div> <hr class="border-zinc-600" />
</div> {#if $user}
<hr class="border-zinc-600" /> <div class="space-y-2">
{#if $user} <div class="ml-1">GitButler Cloud</div>
<div class="space-y-2"> <div
<div class="ml-1">GitButler Cloud</div> class="flex flex-row justify-between border border-zinc-600 rounded-lg p-2 items-center"
<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
<div class="flex flex-row space-x-3"> xmlns="http://www.w3.org/2000/svg"
<svg fill="none"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke-width="1.5"
viewBox="0 0 24 24" stroke="white"
stroke-width="1.5" class="w-6 h-6"
stroke="white" >
class="w-6 h-6" <path
> stroke-linecap="round"
<path stroke-linejoin="round"
stroke-linecap="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"
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">
</svg> {#if $project?.api?.git_url}
<div class="flex flex-row"> <div class="flex flex-col">
{#if $project?.api?.git_url} <div class="text-zinc-300">Git Host</div>
<div class="flex flex-col"> <div class="text-zinc-400 font-mono">
<div class="text-zinc-300"> {hostname($project?.api?.git_url)}
Git Host </div>
</div> <div class="text-zinc-300 mt-3">Repository ID</div>
<div class="text-zinc-400 font-mono"> <div class="text-zinc-400 font-mono">
{hostname($project?.api?.git_url)} {repo_id($project?.api?.git_url)}
</div> </div>
<div class="text-zinc-300 mt-3"> </div>
Repository ID {/if}
</div> <div>
<div class="text-zinc-400 font-mono"> <form disabled={$user === undefined}>
{repo_id($project?.api?.git_url)} <input
</div> class="mr-1"
</div> disabled={$user === undefined}
{/if} type="checkbox"
<div> checked={$isSyncing}
<form disabled={$user === undefined}> on:change={onSyncChange}
<input />
class="mr-1" <label for="sync">Send Data to Server</label>
disabled={$user === undefined} </form>
type="checkbox" </div>
checked={$isSyncing} </div>
on:change={onSyncChange} </div>
/> </div>
<label for="sync" </div>
>Send Data to Server</label {:else}
> <div class="space-y-2">
</form> <div class="flex flex-row space-x-2 items-end">
</div> <div class="">GitButler Cloud</div>
</div> <div class="text-zinc-400">backup your work and access advanced features</div>
</div> </div>
</div> <div class="flex flex-row items-center space-x-2">
</div> <Login {user} {api} />
{:else} </div>
<div class="space-y-2"> </div>
<div class="flex flex-row space-x-2 items-end"> {/if}
<div class="">GitButler Cloud</div> <div class="space-y-2">
<div class="text-zinc-400"> <div class="ml-1">Path</div>
backup your work and access advanced features <div class="text-zinc-400 font-mono">
</div> {$project?.path}
</div> </div>
<div class="flex flex-row items-center space-x-2"> </div>
<Login {user} {api} /> <div class="space-y-2">
</div> <div class="ml-1">Project Name</div>
</div> <!-- text box -->
{/if} <input
<div class="space-y-2"> type="text"
<div class="ml-1">Path</div> class="p-2 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
<div class="text-zinc-400 font-mono"> value={$project?.title}
{$project?.path} />
</div> </div>
</div> <div class="space-y-2">
<div class="space-y-2"> <div class="ml-1">Project Description</div>
<div class="ml-1">Project Name</div> <!-- text box -->
<!-- text box --> <textarea
<input rows="3"
type="text" class="p-2 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
class="p-2 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full" value={$project?.api?.description}
value={$project?.title} />
/> </div>
</div> </div>
<div class="space-y-2"> </div>
<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> </div>

View File

@ -1,362 +1,321 @@
<script lang="ts"> <script lang="ts">
import { Week } from "$lib/week"; import { Week } from '$lib/week';
import type { PageData } from "./$types"; import type { PageData } from './$types';
import { WeekBlockEntry } from "$lib/components/week"; import { WeekBlockEntry } from '$lib/components/week';
import MdKeyboardArrowLeft from "svelte-icons/md/MdKeyboardArrowLeft.svelte"; import MdKeyboardArrowLeft from 'svelte-icons/md/MdKeyboardArrowLeft.svelte';
import MdKeyboardArrowRight from "svelte-icons/md/MdKeyboardArrowRight.svelte"; import MdKeyboardArrowRight from 'svelte-icons/md/MdKeyboardArrowRight.svelte';
import { derived } from "svelte/store"; import { derived } from 'svelte/store';
export let data: PageData; export let data: PageData;
const { project, sessions } = data; const { project, sessions } = data;
let week = Week.from(new Date()); let week = Week.from(new Date());
$: canNavigateForwad = week.end.getTime() < new Date().getTime(); $: canNavigateForwad = week.end.getTime() < new Date().getTime();
const formatDate = (date: Date) => { const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("default", { return new Intl.DateTimeFormat('default', {
weekday: "short", weekday: 'short',
day: "numeric", day: 'numeric',
month: "short", month: 'short'
}).format(date); }).format(date);
}; };
$: sessionsInWeek = derived([sessions], ([sessions]) => { $: sessionsInWeek = derived([sessions], ([sessions]) => {
return sessions.filter((session) => { return sessions.filter((session) => {
return ( return (
week.start <= new Date(session.meta.startTimestampMs) && week.start <= new Date(session.meta.startTimestampMs) &&
new Date(session.meta.startTimestampMs) <= week.end new Date(session.meta.startTimestampMs) <= week.end
); );
}); });
}); });
</script> </script>
<div class="flex flex-col h-full select-none text-zinc-400"> <div class="flex flex-col h-full select-none text-zinc-400">
<header <header class="flex items-center justify-between flex-none px-8 py-1.5 border-b border-zinc-700">
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
<div class="flex items-center justify-start w-64"> class="-ml-2 w-8 h-8 hover:text-zinc-100"
<button on:click={() => (week = Week.previous(week))}
class="-ml-2 w-8 h-8 hover:text-zinc-100" >
on:click={() => (week = Week.previous(week))} <MdKeyboardArrowLeft />
> </button>
<MdKeyboardArrowLeft /> <div class="flex-grow text-center cursor-default grid grid-cols-7">
</button> <span class="col-span-3">{formatDate(Week.nThDay(week, 0))}</span>
<div class="flex-grow text-center cursor-default grid grid-cols-7"> <span>&mdash;</span>
<span class="col-span-3" <span class="col-span-3">{formatDate(Week.nThDay(week, 6))}</span>
>{formatDate(Week.nThDay(week, 0))}</span </div>
> <button
<span>&mdash;</span> class="-mr-2 w-8 h-8 hover:text-zinc-100 disabled:text-zinc-700"
<span class="col-span-3" disabled={!canNavigateForwad}
>{formatDate(Week.nThDay(week, 6))}</span on:click={() => {
> if (canNavigateForwad) {
</div> week = Week.next(week);
<button }
class="-mr-2 w-8 h-8 hover:text-zinc-100 disabled:text-zinc-700" }}
disabled={!canNavigateForwad} >
on:click={() => { <MdKeyboardArrowRight />
if (canNavigateForwad) { </button>
week = Week.next(week); </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">
<MdKeyboardArrowRight /> <!-- sticky top -->
</button> <div
</div> class="overflow-hidden sticky top-0 z-30 bg-zinc-800 border-b border-zinc-700 flex-none px-8"
</header> >
<div class="isolate flex flex-col flex-auto overflow-auto"> <div class="grid-cols-8 divide-x divide-zinc-700 grid">
<div class="h-4/5 overflow-auto flex flex-col flex-none max-w-full border-b border-zinc-700"> <div class="py-4" />
<!-- sticky top --> <div class="flex items-center justify-center">
<div <span
class="overflow-hidden sticky top-0 z-30 bg-zinc-800 border-b border-zinc-700 flex-none px-8" >Mon <span class="items-center justify-center font-semibold"
> >{Week.nThDay(week, 0).getDate()}</span
<div class="grid-cols-8 divide-x divide-zinc-700 grid"> ></span
<div class="py-4" /> >
<div class="flex items-center justify-center"> </div>
<span <div class="flex items-center justify-center">
>Mon <span <span
class="items-center justify-center font-semibold" >Tue <span class="items-center justify-center font-semibold"
>{Week.nThDay(week, 0).getDate()}</span >{Week.nThDay(week, 1).getDate()}</span
></span ></span
> >
</div> </div>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span <span
>Tue <span >Wed <span class="items-center justify-center font-semibold"
class="items-center justify-center font-semibold" >{Week.nThDay(week, 2).getDate()}</span
>{Week.nThDay(week, 1).getDate()}</span ></span
></span >
> </div>
</div> <div class="flex items-center justify-center">
<div class="flex items-center justify-center"> <span
<span >Thu <span class="items-center justify-center font-semibold"
>Wed <span >{Week.nThDay(week, 3).getDate()}</span
class="items-center justify-center font-semibold" ></span
>{Week.nThDay(week, 2).getDate()}</span >
></span </div>
> <div class="flex items-center justify-center">
</div> <span
<div class="flex items-center justify-center"> >Fri <span class="items-center justify-center font-semibold"
<span >{Week.nThDay(week, 4).getDate()}</span
>Thu <span ></span
class="items-center justify-center font-semibold" >
>{Week.nThDay(week, 3).getDate()}</span </div>
></span <div class="flex items-center justify-center">
> <span
</div> >Sat <span class="items-center justify-center font-semibold"
<div class="flex items-center justify-center"> >{Week.nThDay(week, 5).getDate()}</span
<span ></span
>Fri <span >
class="items-center justify-center font-semibold" </div>
>{Week.nThDay(week, 4).getDate()}</span <div class="flex items-center justify-center">
></span <span
> >Sun <span class="items-center justify-center font-semibold"
</div> >{Week.nThDay(week, 6).getDate()}</span
<div class="flex items-center justify-center"> ></span
<span >
>Sat <span </div>
class="items-center justify-center font-semibold" </div>
>{Week.nThDay(week, 5).getDate()}</span </div>
></span <div class="flex flex-auto ">
> <div class="grid flex-auto grid-cols-1 grid-rows-1">
</div> <!-- hours y lines-->
<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 <div
class="text-zinc-500 col-start-1 col-end-2 row-start-1 grid-rows-1 grid grid-cols-8 px-8" class="text-zinc-500 col-start-1 col-end-2 row-start-1 grid-rows-1 grid grid-cols-8 px-8"
> >
<div <div
class="col-start-1 col-end-2 row-start-1 grid justify-end" class="col-start-1 col-end-2 row-start-1 grid justify-end"
style="grid-template-rows: repeat(24, minmax(1.5rem, 1fr));" style="grid-template-rows: repeat(24, minmax(1.5rem, 1fr));"
> >
<div class="row-end-1 h-7" /> <div class="row-end-1 h-7" />
<div> <div>
<div <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500" 12 AM
> </div>
12 AM </div>
</div> <div />
</div> <div>
<div /> <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
<div> 2 AM
<div </div>
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500" </div>
> <div />
2 AM <div>
</div> <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
</div> 4 AM
<div /> </div>
<div> </div>
<div <div />
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500" <div>
> <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
4 AM 6 AM
</div> </div>
</div> </div>
<div /> <div />
<div> <div>
<div <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500" 8 AM
> </div>
6 AM </div>
</div> <div />
</div> <div>
<div /> <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
<div> 10 AM
<div </div>
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500" </div>
> <div />
8 AM <div>
</div> <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
</div> 12 PM
<div /> </div>
<div> </div>
<div <div />
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500" <div>
> <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
10 AM 2 PM
</div> </div>
</div> </div>
<div /> <div />
<div> <div>
<div <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500" 4 PM
> </div>
12 PM </div>
</div> <div />
</div> <div>
<div /> <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
<div> 6 PM
<div </div>
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500" </div>
> <div />
2 PM <div>
</div> <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
</div> 8 PM
<div /> </div>
<div> </div>
<div <div />
class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500" <div>
> <div class="z-20 -mt-2.5 -ml-14 w-14 pr-4 text-right leading-5 text-zinc-500">
4 PM 10 PM
</div> </div>
</div> </div>
<div /> <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 <div
class="text-zinc-500 col-start-1 col-end-2 row-start-1 grid-rows-1 grid grid-cols-8 ml-8 " class="text-zinc-500 col-start-1 col-end-2 row-start-1 grid-rows-1 grid grid-cols-8 ml-8 "
> >
<div <div
class="col-start-2 col-end-9 row-start-1 grid divide-y divide-zinc-700/20" 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));" style="grid-template-rows: repeat(24, minmax(1.5rem, 1fr));"
> >
<div class="row-end-1 h-7" /> <div class="row-end-1 h-7" />
<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" 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> <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" 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> <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" 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> <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" 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> <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" 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> <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" 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> <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" 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> <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" 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> <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" 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> <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" 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> <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" 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> <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" 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 /> <div />
</div> </div>
<!-- day x lines --> <!-- day x lines -->
<div <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" 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 /> <div />
<div /> <div />
<div /> <div />
<div /> <div />
</div> </div>
<!-- actual entries --> <!-- actual entries -->
<ol <ol
class="col-start-1 col-end-2 row-start-1 grid grid-cols-8 px-8" 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;" style="grid-template-rows: 1.75rem repeat(96, minmax(0px, 1fr)) auto;"
> >
{#each $sessionsInWeek as session} {#each $sessionsInWeek as session}
<WeekBlockEntry <WeekBlockEntry
startTime={new Date( startTime={new Date(session.meta.startTimestampMs)}
session.meta.startTimestampMs endTime={new Date(session.meta.startTimestampMs)}
)} label={session.meta.branch}
endTime={new Date( href="/projects/{$project?.id}/sessions/{session.id}/"
session.meta.startTimestampMs />
)} {/each}
label={session.meta.branch} </ol>
href="/projects/{$project?.id}/sessions/{session.id}/" </div>
/> </div>
{/each} </div>
</ol> </div>
</div>
</div>
</div>
</div>
</div> </div>

View File

@ -1,224 +1,208 @@
<script lang="ts"> <script lang="ts">
import { Login } from "$lib/components"; import { Login } from '$lib/components';
import type { PageData } from "./$types"; import type { PageData } from './$types';
import MdAutorenew from "svelte-icons/md/MdAutorenew.svelte"; import MdAutorenew from 'svelte-icons/md/MdAutorenew.svelte';
import { log, toasts } from "$lib"; import { log, toasts } from '$lib';
export let data: PageData; export let data: PageData;
const { user, api } = data; const { user, api } = data;
$: saving = false; $: saving = false;
let userName = $user?.name; let userName = $user?.name;
let userPicture = $user?.picture; let userPicture = $user?.picture;
const fileTypes = ["image/jpeg", "image/png"]; const fileTypes = ['image/jpeg', 'image/png'];
const validFileType = (file: File) => { const validFileType = (file: File) => {
return fileTypes.includes(file.type); return fileTypes.includes(file.type);
}; };
const onPictureChange = (e: Event) => { const onPictureChange = (e: Event) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
const file = target.files?.[0]; const file = target.files?.[0];
if (file && validFileType(file)) { if (file && validFileType(file)) {
userPicture = URL.createObjectURL(file); userPicture = URL.createObjectURL(file);
} else { } else {
userPicture = $user?.picture; userPicture = $user?.picture;
toasts.error("Please use a valid image file"); toasts.error('Please use a valid image file');
} }
}; };
const onSubmit = async (e: SubmitEvent) => { const onSubmit = async (e: SubmitEvent) => {
if (!$user) return; if (!$user) return;
saving = true; saving = true;
const target = e.target as HTMLFormElement; const target = e.target as HTMLFormElement;
const formData = new FormData(target); const formData = new FormData(target);
const name = formData.get("name") as string | undefined; const name = formData.get('name') as string | undefined;
const picture = formData.get("picture") as File | undefined; const picture = formData.get('picture') as File | undefined;
try { try {
$user = await api.user.update($user.access_token, { $user = await api.user.update($user.access_token, {
name, name,
picture: picture, picture: picture
}); });
toasts.success("Profile updated"); toasts.success('Profile updated');
} catch (e) { } catch (e) {
log.error(e); log.error(e);
toasts.error("Failed to update user"); toasts.error('Failed to update user');
} }
saving = false; saving = false;
}; };
</script> </script>
<div class="p-4 mx-auto"> <div class="p-4 mx-auto">
<div class="max-w-xl mx-auto p-4"> <div class="max-w-xl mx-auto p-4">
{#if $user} {#if $user}
<div class="flex flex-col gap-6 text-zinc-100"> <div class="flex flex-col gap-6 text-zinc-100">
<header class="flex items-center justify-between"> <header class="flex items-center justify-between">
<h2 class="text-2xl font-medium"> <h2 class="text-2xl font-medium">GitButler Cloud Account</h2>
GitButler Cloud Account <Login {user} {api} />
</h2> </header>
<Login {user} {api} />
</header>
<form <form
on:submit={onSubmit} on:submit={onSubmit}
class="flex flex-row gap-12 justify-between rounded-lg p-2 items-start" 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"> <fields id="left" class="flex flex-1 flex-col gap-3">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<label for="name" class="text-zinc-400">Name</label> <label for="name" class="text-zinc-400">Name</label>
<input <input
id="name" id="name"
name="name" name="name"
bind:value={userName} bind:value={userName}
type="text" type="text"
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full" class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
required required
/> />
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<label for="email" class="text-zinc-400" <label for="email" class="text-zinc-400">Email</label>
>Email</label <input
> disabled
<input id="email"
disabled name="email"
id="email" bind:value={$user.email}
name="email" type="text"
bind:value={$user.email} class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
type="text" />
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full" </div>
/>
</div>
<footer class="pt-4"> <footer class="pt-4">
{#if saving} {#if saving}
<div <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" 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"> <div class="animate-spin w-5 h-5">
<MdAutorenew /> <MdAutorenew />
</div> </div>
<span>Updating...</span> <span>Updating...</span>
</div> </div>
{:else} {:else}
<button <button type="submit" class="py-1 px-3 rounded text-white bg-blue-400"
type="submit" >Update profile</button
class="py-1 px-3 rounded text-white bg-blue-400" >
>Update profile</button {/if}
> </footer>
{/if} </fields>
</footer>
</fields>
<fields id="right" class="flex flex-col gap-2 items-center"> <fields id="right" class="flex flex-col gap-2 items-center">
{#if $user.picture} {#if $user.picture}
<img <img
class="h-28 w-28 rounded-full border-zinc-300" class="h-28 w-28 rounded-full border-zinc-300"
src={userPicture} src={userPicture}
alt="Your avatar" alt="Your avatar"
required required
/> />
{/if} {/if}
<label <label
for="picture" 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" 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 Edit
<input <input
on:change={onPictureChange} on:change={onPictureChange}
type="file" type="file"
id="picture" id="picture"
name="picture" name="picture"
accept={fileTypes.join("")} accept={fileTypes.join('')}
class="hidden" class="hidden"
/> />
</label> </label>
</fields> </fields>
</form> </form>
</div> </div>
{:else} {:else}
<div <div class="flex flex-col text-white space-y-6 items-center justify-items-center">
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>
<div class="text-3xl font-bold text-white"> <ul class="text-zinc-400 pb-4 space-y-2">
Connect to GitButler Cloud <li class="flex flex-row space-x-3">
</div> <svg
<div> xmlns="http://www.w3.org/2000/svg"
Sign up or log in to GitButler Cloud for more tools and fill="none"
features: viewBox="0 0 24 24"
</div> stroke-width="1.5"
<ul class="text-zinc-400 pb-4 space-y-2"> stroke="white"
<li class="flex flex-row space-x-3"> class="w-6 h-6"
<svg >
xmlns="http://www.w3.org/2000/svg" <path
fill="none" stroke-linecap="round"
viewBox="0 0 24 24" stroke-linejoin="round"
stroke-width="1.5" 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"
stroke="white" />
class="w-6 h-6" </svg>
> <span>Backup everything you do in any of your projects</span>
<path </li>
stroke-linecap="round" <li class="flex flex-row space-x-3">
stroke-linejoin="round" <svg
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" xmlns="http://www.w3.org/2000/svg"
/> fill="none"
</svg> viewBox="0 0 24 24"
<span stroke-width="1.5"
>Backup everything you do in any of your projects</span stroke="white"
> class="w-6 h-6"
</li> >
<li class="flex flex-row space-x-3"> <path
<svg stroke-linecap="round"
xmlns="http://www.w3.org/2000/svg" stroke-linejoin="round"
fill="none" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
viewBox="0 0 24 24" />
stroke-width="1.5" </svg>
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> <span>Sync your data across devices</span>
</li> </li>
<li class="flex flex-row space-x-3"> <li class="flex flex-row space-x-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="white" stroke="white"
class="w-6 h-6" class="w-6 h-6"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="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" 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> </svg>
<span>AI commit message automated suggestions</span> <span>AI commit message automated suggestions</span>
</li> </li>
</ul> </ul>
<div class="mt-8 text-center"> <div class="mt-8 text-center">
<Login {user} {api} /> <Login {user} {api} />
</div> </div>
<div class="text-zinc-300 text-center"> <div class="text-zinc-300 text-center">
You will still need to give us permission for each project You will still need to give us permission for each project before we transfer any data to
before we transfer any data to our servers. You can revoke our servers. You can revoke this permission at any time.
this permission at any time. </div>
</div> </div>
</div> {/if}
{/if} </div>
</div>
</div> </div>

View File

@ -1,22 +1,15 @@
import preprocess from "svelte-preprocess"; import staticAdapter from '@sveltejs/adapter-static';
import staticAdapter from "@sveltejs/adapter-static"; import { vitePreprocess } from '@sveltejs/kit/vite';
import { vitePreprocess } from "@sveltejs/kit/vite";
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
preprocess: [ preprocess: vitePreprocess(),
vitePreprocess(), kit: {
preprocess({ adapter: staticAdapter({
postcss: true, precompress: true,
typescript: true, strict: false
}), })
], }
kit: {
adapter: staticAdapter({
precompress: true,
strict: false,
}),
},
}; };
export default config; export default config;

View File

@ -1,22 +1,22 @@
const config = { const config = {
content: ["./src/**/*.{html,js,svelte,ts}"], content: ['./src/**/*.{html,js,svelte,ts}'],
darkMode: "class", darkMode: 'class',
theme: { theme: {
fontFamily: { fontFamily: {
sans: ["Inter", "SF Pro", "-apple-system", "system-ui"], sans: ['Inter', 'SF Pro', '-apple-system', 'system-ui']
}, },
fontSize: { fontSize: {
xs: "10px", xs: '10px',
sm: "12px", sm: '12px',
base: "13px", base: '13px',
lg: "15px", lg: '15px',
xl: "22px", xl: '22px'
}, },
extend: {}, extend: {}
}, },
plugins: [], plugins: []
}; };
module.exports = config; module.exports = config;

View File

@ -1,17 +1,17 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true "strict": true
} }
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // 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 // 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 // from the referenced tsconfig.json - TypeScript does not merge them in
} }

View File

@ -1,26 +1,26 @@
import { defineConfig } from "vite"; import { defineConfig } from 'vite';
import { sveltekit } from "@sveltejs/kit/vite"; import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()], plugins: [sveltekit()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors // prevent vite from obscuring rust errors
clearScreen: false, clearScreen: false,
// tauri expects a fixed port, fail if that port is not available // tauri expects a fixed port, fail if that port is not available
server: { server: {
port: 1420, port: 1420,
strictPort: true, strictPort: true
}, },
// to make use of `TAURI_DEBUG` and other env variables // to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ["VITE_", "TAURI_"], envPrefix: ['VITE_', 'TAURI_'],
build: { build: {
// Tauri supports es2021 // Tauri supports es2021
target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13", target: process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13',
// don't minify for debug builds // don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? "esbuild" : false, minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
// produce sourcemaps for debug builds // produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG, sourcemap: !!process.env.TAURI_DEBUG
}, }
}); });