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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,128 +1,113 @@
<script lang="ts">
import type { LayoutData } from "./$types";
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
import type { Project } from "$lib/projects";
import { onDestroy } from "svelte";
import { page } from "$app/stores";
import type { LayoutData } from './$types';
import { getContext } from 'svelte';
import type { Writable } from 'svelte/store';
import type { Project } from '$lib/projects';
import { onDestroy } from 'svelte';
import { page } from '$app/stores';
export let data: LayoutData;
export let data: LayoutData;
$: project = data.project;
$: sessions = data.sessions;
$: lastSessionId = $sessions[$sessions.length - 1]?.id;
$: project = data.project;
$: sessions = data.sessions;
$: lastSessionId = $sessions[$sessions.length - 1]?.id;
function projectUrl(project: Project) {
const gitUrl = project.api?.git_url;
// get host from git url
const url = new URL(gitUrl);
const host = url.origin;
const projectId = gitUrl.split("/").pop();
function projectUrl(project: Project) {
const gitUrl = project.api?.git_url;
// get host from git url
const url = new URL(gitUrl);
const host = url.origin;
const projectId = gitUrl.split('/').pop();
return `${host}/projects/${projectId}`;
}
return `${host}/projects/${projectId}`;
}
const contextProjectStore: Writable<Project | null | undefined> =
getContext("project");
$: contextProjectStore.set($project);
onDestroy(() => {
contextProjectStore.set(null);
});
const contextProjectStore: Writable<Project | null | undefined> = getContext('project');
$: contextProjectStore.set($project);
onDestroy(() => {
contextProjectStore.set(null);
});
$: selection = $page?.route?.id?.split("/")?.[3];
$: selection = $page?.route?.id?.split('/')?.[3];
</script>
<nav
class="flex items-center flex-none justify-between py-2 px-8 space-x-3 border-b select-none text-zinc-300 border-zinc-700"
class="flex items-center flex-none justify-between py-2 px-8 space-x-3 border-b select-none text-zinc-300 border-zinc-700"
>
<div
class="text-zinc-400 w-64 font-medium grid grid-cols-3 items-center bg-zinc-700/50 rounded-lg h-7 px-4 gap-1"
>
<a
class="
<div
class="text-zinc-400 w-64 font-medium grid grid-cols-3 items-center bg-zinc-700/50 rounded-lg h-7 px-4 gap-1"
>
<a
class="
{selection === 'week' ? 'bg-zinc-600/70 text-zinc-100' : ''}
rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100"
href="/projects/{$project?.id}/week">Week</a
>
<a
href="/projects/{$project?.id}/day"
class="
href="/projects/{$project?.id}/week">Week</a
>
<a
href="/projects/{$project?.id}/day"
class="
{selection === 'day' ? 'bg-zinc-600/70 text-zinc-100' : ''}
rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100"
>Day</a
>
<a
href="/projects/{$project?.id}/sessions/{lastSessionId}"
class="
rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100">Day</a
>
<a
href="/projects/{$project?.id}/sessions/{lastSessionId}"
class="
{selection === 'sessions' ? 'bg-zinc-600/70 text-zinc-100' : ''}
rounded-lg h-7 flex items-center justify-center p-3 text-center hover:text-zinc-100"
title="go to current session">Session</a
>
</div>
title="go to current session">Session</a
>
</div>
<ul>
<li>
<a
href="/projects/{$project?.id}/settings"
class="text-zinc-400 hover:text-zinc-300"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</a>
</li>
</ul>
<ul>
<li>
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</a>
</li>
</ul>
</nav>
<slot />
<div class="absolute bottom-0 left-0 w-full">
<div
class="flex items-center flex-shrink-0 h-6 border-t select-none border-zinc-700 bg-zinc-900 "
>
<div
class="flex flex-row mx-4 items-center space-x-2 justify-between w-full"
>
{#if $project?.api?.sync}
<a
href="/projects/{$project?.id}/settings"
class="text-zinc-400 hover:text-zinc-300"
>
<div class="flex flex-row items-center space-x-2 ">
<div class="w-2 h-2 bg-green-700 rounded-full" />
<div class="text-zinc-200">Syncing</div>
</div>
</a>
<a target="_blank" rel="noreferrer" href={projectUrl($project)}
>Open in GitButler Cloud</a
>
{:else}
<a
href="/projects/{$project?.id}/settings"
class="text-zinc-400 hover:text-zinc-300"
>
<div class="flex flex-row items-center space-x-2 ">
<div class="w-2 h-2 bg-red-700 rounded-full" />
<div class="text-zinc-200">Offline</div>
</div>
</a>
{/if}
</div>
</div>
<div
class="flex items-center flex-shrink-0 h-6 border-t select-none border-zinc-700 bg-zinc-900 "
>
<div class="flex flex-row mx-4 items-center space-x-2 justify-between w-full">
{#if $project?.api?.sync}
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
<div class="flex flex-row items-center space-x-2 ">
<div class="w-2 h-2 bg-green-700 rounded-full" />
<div class="text-zinc-200">Syncing</div>
</div>
</a>
<a target="_blank" rel="noreferrer" href={projectUrl($project)}>Open in GitButler Cloud</a>
{:else}
<a href="/projects/{$project?.id}/settings" class="text-zinc-400 hover:text-zinc-300">
<div class="flex flex-row items-center space-x-2 ">
<div class="w-2 h-2 bg-red-700 rounded-full" />
<div class="text-zinc-200">Offline</div>
</div>
</a>
{/if}
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,224 +1,208 @@
<script lang="ts">
import { Login } from "$lib/components";
import type { PageData } from "./$types";
import MdAutorenew from "svelte-icons/md/MdAutorenew.svelte";
import { log, toasts } from "$lib";
import { Login } from '$lib/components';
import type { PageData } from './$types';
import MdAutorenew from 'svelte-icons/md/MdAutorenew.svelte';
import { log, toasts } from '$lib';
export let data: PageData;
const { user, api } = data;
export let data: PageData;
const { user, api } = data;
$: saving = false;
$: saving = false;
let userName = $user?.name;
let userPicture = $user?.picture;
let userName = $user?.name;
let userPicture = $user?.picture;
const fileTypes = ["image/jpeg", "image/png"];
const fileTypes = ['image/jpeg', 'image/png'];
const validFileType = (file: File) => {
return fileTypes.includes(file.type);
};
const validFileType = (file: File) => {
return fileTypes.includes(file.type);
};
const onPictureChange = (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
const onPictureChange = (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (file && validFileType(file)) {
userPicture = URL.createObjectURL(file);
} else {
userPicture = $user?.picture;
toasts.error("Please use a valid image file");
}
};
if (file && validFileType(file)) {
userPicture = URL.createObjectURL(file);
} else {
userPicture = $user?.picture;
toasts.error('Please use a valid image file');
}
};
const onSubmit = async (e: SubmitEvent) => {
if (!$user) return;
saving = true;
const onSubmit = async (e: SubmitEvent) => {
if (!$user) return;
saving = true;
const target = e.target as HTMLFormElement;
const formData = new FormData(target);
const name = formData.get("name") as string | undefined;
const picture = formData.get("picture") as File | undefined;
const target = e.target as HTMLFormElement;
const formData = new FormData(target);
const name = formData.get('name') as string | undefined;
const picture = formData.get('picture') as File | undefined;
try {
$user = await api.user.update($user.access_token, {
name,
picture: picture,
});
toasts.success("Profile updated");
} catch (e) {
log.error(e);
toasts.error("Failed to update user");
}
try {
$user = await api.user.update($user.access_token, {
name,
picture: picture
});
toasts.success('Profile updated');
} catch (e) {
log.error(e);
toasts.error('Failed to update user');
}
saving = false;
};
saving = false;
};
</script>
<div class="p-4 mx-auto">
<div class="max-w-xl mx-auto p-4">
{#if $user}
<div class="flex flex-col gap-6 text-zinc-100">
<header class="flex items-center justify-between">
<h2 class="text-2xl font-medium">
GitButler Cloud Account
</h2>
<Login {user} {api} />
</header>
<div class="max-w-xl mx-auto p-4">
{#if $user}
<div class="flex flex-col gap-6 text-zinc-100">
<header class="flex items-center justify-between">
<h2 class="text-2xl font-medium">GitButler Cloud Account</h2>
<Login {user} {api} />
</header>
<form
on:submit={onSubmit}
class="flex flex-row gap-12 justify-between rounded-lg p-2 items-start"
>
<fields id="left" class="flex flex-1 flex-col gap-3">
<div class="flex flex-col gap-1">
<label for="name" class="text-zinc-400">Name</label>
<input
id="name"
name="name"
bind:value={userName}
type="text"
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
required
/>
</div>
<form
on:submit={onSubmit}
class="flex flex-row gap-12 justify-between rounded-lg p-2 items-start"
>
<fields id="left" class="flex flex-1 flex-col gap-3">
<div class="flex flex-col gap-1">
<label for="name" class="text-zinc-400">Name</label>
<input
id="name"
name="name"
bind:value={userName}
type="text"
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
required
/>
</div>
<div class="flex flex-col gap-1">
<label for="email" class="text-zinc-400"
>Email</label
>
<input
disabled
id="email"
name="email"
bind:value={$user.email}
type="text"
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
/>
</div>
<div class="flex flex-col gap-1">
<label for="email" class="text-zinc-400">Email</label>
<input
disabled
id="email"
name="email"
bind:value={$user.email}
type="text"
class="px-2 py-1 text-zinc-300 bg-black border border-zinc-600 rounded-lg w-full"
/>
</div>
<footer class="pt-4">
{#if saving}
<div
class="flex w-32 flex-row w-content items-center gap-1 justify-center py-1 px-3 rounded text-white bg-blue-400"
>
<div class="animate-spin w-5 h-5">
<MdAutorenew />
</div>
<span>Updating...</span>
</div>
{:else}
<button
type="submit"
class="py-1 px-3 rounded text-white bg-blue-400"
>Update profile</button
>
{/if}
</footer>
</fields>
<footer class="pt-4">
{#if saving}
<div
class="flex w-32 flex-row w-content items-center gap-1 justify-center py-1 px-3 rounded text-white bg-blue-400"
>
<div class="animate-spin w-5 h-5">
<MdAutorenew />
</div>
<span>Updating...</span>
</div>
{:else}
<button type="submit" class="py-1 px-3 rounded text-white bg-blue-400"
>Update profile</button
>
{/if}
</footer>
</fields>
<fields id="right" class="flex flex-col gap-2 items-center">
{#if $user.picture}
<img
class="h-28 w-28 rounded-full border-zinc-300"
src={userPicture}
alt="Your avatar"
required
/>
{/if}
<fields id="right" class="flex flex-col gap-2 items-center">
{#if $user.picture}
<img
class="h-28 w-28 rounded-full border-zinc-300"
src={userPicture}
alt="Your avatar"
required
/>
{/if}
<label
for="picture"
class="px-2 -mt-6 -ml-16 cursor-pointer text-center font-sm text-zinc-300 bg-zinc-800 border border-zinc-600 rounded-lg"
>
Edit
<input
on:change={onPictureChange}
type="file"
id="picture"
name="picture"
accept={fileTypes.join("")}
class="hidden"
/>
</label>
</fields>
</form>
</div>
{:else}
<div
class="flex flex-col text-white space-y-6 items-center justify-items-center"
>
<div class="text-3xl font-bold text-white">
Connect to GitButler Cloud
</div>
<div>
Sign up or log in to GitButler Cloud for more tools and
features:
</div>
<ul class="text-zinc-400 pb-4 space-y-2">
<li class="flex flex-row space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="white"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"
/>
</svg>
<span
>Backup everything you do in any of your projects</span
>
</li>
<li class="flex flex-row space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="white"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
/>
</svg>
<label
for="picture"
class="px-2 -mt-6 -ml-16 cursor-pointer text-center font-sm text-zinc-300 bg-zinc-800 border border-zinc-600 rounded-lg"
>
Edit
<input
on:change={onPictureChange}
type="file"
id="picture"
name="picture"
accept={fileTypes.join('')}
class="hidden"
/>
</label>
</fields>
</form>
</div>
{:else}
<div class="flex flex-col text-white space-y-6 items-center justify-items-center">
<div class="text-3xl font-bold text-white">Connect to GitButler Cloud</div>
<div>Sign up or log in to GitButler Cloud for more tools and features:</div>
<ul class="text-zinc-400 pb-4 space-y-2">
<li class="flex flex-row space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="white"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"
/>
</svg>
<span>Backup everything you do in any of your projects</span>
</li>
<li class="flex flex-row space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="white"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
/>
</svg>
<span>Sync your data across devices</span>
</li>
<li class="flex flex-row space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="white"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
<span>AI commit message automated suggestions</span>
</li>
</ul>
<div class="mt-8 text-center">
<Login {user} {api} />
</div>
<div class="text-zinc-300 text-center">
You will still need to give us permission for each project
before we transfer any data to our servers. You can revoke
this permission at any time.
</div>
</div>
{/if}
</div>
<span>Sync your data across devices</span>
</li>
<li class="flex flex-row space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="white"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
<span>AI commit message automated suggestions</span>
</li>
</ul>
<div class="mt-8 text-center">
<Login {user} {api} />
</div>
<div class="text-zinc-300 text-center">
You will still need to give us permission for each project before we transfer any data to
our servers. You can revoke this permission at any time.
</div>
</div>
{/if}
</div>
</div>

View File

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

View File

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

View File

@ -1,17 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

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