fubi 1.0.0-beta.3 → 1.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.es.js +21288 -0
- package/dist/index.iife.js +6 -0
- package/package.json +23 -21
- package/.prettierignore +0 -9
- package/.prettierrc +0 -23
- package/.vscode/extensions.json +0 -3
- package/config.toml +0 -3
- package/demo/index.html +0 -64
- package/index.html +0 -19
- package/publish.js +0 -77
- package/src/components/App.svelte +0 -17
- package/src/components/Debug.svelte +0 -39
- package/src/components/Layout.svelte +0 -47
- package/src/components/comment/CommentCard.svelte +0 -49
- package/src/components/comment/CommentFocusElement.svelte +0 -14
- package/src/components/comment/CommentMenu.svelte +0 -18
- package/src/components/comment/CommentPriority.svelte +0 -31
- package/src/components/comment/CommentReplyButton.svelte +0 -35
- package/src/components/comment/CommentUser.svelte +0 -22
- package/src/components/icons/AlertIcon.svelte +0 -7
- package/src/components/icons/CancelIcon.svelte +0 -8
- package/src/components/icons/CheckCircle.svelte +0 -7
- package/src/components/icons/CheckIcon.svelte +0 -7
- package/src/components/icons/CheckSmallIcon.svelte +0 -7
- package/src/components/icons/ChevronLeftIcon.svelte +0 -7
- package/src/components/icons/ChevronRightIcon.svelte +0 -7
- package/src/components/icons/ChevronsUpIcon.svelte +0 -8
- package/src/components/icons/DotsIcon.svelte +0 -9
- package/src/components/icons/PlusIcon.svelte +0 -8
- package/src/components/index.ts +0 -0
- package/src/components/toolbar/ButtonToolbar.svelte +0 -29
- package/src/components/toolbar/Menu.svelte +0 -42
- package/src/components/toolbar/Toolbar.svelte +0 -55
- package/src/components/ui/Button.svelte +0 -33
- package/src/components/ui/ButtonIcon.svelte +0 -31
- package/src/components/ui/ButtonSmall.svelte +0 -12
- package/src/components/ui/ButtonTabbar.svelte +0 -35
- package/src/components/ui/Chip.svelte +0 -28
- package/src/components/ui/Icon.svelte +0 -38
- package/src/components/ui/Input.svelte +0 -9
- package/src/components/ui/List.svelte +0 -18
- package/src/components/ui/ListItem.svelte +0 -87
- package/src/components/ui/ListLoader.svelte +0 -20
- package/src/components/ui/Loader.svelte +0 -39
- package/src/components/ui/Logo.svelte +0 -20
- package/src/components/ui/Msg.svelte +0 -21
- package/src/components/ui/Navbar.svelte +0 -33
- package/src/components/ui/Page.svelte +0 -67
- package/src/components/ui/Tabbar.svelte +0 -60
- package/src/components/ui/Toggle.svelte +0 -8
- package/src/components/ui/Window.svelte +0 -61
- package/src/index.ts +0 -28
- package/src/lib/actions.ts +0 -58
- package/src/lib/haptics.ts +0 -94
- package/src/lib/logger.ts +0 -22
- package/src/lib/routes.ts +0 -37
- package/src/lib/utils.ts +0 -27
- package/src/modules/app.svelte.ts +0 -66
- package/src/modules/auth.svelte.ts +0 -80
- package/src/modules/collection.svelte.ts +0 -79
- package/src/modules/comments.svelte.ts +0 -34
- package/src/modules/dom.svelte.ts +0 -26
- package/src/modules/domains.svelte.ts +0 -91
- package/src/modules/environment.svelte.ts +0 -10
- package/src/modules/events.svelte.ts +0 -23
- package/src/modules/folders.svelte.ts +0 -64
- package/src/modules/home.svelte.ts +0 -29
- package/src/modules/hover.svelte.ts +0 -3
- package/src/modules/index.ts +0 -28
- package/src/modules/keys.svelte.ts +0 -72
- package/src/modules/module.svelte.ts +0 -47
- package/src/modules/navbar.svelte.ts +0 -11
- package/src/modules/pages.svelte.ts +0 -126
- package/src/modules/project.svelte.ts +0 -70
- package/src/modules/router.svelte.ts +0 -99
- package/src/modules/tabbar.svelte.ts +0 -21
- package/src/modules/teams.svelte.ts +0 -0
- package/src/modules/toolbar.svelte.ts +0 -34
- package/src/modules/watch.svelte.ts +0 -8
- package/src/modules/win.svelte.ts +0 -273
- package/src/pages/Comments.svelte +0 -32
- package/src/pages/Error.svelte +0 -25
- package/src/pages/Folders.svelte +0 -53
- package/src/pages/Login.svelte +0 -44
- package/src/pages/Pages.svelte +0 -75
- package/src/pages/Thread.svelte +0 -11
- package/src/styles/global.css +0 -16
- package/src/styles/router.css +0 -79
- package/src/styles/styles.css +0 -159
- package/src/styles/tailwind.css +0 -115
- package/src/styles/variables.css +0 -99
- package/src/test.ts +0 -22
- package/src/types/fubi.ts +0 -0
- package/src/types/msg.ts +0 -5
- package/src/types/pocketbase.ts +0 -7
- package/src/types/user.ts +0 -8
- package/svelte.config.js +0 -11
- package/tsconfig.json +0 -41
- package/vite.config.ts +0 -61
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { logger } from "@logger";
|
|
2
|
-
|
|
3
|
-
import { CollectionItem } from "@/types/pocketbase";
|
|
4
|
-
import { CollectionModule } from "./collection.svelte";
|
|
5
|
-
|
|
6
|
-
import type { ProjectType } from "./project.svelte";
|
|
7
|
-
|
|
8
|
-
export interface DomainType extends CollectionItem {
|
|
9
|
-
name: string;
|
|
10
|
-
project: ProjectType;
|
|
11
|
-
origin: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const log = logger("domains");
|
|
15
|
-
|
|
16
|
-
export class Domains extends CollectionModule<DomainType> {
|
|
17
|
-
url = $state<URL | null>(null);
|
|
18
|
-
current = $derived.by(() => {
|
|
19
|
-
const origin = this.url?.origin;
|
|
20
|
-
if (!origin) return null;
|
|
21
|
-
return this.data.find((d) => d.origin === origin) || null;
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
start() {
|
|
25
|
-
this.initCollection({
|
|
26
|
-
name: "domains",
|
|
27
|
-
filters: `project = "${this.modules.project.id}"`,
|
|
28
|
-
sort: "-updated",
|
|
29
|
-
query: { project: this.modules.project.id }, // project param is required in API rules
|
|
30
|
-
onFinish: () => this.check(),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
this.getUrl();
|
|
34
|
-
|
|
35
|
-
this.fetch();
|
|
36
|
-
//this.testBadRegistration();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async check() {
|
|
40
|
-
if (this.current) {
|
|
41
|
-
log.info(`Domain already registered: ${this.current.origin}`);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const newDomain = await this.collection.create({
|
|
47
|
-
project: this.modules.project.id,
|
|
48
|
-
origin,
|
|
49
|
-
});
|
|
50
|
-
this.current = newDomain;
|
|
51
|
-
this.track("ok", `Domain registered`, { domain: newDomain });
|
|
52
|
-
} catch (err: any) {
|
|
53
|
-
this.track("error", "Failed to register domain", { error: err.message, origin });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async verify() {
|
|
58
|
-
if (!this.current) {
|
|
59
|
-
log.error("No current domain to verify");
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!this.modules.auth.user) return;
|
|
64
|
-
|
|
65
|
-
if (this.current.verified) {
|
|
66
|
-
log.info("Domain already verified:", this.current.origin);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Here you would implement actual verification logic, e.g., checking DNS records or a verification file.
|
|
71
|
-
await this.collection.update(this.current.id, { verified: true });
|
|
72
|
-
this.current.verified = true;
|
|
73
|
-
log.ok("Domain verified:", this.current.origin);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
getUrl() {
|
|
77
|
-
this.url = new URL(window.location.href);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async testBadRegistration() {
|
|
81
|
-
try {
|
|
82
|
-
await this.collection.create({
|
|
83
|
-
origin: "https://evil-site.com",
|
|
84
|
-
project: this.modules.project.id,
|
|
85
|
-
});
|
|
86
|
-
log.warn("BAD: Registered fake origin - API rule not working!");
|
|
87
|
-
} catch (err: any) {
|
|
88
|
-
log.info("GOOD: Fake origin blocked:", err.message);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Module } from "./module.svelte";
|
|
2
|
-
|
|
3
|
-
export class Events extends Module {
|
|
4
|
-
listeners = $state(new Map()) as Map<string, any>;
|
|
5
|
-
|
|
6
|
-
add(
|
|
7
|
-
type: string,
|
|
8
|
-
handler: (e: any) => void,
|
|
9
|
-
options?: {
|
|
10
|
-
target?: EventTarget;
|
|
11
|
-
capture?: boolean;
|
|
12
|
-
},
|
|
13
|
-
) {
|
|
14
|
-
const { target = document, capture = false } = options || {};
|
|
15
|
-
|
|
16
|
-
if (!this.listeners.has(type)) {
|
|
17
|
-
this.listeners.set(type, []);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
this.listeners.get(type).push({ handler, target });
|
|
21
|
-
target.addEventListener(type, handler, capture);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { logger } from "@logger";
|
|
2
|
-
import { CollectionItem } from "@/types/pocketbase";
|
|
3
|
-
import { CollectionModule } from "./collection.svelte";
|
|
4
|
-
|
|
5
|
-
export interface FolderType extends CollectionItem {
|
|
6
|
-
name: string;
|
|
7
|
-
locked: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const log = logger("folders");
|
|
11
|
-
|
|
12
|
-
export class Folders extends CollectionModule<FolderType> {
|
|
13
|
-
unlockedCount = $derived(this.data.filter((f) => !f.locked).length);
|
|
14
|
-
declare current: FolderType;
|
|
15
|
-
|
|
16
|
-
constructor() {
|
|
17
|
-
super();
|
|
18
|
-
this.storage<FolderType>("current", "fubi-folders-current", {} as FolderType);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async start() {
|
|
22
|
-
this.initCollection({
|
|
23
|
-
name: "folders",
|
|
24
|
-
filters: `project = "${this.modules.project.id}"`,
|
|
25
|
-
expand: "users,project.team",
|
|
26
|
-
sort: "-updated",
|
|
27
|
-
transform: (record, authUser) => {
|
|
28
|
-
const isAdmin = record.expand?.project?.expand?.team?.admin === authUser?.id;
|
|
29
|
-
const isAuthorizedUser = record.expand?.users?.some((u: { id: string }) => u.id === authUser?.id);
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
...record,
|
|
33
|
-
locked: !(isAdmin || isAuthorizedUser),
|
|
34
|
-
};
|
|
35
|
-
},
|
|
36
|
-
query: { project: this.modules.project.id },
|
|
37
|
-
onChange: () => this.checkAccess(),
|
|
38
|
-
onFinish: () => {
|
|
39
|
-
this.checkAccess();
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
checkAccess() {
|
|
45
|
-
if (this.current.locked) {
|
|
46
|
-
log.warn("current locked");
|
|
47
|
-
this.modules.router.goto("folders");
|
|
48
|
-
this.current = {} as FolderType;
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (this.unlockedCount < 2 && this.modules.router.current === "folders") {
|
|
52
|
-
log.warn("only one or no unlocked folder, redirect to pages");
|
|
53
|
-
this.modules.router.goto("pages");
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
select(folder: FolderType) {
|
|
59
|
-
if (this.current.name === folder.name) return;
|
|
60
|
-
this.modules.pages.data = [];
|
|
61
|
-
this.current = folder;
|
|
62
|
-
this.checkAccess();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { ScrollState } from "runed";
|
|
2
|
-
import { logger } from "@logger";
|
|
3
|
-
|
|
4
|
-
import { Module } from "./module.svelte";
|
|
5
|
-
|
|
6
|
-
const log = logger("home");
|
|
7
|
-
|
|
8
|
-
export class Home extends Module {
|
|
9
|
-
scroll = new ScrollState({
|
|
10
|
-
element: () => this.scrollContainer,
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
manualActiveState = $state<boolean>(false);
|
|
14
|
-
manualActiveTab = $state<"Pages" | "Comments">("Pages");
|
|
15
|
-
|
|
16
|
-
activeTab = $derived(
|
|
17
|
-
this.manualActiveState ? this.manualActiveTab : this.scroll.progress.x > 50 ? "Comments" : "Pages",
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
scrollContainer = $state<HTMLElement>();
|
|
21
|
-
|
|
22
|
-
scrollTo(tab: "Pages" | "Comments") {
|
|
23
|
-
console.log("scrollTo", tab);
|
|
24
|
-
this.manualActiveState = true;
|
|
25
|
-
this.manualActiveTab = tab;
|
|
26
|
-
|
|
27
|
-
this.scroll.x = tab === "Pages" ? 0 : (this.scrollContainer?.scrollWidth ?? 0);
|
|
28
|
-
}
|
|
29
|
-
}
|
package/src/modules/index.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { getContext } from "svelte";
|
|
2
|
-
|
|
3
|
-
import type { App } from "./app.svelte";
|
|
4
|
-
|
|
5
|
-
export { Auth } from "./auth.svelte";
|
|
6
|
-
export { Events } from "./events.svelte";
|
|
7
|
-
export { Toolbar } from "./toolbar.svelte";
|
|
8
|
-
export { Keys } from "./keys.svelte";
|
|
9
|
-
export { Project } from "./project.svelte";
|
|
10
|
-
export { Folders } from "./folders.svelte";
|
|
11
|
-
export { Pages } from "./pages.svelte";
|
|
12
|
-
export { Dom } from "./dom.svelte";
|
|
13
|
-
export { Win } from "./win.svelte";
|
|
14
|
-
export { Navbar } from "./navbar.svelte";
|
|
15
|
-
export { Tabbar } from "./tabbar.svelte";
|
|
16
|
-
export { Environment } from "./environment.svelte";
|
|
17
|
-
export { Router } from "./router.svelte";
|
|
18
|
-
export { Hover } from "./hover.svelte";
|
|
19
|
-
export { Comments } from "./comments.svelte";
|
|
20
|
-
export { Domains } from "./domains.svelte";
|
|
21
|
-
export { Home } from "./home.svelte";
|
|
22
|
-
|
|
23
|
-
export { watch } from "./watch.svelte";
|
|
24
|
-
|
|
25
|
-
export function getModules(): App["modules"] {
|
|
26
|
-
const context = getContext("app") as App;
|
|
27
|
-
return context.modules;
|
|
28
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { untrack } from "svelte";
|
|
2
|
-
import { Module } from "./module.svelte";
|
|
3
|
-
import { watch } from ".";
|
|
4
|
-
|
|
5
|
-
type KeyEventType = {
|
|
6
|
-
key: string | null;
|
|
7
|
-
code: string | null;
|
|
8
|
-
ctrlKey: boolean;
|
|
9
|
-
metaKey: boolean;
|
|
10
|
-
altKey: boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export class Keys extends Module {
|
|
14
|
-
handlers = new Map<string, Set<() => void>>();
|
|
15
|
-
|
|
16
|
-
active = $state<KeyEventType>({
|
|
17
|
-
key: null,
|
|
18
|
-
code: null,
|
|
19
|
-
ctrlKey: false,
|
|
20
|
-
metaKey: false,
|
|
21
|
-
altKey: false,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
start() {
|
|
25
|
-
this.modules.events.add("keydown", (e: KeyboardEvent) => this.handleKeydown(e));
|
|
26
|
-
this.modules.events.add("keyup", (e: KeyboardEvent) => this.handleKeyup(e));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
on(key: string, handler: () => void) {
|
|
30
|
-
if (!this.handlers.has(key)) this.handlers.set(key, new Set());
|
|
31
|
-
this.handlers.get(key)!.add(handler);
|
|
32
|
-
|
|
33
|
-
// cleanup
|
|
34
|
-
return () => {
|
|
35
|
-
this.handlers.get(key)?.delete(handler);
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
handleKeydown = (e: KeyboardEvent) => {
|
|
40
|
-
const { key, code, ctrlKey, metaKey, altKey } = e;
|
|
41
|
-
this.active = { key, code, ctrlKey, metaKey, altKey };
|
|
42
|
-
|
|
43
|
-
const keyHandlers = this.handlers.get(key);
|
|
44
|
-
if (keyHandlers) keyHandlers.forEach((handler) => handler());
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
handleKeyup = (e: KeyboardEvent) => {
|
|
48
|
-
this.reset();
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
hasFocus(): boolean {
|
|
52
|
-
const activeEl = document.activeElement as HTMLElement | null;
|
|
53
|
-
const shadowActiveEl = this.modules.win.el?.shadowRoot?.activeElement as HTMLElement | null;
|
|
54
|
-
|
|
55
|
-
const elementsToCheck = [activeEl, shadowActiveEl].filter(Boolean) as HTMLElement[];
|
|
56
|
-
|
|
57
|
-
for (const el of elementsToCheck) {
|
|
58
|
-
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") return true;
|
|
59
|
-
if (el.contentEditable === "true") return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
reset() {
|
|
66
|
-
this.active.key = null;
|
|
67
|
-
this.active.code = null;
|
|
68
|
-
this.active.ctrlKey = false;
|
|
69
|
-
this.active.metaKey = false;
|
|
70
|
-
this.active.altKey = false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { App } from "./app.svelte";
|
|
2
|
-
import { logger } from "@logger";
|
|
3
|
-
import { PersistedState } from "runed";
|
|
4
|
-
|
|
5
|
-
const log = logger("telemetry");
|
|
6
|
-
export abstract class Module {
|
|
7
|
-
modules!: App["modules"];
|
|
8
|
-
|
|
9
|
-
init = (modules: App["modules"]) => (this.modules = modules);
|
|
10
|
-
|
|
11
|
-
start?(): void;
|
|
12
|
-
|
|
13
|
-
destroy?(): void;
|
|
14
|
-
|
|
15
|
-
track = async (type: "info" | "error" | "warn" | "ok" = "info", message: string, data?: any) => {
|
|
16
|
-
try {
|
|
17
|
-
await this.modules.pb.collection("logs").create({
|
|
18
|
-
project: this.modules.project.id,
|
|
19
|
-
user: this.modules.pb.authStore.record?.id || null,
|
|
20
|
-
type,
|
|
21
|
-
message,
|
|
22
|
-
meta: {
|
|
23
|
-
domain: this.modules.domains?.current || null,
|
|
24
|
-
folder: this.modules.folders?.current || null,
|
|
25
|
-
route: this.modules.router.current,
|
|
26
|
-
},
|
|
27
|
-
data: data ? data : null,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
log[type](message, ...(data ? [data] : []));
|
|
31
|
-
} catch (e) {
|
|
32
|
-
log[type](message, ...(data ? [data] : []));
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
public storage<T>(propertyKey: string, key: string, initial: T) {
|
|
37
|
-
const state = new PersistedState(key, initial);
|
|
38
|
-
Object.defineProperty(this, propertyKey, {
|
|
39
|
-
get: () => state.current,
|
|
40
|
-
set: (value: T) => {
|
|
41
|
-
state.current = value;
|
|
42
|
-
},
|
|
43
|
-
enumerable: true,
|
|
44
|
-
configurable: true,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { CollectionItem } from "@/types/pocketbase";
|
|
2
|
-
import { CollectionModule } from "./collection.svelte";
|
|
3
|
-
|
|
4
|
-
import { logger } from "@logger";
|
|
5
|
-
import { CommentType } from "./comments.svelte";
|
|
6
|
-
export interface PageType extends CollectionItem {
|
|
7
|
-
name: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export type PageURL = {
|
|
11
|
-
origin: string;
|
|
12
|
-
protocol: string;
|
|
13
|
-
username: string;
|
|
14
|
-
password: string;
|
|
15
|
-
host: string;
|
|
16
|
-
hostname: string;
|
|
17
|
-
href: string;
|
|
18
|
-
pathname: string;
|
|
19
|
-
port: string;
|
|
20
|
-
search: string;
|
|
21
|
-
searchParams: URLSearchParams;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const log = logger("pages");
|
|
25
|
-
|
|
26
|
-
export class Pages extends CollectionModule<PageType> {
|
|
27
|
-
url = $state<URL | null>(null);
|
|
28
|
-
filters = $derived(
|
|
29
|
-
this.modules.folders.current && this.modules.domains.current
|
|
30
|
-
? `folder = "${this.modules.folders.current?.id}" && domain = "${this.modules.domains.current?.id}"`
|
|
31
|
-
: undefined,
|
|
32
|
-
);
|
|
33
|
-
current = $state<PageType | undefined>(undefined);
|
|
34
|
-
isNew = $derived(this.current?.id === "");
|
|
35
|
-
|
|
36
|
-
async start() {
|
|
37
|
-
this.initCollection({
|
|
38
|
-
name: "pages",
|
|
39
|
-
filters: this.filters,
|
|
40
|
-
sort: "-updated",
|
|
41
|
-
expand: "comments_via_page",
|
|
42
|
-
transform: (record, authUser) => {
|
|
43
|
-
const comments = record.expand?.comments_via_page || [];
|
|
44
|
-
const total = comments.length;
|
|
45
|
-
const done = comments.filter((c: CommentType) => c.done === true).length;
|
|
46
|
-
const progress = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
...record,
|
|
50
|
-
comments: comments,
|
|
51
|
-
totalComments: total,
|
|
52
|
-
doneComments: done,
|
|
53
|
-
progress,
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
onFinish: () => {
|
|
57
|
-
this.checkCurrent();
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
this.trackNavigation();
|
|
62
|
-
|
|
63
|
-
$effect(() => {
|
|
64
|
-
if (this.filters) this.fetch();
|
|
65
|
-
else this.data = [];
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
//this.modules.router.goto("error");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
checkCurrent() {
|
|
72
|
-
const current = this.data.find((page) => page.pathname === this.modules.domains.url?.pathname);
|
|
73
|
-
|
|
74
|
-
if (current) {
|
|
75
|
-
this.current = current;
|
|
76
|
-
} else {
|
|
77
|
-
this.current = {
|
|
78
|
-
id: "",
|
|
79
|
-
collectionId: "",
|
|
80
|
-
collectionName: "pages",
|
|
81
|
-
created: "",
|
|
82
|
-
updated: "",
|
|
83
|
-
name: this.getCurrentName(),
|
|
84
|
-
pathname: this.modules.domains.url?.pathname,
|
|
85
|
-
} as PageType;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
getCurrentName(): string {
|
|
90
|
-
if (!this.modules.domains.url) return "";
|
|
91
|
-
|
|
92
|
-
const path = this.modules.domains.url?.pathname || "";
|
|
93
|
-
const pageTitle = document?.querySelector('meta[name="page-title"]') as HTMLMetaElement;
|
|
94
|
-
const ogTitle = document?.querySelector('meta[property="og:title"]') as HTMLMetaElement;
|
|
95
|
-
const title = document?.title as string;
|
|
96
|
-
|
|
97
|
-
const name = path === "/" ? "Homepage" : pageTitle?.content || ogTitle?.content || title || path;
|
|
98
|
-
|
|
99
|
-
return name;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
handleRouteChange(): void {
|
|
103
|
-
this.fetch(); // TODO: make sure this is right
|
|
104
|
-
this.modules.domains.getUrl();
|
|
105
|
-
log.info("Route changed to", this.modules.domains.url?.pathname);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
trackNavigation(): void {
|
|
109
|
-
// Intercept pushState/replaceState
|
|
110
|
-
const originalPush = history.pushState;
|
|
111
|
-
const originalReplace = history.replaceState;
|
|
112
|
-
|
|
113
|
-
history.pushState = (...args) => {
|
|
114
|
-
originalPush.apply(history, args);
|
|
115
|
-
this.handleRouteChange();
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
history.replaceState = (...args) => {
|
|
119
|
-
originalReplace.apply(history, args);
|
|
120
|
-
this.handleRouteChange();
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Browser back/forward
|
|
124
|
-
this.modules.events.add("popstate", () => this.handleRouteChange(), { target: window });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { RecordModel } from "pocketbase";
|
|
2
|
-
|
|
3
|
-
import { CollectionModule } from "./collection.svelte";
|
|
4
|
-
import { CollectionItem } from "@/types/pocketbase";
|
|
5
|
-
|
|
6
|
-
import { logger } from "@logger";
|
|
7
|
-
const log = logger("project");
|
|
8
|
-
|
|
9
|
-
import { UserType } from "@/types/user";
|
|
10
|
-
|
|
11
|
-
export interface ProjectType extends CollectionItem {
|
|
12
|
-
name: string;
|
|
13
|
-
origin: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class Project extends CollectionModule<ProjectType> {
|
|
17
|
-
id: string = $state("");
|
|
18
|
-
current = $state({} as ProjectType | null);
|
|
19
|
-
isTeamMember = $state<boolean | null>(null);
|
|
20
|
-
|
|
21
|
-
constructor(projectId: string) {
|
|
22
|
-
super();
|
|
23
|
-
this.id = projectId;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
start() {
|
|
27
|
-
this.initCollection({
|
|
28
|
-
name: "projects",
|
|
29
|
-
filters: `id = "${this.id}"`,
|
|
30
|
-
sort: "-updated",
|
|
31
|
-
perPage: 1,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Checks if the user is a team member or admin of the current project
|
|
36
|
-
* @param userId - ID of the user to check
|
|
37
|
-
* @returns boolean - true if user is a team member or admin, false otherwise
|
|
38
|
-
*/
|
|
39
|
-
async checkTeamMembership(userId: string): Promise<boolean> {
|
|
40
|
-
try {
|
|
41
|
-
const result = await this.modules.pb.collection("projects").getList(1, 1, {
|
|
42
|
-
filter: `id = "${this.id}" && (team.users.id = "${userId}" || team.admin.id = "${userId}")`,
|
|
43
|
-
query: { project: this.id },
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
this.isTeamMember = result.items.length > 0;
|
|
47
|
-
log.info(this.isTeamMember ? "User is a team member" : "User is an external user");
|
|
48
|
-
|
|
49
|
-
return this.isTeamMember;
|
|
50
|
-
} catch (err) {
|
|
51
|
-
this.track("error", "Team check failed:", err);
|
|
52
|
-
this.isTeamMember = false;
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async fetchProject() {
|
|
58
|
-
log.info(`Fetching project with id = ${this.id}`);
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const record = await this.modules.pb.collection("projects").getOne<RecordModel>(this.id);
|
|
62
|
-
this.current = record as ProjectType;
|
|
63
|
-
return record as ProjectType;
|
|
64
|
-
} catch (err) {
|
|
65
|
-
log.error("Invalid or no projectId found:", err);
|
|
66
|
-
this.current = null;
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { mount } from "svelte";
|
|
2
|
-
import { routes } from "../lib/routes";
|
|
3
|
-
import { logger } from "../lib/logger";
|
|
4
|
-
import { Module } from "./module.svelte";
|
|
5
|
-
|
|
6
|
-
import { UserType } from "../types/user";
|
|
7
|
-
|
|
8
|
-
const NAVIGATION_DURATION = 400;
|
|
9
|
-
|
|
10
|
-
const log = logger("router");
|
|
11
|
-
|
|
12
|
-
export class Router extends Module {
|
|
13
|
-
routes = $state(routes);
|
|
14
|
-
navigating = $state(false);
|
|
15
|
-
direction = $state<"forward" | "back">("forward");
|
|
16
|
-
|
|
17
|
-
previous = $state(null as string | null);
|
|
18
|
-
declare current: string;
|
|
19
|
-
next = $state(null as string | null);
|
|
20
|
-
|
|
21
|
-
constructor() {
|
|
22
|
-
super();
|
|
23
|
-
this.storage<string>("current", "fubi-router-current", "login");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
start() {
|
|
27
|
-
$effect(() => {
|
|
28
|
-
if (this.modules.win.el) this.mountPages();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
console.log(routes);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
mountPages() {
|
|
35
|
-
Object.entries(this.routes).forEach(([name, route]) => {
|
|
36
|
-
log.info("Mounting page:", name);
|
|
37
|
-
if (!this.modules.win.el) return;
|
|
38
|
-
mount(route.component, { target: this.modules.win.el });
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
determineRoute(user: UserType | null, isAllowed: boolean) {
|
|
43
|
-
if (!user) return ["login", "back"];
|
|
44
|
-
if (!isAllowed) return ["login", "back"];
|
|
45
|
-
|
|
46
|
-
if (this.current === "login") {
|
|
47
|
-
const accessibleFolders = this.modules.folders.data.filter((f) => f.unlocked);
|
|
48
|
-
if (accessibleFolders.length === 1) return ["home", "forward"];
|
|
49
|
-
else return ["folders", "forward"];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (this.current === "pages") return ["comments"];
|
|
53
|
-
|
|
54
|
-
return [this.current || "home"];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
hasRoute(name: string) {
|
|
58
|
-
return name in this.routes;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
goto(name: string, direction: "forward" | "back" = "forward") {
|
|
62
|
-
if (!this.hasRoute(name) || this.current === name) {
|
|
63
|
-
log.error("Invalid or same route, cannot navigate to:", name);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (!this.modules.auth.user) {
|
|
67
|
-
log.warn("No users module, cannot navigate.");
|
|
68
|
-
this.transition("login", "back");
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const nextRoute = this.modules.auth.user ? name : "login";
|
|
73
|
-
this.transition(nextRoute, direction);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private transition(pageName: string, direction: "forward" | "back") {
|
|
77
|
-
if (this.navigating) {
|
|
78
|
-
log.warn("Already navigating, ignoring.");
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
this.navigating = true;
|
|
83
|
-
this.direction = direction;
|
|
84
|
-
|
|
85
|
-
if (direction === "forward") this.next = pageName;
|
|
86
|
-
else this.previous = pageName;
|
|
87
|
-
|
|
88
|
-
this.modules.win.el?.classList.add(`router-transition-${direction}`);
|
|
89
|
-
|
|
90
|
-
setTimeout(() => {
|
|
91
|
-
this.navigating = false;
|
|
92
|
-
this.previous = this.current;
|
|
93
|
-
this.current = pageName;
|
|
94
|
-
this.next = null;
|
|
95
|
-
this.modules.win.el?.classList.remove(`router-transition-${direction}`);
|
|
96
|
-
this.modules.win.focusPageContent();
|
|
97
|
-
}, NAVIGATION_DURATION);
|
|
98
|
-
}
|
|
99
|
-
}
|