Compare commits
No commits in common. "cae360fc0efba9ecb8e98a61311a2baca089a151" and "52c94d22ad15199b50c7759283624c666c7dd9b1" have entirely different histories.
cae360fc0e
...
52c94d22ad
@ -280,9 +280,9 @@ func (rs *notesResource) CreateVersion(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Handler for returning full data of the currently scoped note version. Identical to the beginning
|
// Handler for returning full data of the currently scoped note version. Identical to the beginning
|
||||||
// of the `RollbackNoteVersion` handler.
|
// of the `RollbackNoteVersion` handler.
|
||||||
func (rs *notesResource) GetFullVersion(w http.ResponseWriter, r *http.Request) {
|
func (rs *notesResource) GetFullVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
fullVersion, ok := r.Context().Value(versionCtxKey{}).(*data.GetVersionRow)
|
fullVersion, ok := r.Context().Value(noteCtxKey{}).(*data.GetVersionRow)
|
||||||
if !ok {
|
if !ok {
|
||||||
respondError(w, http.StatusNotFound, "Version not found")
|
respondError(w, http.StatusNotFound, "Note not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,47 +262,6 @@
|
|||||||
@apply text-[var(--dark-text)]/60;
|
@apply text-[var(--dark-text)]/60;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Versions dropdown */
|
|
||||||
.versions-dropdown {
|
|
||||||
@apply absolute right-0 z-10 mt-2 w-52 origin-top-right space-y-0.5 rounded-md bg-[var(--light-foreground)] shadow-lg ring-1 ring-[var(--light-foreground)]/10 focus:outline-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .versions-dropdown {
|
|
||||||
@apply bg-[var(--dark-foreground)] ring-[var(--dark-foreground)]/10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.versions-dropdown-item {
|
|
||||||
@apply w-full cursor-pointer bg-[var(--light-foreground)] p-3 transition-colors hover:bg-[var(--light-background)];
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .versions-dropdown-item {
|
|
||||||
@apply bg-[var(--dark-foreground)] hover:bg-[var(--dark-background)];
|
|
||||||
}
|
|
||||||
|
|
||||||
.versions-dropdown-item-active {
|
|
||||||
@apply bg-[var(--light-accent)]/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .versions-dropdown-item-active {
|
|
||||||
@apply bg-[var(--dark-accent)]/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.versions-dropdown-item-text {
|
|
||||||
@apply text-xs text-[var(--light-text)]/60;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .versions-dropdown-item-text {
|
|
||||||
@apply text-[var(--dark-text)]/60;
|
|
||||||
}
|
|
||||||
|
|
||||||
.versions-dropdown-divider {
|
|
||||||
@apply divide-y divide-[var(--light-text)]/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .versions-dropdown-divider {
|
|
||||||
@apply divide-[var(--dark-text)]/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Note editor */
|
/* Note editor */
|
||||||
.note-title-input {
|
.note-title-input {
|
||||||
@apply w-full rounded-2xl border-b border-[var(--light-text)]/20 bg-transparent pb-2 text-2xl font-bold focus:border-[var(--light-accent)];
|
@apply w-full rounded-2xl border-b border-[var(--light-text)]/20 bg-transparent pb-2 text-2xl font-bold focus:border-[var(--light-accent)];
|
||||||
@ -321,7 +280,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.note-textarea {
|
.note-textarea {
|
||||||
@apply h-full max-h-full min-h-1/2 w-full resize-none rounded-2xl bg-transparent p-3.5 font-mono outline-none focus:border-4 focus:border-[var(--light-accent)]/60;
|
@apply h-full max-h-full w-full resize-none rounded-2xl bg-transparent p-3.5 font-mono outline-none focus:border-4 focus:border-[var(--light-accent)]/60;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-save-button {
|
.note-save-button {
|
||||||
|
@ -26,7 +26,6 @@ export interface FullNote {
|
|||||||
content: string
|
content: string
|
||||||
versionNumber: number
|
versionNumber: number
|
||||||
versionCreatedAt: Date
|
versionCreatedAt: Date
|
||||||
isActiveVersion: boolean
|
|
||||||
noteCreatedAt: Date
|
noteCreatedAt: Date
|
||||||
noteUpdatedAt: Date
|
noteUpdatedAt: Date
|
||||||
}
|
}
|
||||||
@ -61,34 +60,22 @@ interface NewNoteResponse {
|
|||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VersionMetadata {
|
interface VersionMetadataResponse {
|
||||||
versionID: string
|
versionID: string
|
||||||
title: string
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FullVersionResponse {
|
||||||
|
versionID: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
versionNumber: number
|
versionNumber: number
|
||||||
isActive: boolean
|
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiVersionMetadataResponse {
|
|
||||||
version_id: string
|
|
||||||
title: string
|
|
||||||
version_number: number
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiFullVersionResponse {
|
|
||||||
version_id: string
|
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
version_number: number
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some of these could just be local variables as they're not used globally
|
|
||||||
export const currentUser: Writable<User | null> = writable(null)
|
export const currentUser: Writable<User | null> = writable(null)
|
||||||
export const currentFullNote: Writable<FullNote | null> = writable(null)
|
export const currentFullNote: Writable<FullNote | null> = writable(null)
|
||||||
export const availableNotes: Writable<NoteMetadata[] | null> = writable(null)
|
export const availableNotes: Writable<NoteMetadata[] | null> = writable(null)
|
||||||
export const versionHistory: Writable<VersionMetadata[] | null> = writable(null)
|
|
||||||
export const accessToken: Writable<string | null> = writable(null)
|
export const accessToken: Writable<string | null> = writable(null)
|
||||||
export const csrfToken: Writable<string | null> = writable(null)
|
export const csrfToken: Writable<string | null> = writable(null)
|
||||||
export const isPending: Writable<boolean> = writable(false)
|
export const isPending: Writable<boolean> = writable(false)
|
||||||
@ -100,10 +87,6 @@ class ApiClient {
|
|||||||
private lastAtUpdate = new Date(0)
|
private lastAtUpdate = new Date(0)
|
||||||
private lastCsrfUpdate = new Date(0)
|
private lastCsrfUpdate = new Date(0)
|
||||||
private refreshInProgress = false
|
private refreshInProgress = false
|
||||||
private activeVersion = -1
|
|
||||||
private loadedNotesCache = new Map<string, FullNote>()
|
|
||||||
private loadedHistoryCache = new Map<string, VersionMetadata[]>()
|
|
||||||
private loadedVersionsCache = new Map<string, FullNote>() // Key: noteID + versionID
|
|
||||||
|
|
||||||
constructor(baseUrl: string) {
|
constructor(baseUrl: string) {
|
||||||
this.baseUrl = baseUrl
|
this.baseUrl = baseUrl
|
||||||
@ -307,50 +290,11 @@ class ApiClient {
|
|||||||
content: apiResponse.content,
|
content: apiResponse.content,
|
||||||
versionNumber: apiResponse.version_number,
|
versionNumber: apiResponse.version_number,
|
||||||
versionCreatedAt: new Date(apiResponse.version_created_at),
|
versionCreatedAt: new Date(apiResponse.version_created_at),
|
||||||
isActiveVersion: true, // This endpoint serves only the latest version
|
|
||||||
noteCreatedAt: new Date(apiResponse.note_created_at),
|
noteCreatedAt: new Date(apiResponse.note_created_at),
|
||||||
noteUpdatedAt: new Date(apiResponse.note_updated_at)
|
noteUpdatedAt: new Date(apiResponse.note_updated_at)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private deserializeVersionMetadatas(
|
|
||||||
apiResponses: ApiVersionMetadataResponse[]
|
|
||||||
): VersionMetadata[] {
|
|
||||||
return apiResponses.map((res) => {
|
|
||||||
return {
|
|
||||||
versionID: res.version_id,
|
|
||||||
title: res.title,
|
|
||||||
versionNumber: res.version_number,
|
|
||||||
isActive: this.activeVersion === res.version_number,
|
|
||||||
createdAt: new Date(res.created_at)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private joinDeserializedVersion(
|
|
||||||
noteID: string,
|
|
||||||
apiResponse: ApiFullVersionResponse
|
|
||||||
): FullNote | undefined {
|
|
||||||
// Cache lookups are safe here due to this always being called *after* fetching the actual `FullNote`
|
|
||||||
const cachedNote = this.loadedNotesCache.get(noteID)
|
|
||||||
|
|
||||||
if (!cachedNote) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: cachedNote.id,
|
|
||||||
owner: cachedNote.owner,
|
|
||||||
title: apiResponse.title,
|
|
||||||
content: apiResponse.content,
|
|
||||||
versionNumber: apiResponse.version_number,
|
|
||||||
versionCreatedAt: new Date(apiResponse.created_at),
|
|
||||||
isActiveVersion: cachedNote.versionNumber === apiResponse.version_number,
|
|
||||||
noteCreatedAt: cachedNote.noteCreatedAt,
|
|
||||||
noteUpdatedAt: cachedNote.noteUpdatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async register(username: string, password: string): Promise<void> {
|
public async register(username: string, password: string): Promise<void> {
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
@ -594,24 +538,11 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getActiveFullNote(
|
public async getFullNote(noteID: string): Promise<FullNote | undefined> {
|
||||||
noteID: string,
|
|
||||||
fetchRemote: boolean
|
|
||||||
): Promise<FullNote | undefined> {
|
|
||||||
if (!UUID_REGEX.test(noteID)) {
|
if (!UUID_REGEX.test(noteID)) {
|
||||||
throw new Error("Invalid note ID format.")
|
throw new Error("Invalid note ID format.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt cache lookup only if we didn't just push new updates
|
|
||||||
if (!fetchRemote) {
|
|
||||||
const cachedNote = this.loadedNotesCache.get(noteID)
|
|
||||||
if (cachedNote != null) {
|
|
||||||
// console.log(`full note cache hit ${noteID}`)
|
|
||||||
this.activeVersion = cachedNote.versionNumber
|
|
||||||
return cachedNote
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/notes/${noteID}`, {
|
const response = await fetch(`${this.baseUrl}/notes/${noteID}`, {
|
||||||
@ -625,9 +556,7 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
const note = this.deserializeFullNote(data)
|
const note = this.deserializeFullNote(data)
|
||||||
|
|
||||||
console.log(`caching ${noteID}`)
|
console.log(note)
|
||||||
this.loadedNotesCache.set(noteID, note)
|
|
||||||
this.activeVersion = note.versionNumber
|
|
||||||
|
|
||||||
return note
|
return note
|
||||||
},
|
},
|
||||||
@ -660,22 +589,11 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNoteHistory(
|
public async getNoteHistory(noteID: string): Promise<VersionMetadataResponse[] | undefined> {
|
||||||
noteID: string,
|
|
||||||
fetchRemote: boolean
|
|
||||||
): Promise<VersionMetadata[] | undefined> {
|
|
||||||
if (!UUID_REGEX.test(noteID)) {
|
if (!UUID_REGEX.test(noteID)) {
|
||||||
throw new Error("Invalid note ID format.")
|
throw new Error("Invalid note ID format.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fetchRemote) {
|
|
||||||
const cachedVersions = this.loadedHistoryCache.get(noteID)
|
|
||||||
if (cachedVersions != null) {
|
|
||||||
// console.log(`full version cache hit ${noteID}`)
|
|
||||||
return cachedVersions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/notes/${noteID}/versions`, {
|
const response = await fetch(`${this.baseUrl}/notes/${noteID}/versions`, {
|
||||||
@ -684,13 +602,10 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await this.handleResponse<ApiVersionMetadataResponse[]>(response, {
|
const versions = await this.handleResponse<VersionMetadataResponse[]>(response, {
|
||||||
useBearerAuth: false
|
useBearerAuth: false
|
||||||
})
|
})
|
||||||
const versions = this.deserializeVersionMetadatas(data)
|
console.log(`got ${versions.length} version metadata results`)
|
||||||
|
|
||||||
console.log(`got ${versions.length} version metadata results, caching ${noteID}`)
|
|
||||||
this.loadedHistoryCache.set(noteID, versions)
|
|
||||||
|
|
||||||
return versions
|
return versions
|
||||||
},
|
},
|
||||||
@ -727,7 +642,10 @@ class ApiClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFullVersion(noteID: string, versionID: string): Promise<FullNote | undefined> {
|
public async getFullVersion(
|
||||||
|
noteID: string,
|
||||||
|
versionID: string
|
||||||
|
): Promise<FullVersionResponse | undefined> {
|
||||||
if (!UUID_REGEX.test(noteID)) {
|
if (!UUID_REGEX.test(noteID)) {
|
||||||
throw new Error("Invalid note ID format.")
|
throw new Error("Invalid note ID format.")
|
||||||
}
|
}
|
||||||
@ -736,18 +654,6 @@ class ApiClient {
|
|||||||
throw new Error("Invalid version ID format.")
|
throw new Error("Invalid version ID format.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Versions aren't editable so we don't need to prevent the system from attempting
|
|
||||||
// to locate each request's contents first from the cache
|
|
||||||
|
|
||||||
const cachedVersion = this.loadedVersionsCache.get(noteID + versionID)
|
|
||||||
if (cachedVersion != null) {
|
|
||||||
// console.log(`full version cache hit [${noteID}, ${versionID}]`)
|
|
||||||
return cachedVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: check if the requested version is the current version -> use `loadedNotesCache`
|
|
||||||
// (we probably have to modify the caching mechanism so we can look the regular note items up using versionID)
|
|
||||||
|
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/notes/${noteID}/${versionID}`, {
|
const response = await fetch(`${this.baseUrl}/notes/${noteID}/${versionID}`, {
|
||||||
@ -755,20 +661,7 @@ class ApiClient {
|
|||||||
...this.getAuthHeader()
|
...this.getAuthHeader()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return await this.handleResponse<FullVersionResponse>(response, { useBearerAuth: false })
|
||||||
const data = await this.handleResponse<ApiFullVersionResponse>(response, {
|
|
||||||
useBearerAuth: false
|
|
||||||
})
|
|
||||||
const version = this.joinDeserializedVersion(noteID, data)
|
|
||||||
|
|
||||||
if (!version) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`caching [${noteID}, ${versionID}]`)
|
|
||||||
this.loadedVersionsCache.set(noteID + versionID, version)
|
|
||||||
|
|
||||||
return version
|
|
||||||
},
|
},
|
||||||
{ useBearerAuth: true }
|
{ useBearerAuth: true }
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { marked } from "marked"
|
import { marked, Renderer } from "marked"
|
||||||
import { TITLE_MAX_LENGTH } from "$lib/const"
|
import { TITLE_MAX_LENGTH } from "$lib/const"
|
||||||
import { apiClient, type FullNote } from "$lib/client"
|
import type { FullNote } from "$lib/client"
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
export let note: FullNote
|
export let note: FullNote
|
||||||
@ -46,23 +46,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exitEditMode = () => {
|
|
||||||
isEditing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
// Ctrl+Enter or Command+Enter keyboard shortcut for saving the current note
|
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
|
||||||
event.preventDefault()
|
|
||||||
handleSave()
|
|
||||||
}
|
|
||||||
// Esc keyboard shortcut for quitting edit mode
|
|
||||||
else if (event.key === "Escape") {
|
|
||||||
event.preventDefault()
|
|
||||||
exitEditMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
saveNote(editableTitle, editableContent)
|
saveNote(editableTitle, editableContent)
|
||||||
}
|
}
|
||||||
@ -122,6 +105,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- TODO: capture Ctrl+Enter keyboard shortcut to save the note contents -->
|
||||||
|
|
||||||
<div class="flex h-full flex-col">
|
<div class="flex h-full flex-col">
|
||||||
<!-- Note title -->
|
<!-- Note title -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@ -130,7 +115,6 @@
|
|||||||
type="text"
|
type="text"
|
||||||
bind:value={editableTitle}
|
bind:value={editableTitle}
|
||||||
on:input={handleTitleChange}
|
on:input={handleTitleChange}
|
||||||
on:keydown={handleKeyDown}
|
|
||||||
placeholder="Title"
|
placeholder="Title"
|
||||||
class="note-title-input"
|
class="note-title-input"
|
||||||
/>
|
/>
|
||||||
@ -138,7 +122,7 @@
|
|||||||
{editableTitle.length}/{TITLE_MAX_LENGTH} characters
|
{editableTitle.length}/{TITLE_MAX_LENGTH} characters
|
||||||
</div>
|
</div>
|
||||||
<div class="note-char-count ml-3">
|
<div class="note-char-count ml-3">
|
||||||
Last updated: {new Date(note.versionCreatedAt).toLocaleString()}
|
Last updated: {new Date(note.noteUpdatedAt).toLocaleString()}
|
||||||
{#if !isEditing}
|
{#if !isEditing}
|
||||||
<!-- Minus 1 due to versioning beginning at 2 in the DB -->
|
<!-- Minus 1 due to versioning beginning at 2 in the DB -->
|
||||||
• Version: {note.versionNumber - 1}
|
• Version: {note.versionNumber - 1}
|
||||||
@ -148,8 +132,8 @@
|
|||||||
<h1 class="border-[var(--light-text)]/20 border-b pb-2 text-2xl font-bold">
|
<h1 class="border-[var(--light-text)]/20 border-b pb-2 text-2xl font-bold">
|
||||||
{note.title || "Untitled Note"}
|
{note.title || "Untitled Note"}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="note-char-count">
|
<div class="note-char-count ml-1">
|
||||||
Last updated: {new Date(note.versionCreatedAt).toLocaleString()}
|
Last updated: {new Date(note.noteUpdatedAt).toLocaleString()}
|
||||||
{#if !isEditing}
|
{#if !isEditing}
|
||||||
<!-- Minus 1 due to versioning beginning at 2 in the DB -->
|
<!-- Minus 1 due to versioning beginning at 2 in the DB -->
|
||||||
• Version: {note.versionNumber - 1}
|
• Version: {note.versionNumber - 1}
|
||||||
@ -166,7 +150,6 @@
|
|||||||
bind:this={textarea}
|
bind:this={textarea}
|
||||||
bind:value={editableContent}
|
bind:value={editableContent}
|
||||||
on:input={handleContentChange}
|
on:input={handleContentChange}
|
||||||
on:keydown={handleKeyDown}
|
|
||||||
placeholder="Markdown contents"
|
placeholder="Markdown contents"
|
||||||
class="note-textarea"
|
class="note-textarea"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
@ -8,22 +8,19 @@
|
|||||||
import {
|
import {
|
||||||
apiClient,
|
apiClient,
|
||||||
currentUser,
|
currentUser,
|
||||||
|
// isPending,
|
||||||
currentFullNote,
|
currentFullNote,
|
||||||
availableNotes,
|
availableNotes,
|
||||||
versionHistory,
|
cError
|
||||||
cError,
|
|
||||||
type FullNote
|
|
||||||
} from "$lib/client"
|
} from "$lib/client"
|
||||||
import ToggleSidebar from "$lib/icons/ToggleSidebar.svelte"
|
import ToggleSidebar from "$lib/icons/ToggleSidebar.svelte"
|
||||||
import VersionArrow from "$lib/icons/VersionArrow.svelte"
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
export let isLoading = false
|
export let isLoading = false
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let sidebarOpen = window.innerWidth > 768
|
let sidebarOpen = false
|
||||||
let showSettings = false
|
let showSettings = false
|
||||||
let showVersionsDropdown = false
|
|
||||||
let isEditing = false
|
let isEditing = false
|
||||||
|
|
||||||
onMount(async (): Promise<any> => {
|
onMount(async (): Promise<any> => {
|
||||||
@ -69,14 +66,11 @@
|
|||||||
const notes = await apiClient.listNotes()
|
const notes = await apiClient.listNotes()
|
||||||
|
|
||||||
if (notes) {
|
if (notes) {
|
||||||
|
console.log(notes)
|
||||||
availableNotes.set(notes)
|
availableNotes.set(notes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleVersionDropdown = () => {
|
|
||||||
showVersionsDropdown = !showVersionsDropdown
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
sidebarOpen = !sidebarOpen
|
sidebarOpen = !sidebarOpen
|
||||||
}
|
}
|
||||||
@ -100,58 +94,27 @@
|
|||||||
// (we need to find the ID from the updated `availableNotes`)
|
// (we need to find the ID from the updated `availableNotes`)
|
||||||
if ($availableNotes && $availableNotes.length > 0) {
|
if ($availableNotes && $availableNotes.length > 0) {
|
||||||
const latestNote = $availableNotes[0] // Assuming the latest note is first
|
const latestNote = $availableNotes[0] // Assuming the latest note is first
|
||||||
await selectNote(latestNote.id, true)
|
await selectNote(latestNote.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
isEditing = true // Open brand new notes in edit mode by default
|
isEditing = true // Open brand new notes in edit mode by default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLatestVersion = (note: FullNote | null) => {
|
const selectNote = async (noteId: string) => {
|
||||||
if (!note) {
|
// TODO: check from cache props first before requesting from API
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return note.isActiveVersion
|
console.log(`loading ${noteId}`)
|
||||||
}
|
const note = await apiClient.getFullNote(noteId)
|
||||||
|
|
||||||
const selectNote = async (noteID: string, fetchRemote: boolean) => {
|
|
||||||
const note = await apiClient.getActiveFullNote(noteID, fetchRemote)
|
|
||||||
|
|
||||||
if (note) {
|
if (note) {
|
||||||
currentFullNote.set(note)
|
currentFullNote.set(note)
|
||||||
isEditing = false
|
isEditing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = await apiClient.getNoteHistory(noteID, fetchRemote)
|
|
||||||
|
|
||||||
if (history) {
|
|
||||||
versionHistory.set(history)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MOBILE: Close sidebar when selecting a note from the list
|
|
||||||
if (window.innerWidth < 768) {
|
|
||||||
closeSidebar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectVersion = async (versionID: string) => {
|
|
||||||
if ($currentFullNote) {
|
|
||||||
const note = await apiClient.getFullVersion($currentFullNote.id, versionID)
|
|
||||||
|
|
||||||
if (note) {
|
|
||||||
currentFullNote.set(note)
|
|
||||||
showVersionsDropdown = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleEditMode = () => {
|
const toggleEditMode = () => {
|
||||||
if (isLatestVersion($currentFullNote)) {
|
isEditing = !isEditing
|
||||||
isEditing = !isEditing
|
|
||||||
} else {
|
|
||||||
cError.set("Editing historical versions is prohibited.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveNote = async (title: string, content: string) => {
|
const saveNote = async (title: string, content: string) => {
|
||||||
@ -159,7 +122,7 @@
|
|||||||
await apiClient.createVersion($currentFullNote.id, title, content)
|
await apiClient.createVersion($currentFullNote.id, title, content)
|
||||||
|
|
||||||
// Refresh the current note to get the latest version (+ assure that client and server are synced)
|
// Refresh the current note to get the latest version (+ assure that client and server are synced)
|
||||||
await selectNote($currentFullNote.id, true)
|
await selectNote($currentFullNote.id)
|
||||||
|
|
||||||
// Refresh the notes list to update any changes (ordering based on `updatedAt` field)
|
// Refresh the notes list to update any changes (ordering based on `updatedAt` field)
|
||||||
await loadNotes()
|
await loadNotes()
|
||||||
@ -214,49 +177,9 @@
|
|||||||
|
|
||||||
<div class="flex items-center space-x-4 pl-2">
|
<div class="flex items-center space-x-4 pl-2">
|
||||||
{#if $currentFullNote}
|
{#if $currentFullNote}
|
||||||
<!-- Note edit/preview button (only shown for latest version) -->
|
<button on:click={toggleEditMode} class="btn-primary rounded-full">
|
||||||
{#if isLatestVersion($currentFullNote)}
|
{isEditing ? "Preview" : "Edit"}
|
||||||
<button on:click={toggleEditMode} class="btn-primary rounded-full">
|
</button>
|
||||||
{isEditing ? "Preview" : "Edit"}
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button disabled class="btn-primary cursor-not-allowed rounded-full opacity-40">
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Version history dropdown menu -->
|
|
||||||
<div class="relative">
|
|
||||||
<button
|
|
||||||
on:click={toggleVersionDropdown}
|
|
||||||
class="btn-secondary flex items-center rounded-full"
|
|
||||||
>
|
|
||||||
<span>Versions</span>
|
|
||||||
<VersionArrow />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if showVersionsDropdown && $versionHistory && $versionHistory.length > 0}
|
|
||||||
<div class="versions-dropdown">
|
|
||||||
{#each $versionHistory as version}
|
|
||||||
<button
|
|
||||||
on:click={() => selectVersion(version.versionID)}
|
|
||||||
class="versions-dropdown-item {$currentFullNote.versionNumber ===
|
|
||||||
version.versionNumber
|
|
||||||
? 'versions-dropdown-item-active'
|
|
||||||
: ''}"
|
|
||||||
>
|
|
||||||
<p class="versions-dropdown-item-text truncate font-bold">{version.title}</p>
|
|
||||||
<span class="versions-dropdown-item-text"
|
|
||||||
>{version.createdAt.toLocaleString()}</span
|
|
||||||
>
|
|
||||||
{#if version.isActive}<span class="versions-dropdown-item-text ml-2 opacity-70"
|
|
||||||
>(active)</span
|
|
||||||
>{/if}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -268,7 +191,7 @@
|
|||||||
<!-- Note content area -->
|
<!-- Note content area -->
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
{#if $currentFullNote}
|
{#if $currentFullNote}
|
||||||
<NoteEditor note={$currentFullNote} bind:isEditing {saveNote} />
|
<NoteEditor note={$currentFullNote} {isEditing} {saveNote} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex h-full flex-col items-center justify-center">
|
<div class="flex h-full flex-col items-center justify-center">
|
||||||
<p class="mb-4 text-lg">None selected</p>
|
<p class="mb-4 text-lg">None selected</p>
|
||||||
|
@ -13,7 +13,9 @@
|
|||||||
export let toggleSettings: () => void
|
export let toggleSettings: () => void
|
||||||
export let logout: () => Promise<void>
|
export let logout: () => Promise<void>
|
||||||
export let createNewNote: () => Promise<void>
|
export let createNewNote: () => Promise<void>
|
||||||
export let selectNote: (noteId: string, fetchRemote: boolean) => Promise<void>
|
export let selectNote: (noteId: string) => Promise<void>
|
||||||
|
|
||||||
|
// TODO: add caching props -> e.g. `export const previousNotes: FullNote[]`
|
||||||
|
|
||||||
const formatDate = (dateString: string | Date): string => {
|
const formatDate = (dateString: string | Date): string => {
|
||||||
if (!dateString) {
|
if (!dateString) {
|
||||||
@ -38,7 +40,7 @@
|
|||||||
const handleNoteKeydown = (event: KeyboardEvent, noteID: string) => {
|
const handleNoteKeydown = (event: KeyboardEvent, noteID: string) => {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
event.preventDefault() // Prevent page scroll on space
|
event.preventDefault() // Prevent page scroll on space
|
||||||
selectNote(noteID, true) // Sync with API due to pushing updates
|
selectNote(noteID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,8 +51,8 @@
|
|||||||
: notes
|
: notes
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- TODO: add admin modal button (+ component similar to the settings modal) if the user is an admin -->
|
<!-- TODO: make the sidebar take up whole screen width on mobile -->
|
||||||
<!-- TODO: component-level paging support (via the implementation in `pages.ts`) -->
|
<!-- TODO: add admin modal (opens a view similar to the settings modal) button to the bottom (if the user is an admin) -->
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
class="sidebar z-10"
|
class="sidebar z-10"
|
||||||
@ -97,7 +99,7 @@
|
|||||||
<li
|
<li
|
||||||
class="sidebar-item"
|
class="sidebar-item"
|
||||||
class:sidebar-item-active={currentNote && note.id === currentNote.id}
|
class:sidebar-item-active={currentNote && note.id === currentNote.id}
|
||||||
on:click={() => selectNote(note.id, false)}
|
on:click={() => selectNote(note.id)}
|
||||||
on:keydown={(e) => handleNoteKeydown(e, note.id)}
|
on:keydown={(e) => handleNoteKeydown(e, note.id)}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="option"
|
role="option"
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
<svg class="ml-0.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 252 B |
Loading…
x
Reference in New Issue
Block a user