+27
-17
crates/server/src/firehose.rs
+27
-17
crates/server/src/firehose.rs
···
1
1
//! Firehose consumption via AT Protocol Jetstream.
2
2
//!
3
3
//! Provides WebSocket subscription to Jetstream for indexing public records.
4
-
//! Filters for `app.malfestio.*` collections and indexes them locally.
4
+
//! Filters for `org.stormlightlabs.malfestio.*` collections and indexes them locally.
5
5
6
6
use crate::db::DbPool;
7
7
use async_trait::async_trait;
···
15
15
pub const DEFAULT_JETSTREAM_URL: &str = "wss://jetstream2.us-west.bsky.network/subscribe";
16
16
17
17
/// Collections we're interested in indexing.
18
-
pub const MALFESTIO_COLLECTIONS: &[&str] = &["app.malfestio.deck", "app.malfestio.card", "app.malfestio.note"];
18
+
pub const MALFESTIO_COLLECTIONS: &[&str] = &[
19
+
"org.stormlightlabs.malfestio.deck",
20
+
"org.stormlightlabs.malfestio.card",
21
+
"org.stormlightlabs.malfestio.note",
22
+
];
19
23
20
24
/// Deck record structure matching the Lexicon schema.
21
25
#[derive(Debug, Deserialize)]
···
85
89
async fn index_deck(
86
90
&self, did: &str, rkey: &str, rev: &str, record: &Value,
87
91
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
88
-
let at_uri = format!("at://{}/app.malfestio.deck/{}", did, rkey);
92
+
let at_uri = format!("at://{}/org.stormlightlabs.malfestio.deck/{}", did, rkey);
89
93
let deck: DeckRecord = serde_json::from_value(record.clone())?;
90
94
let created_at = parse_record_datetime(&deck.created_at);
91
95
···
122
126
async fn index_card(
123
127
&self, did: &str, rkey: &str, rev: &str, record: &Value,
124
128
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
125
-
let at_uri = format!("at://{}/app.malfestio.card/{}", did, rkey);
129
+
let at_uri = format!("at://{}/org.stormlightlabs.malfestio.card/{}", did, rkey);
126
130
let card: CardRecord = serde_json::from_value(record.clone())?;
127
131
let created_at = parse_record_datetime(&card.created_at);
128
132
let card_type = card.card_type.unwrap_or_else(|| "basic".to_string());
···
160
164
async fn index_note(
161
165
&self, did: &str, rkey: &str, rev: &str, record: &Value,
162
166
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
163
-
let at_uri = format!("at://{}/app.malfestio.note/{}", did, rkey);
167
+
let at_uri = format!("at://{}/org.stormlightlabs.malfestio.note/{}", did, rkey);
164
168
let note: NoteRecord = serde_json::from_value(record.clone())?;
165
169
let created_at = parse_record_datetime(¬e.created_at);
166
170
let visibility = note.visibility.unwrap_or_else(|| "public".to_string());
···
201
205
let client = self.pool.get().await?;
202
206
203
207
let table = match collection {
204
-
"app.malfestio.deck" => "indexed_decks",
205
-
"app.malfestio.card" => "indexed_cards",
206
-
"app.malfestio.note" => "indexed_notes",
208
+
"org.stormlightlabs.malfestio.deck" => "indexed_decks",
209
+
"org.stormlightlabs.malfestio.card" => "indexed_cards",
210
+
"org.stormlightlabs.malfestio.note" => "indexed_notes",
207
211
_ => return Ok(()),
208
212
};
209
213
···
289
293
match operation.as_str() {
290
294
"create" | "update" => {
291
295
let result = match collection.as_str() {
292
-
"app.malfestio.deck" => self.index_deck(&did, rkey, rev, &commit.record).await,
293
-
"app.malfestio.card" => self.index_card(&did, rkey, rev, &commit.record).await,
294
-
"app.malfestio.note" => self.index_note(&did, rkey, rev, &commit.record).await,
296
+
"org.stormlightlabs.malfestio.deck" => {
297
+
self.index_deck(&did, rkey, rev, &commit.record).await
298
+
}
299
+
"org.stormlightlabs.malfestio.card" => {
300
+
self.index_card(&did, rkey, rev, &commit.record).await
301
+
}
302
+
"org.stormlightlabs.malfestio.note" => {
303
+
self.index_note(&did, rkey, rev, &commit.record).await
304
+
}
295
305
_ => Ok(()),
296
306
};
297
307
···
414
424
415
425
#[test]
416
426
fn test_malfestio_collections() {
417
-
assert!(MALFESTIO_COLLECTIONS.contains(&"app.malfestio.deck"));
418
-
assert!(MALFESTIO_COLLECTIONS.contains(&"app.malfestio.card"));
419
-
assert!(MALFESTIO_COLLECTIONS.contains(&"app.malfestio.note"));
427
+
assert!(MALFESTIO_COLLECTIONS.contains(&"org.stormlightlabs.malfestio.deck"));
428
+
assert!(MALFESTIO_COLLECTIONS.contains(&"org.stormlightlabs.malfestio.card"));
429
+
assert!(MALFESTIO_COLLECTIONS.contains(&"org.stormlightlabs.malfestio.note"));
420
430
}
421
431
422
432
#[test]
···
425
435
"title": "Test Deck",
426
436
"description": "A test deck",
427
437
"tags": ["rust", "learning"],
428
-
"cardRefs": ["at://did:plc:abc/app.malfestio.card/123"],
438
+
"cardRefs": ["at://did:plc:abc/org.stormlightlabs.malfestio.card/123"],
429
439
"sourceRefs": [],
430
440
"license": "CC-BY-4.0",
431
441
"createdAt": "2024-01-01T00:00:00Z"
···
442
452
#[test]
443
453
fn test_parse_card_record() {
444
454
let json = serde_json::json!({
445
-
"deckRef": "at://did:plc:abc/app.malfestio.deck/123",
455
+
"deckRef": "at://did:plc:abc/org.stormlightlabs.malfestio.deck/123",
446
456
"front": "What is Rust?",
447
457
"back": "A systems programming language",
448
458
"cardType": "basic",
···
451
461
});
452
462
453
463
let card: CardRecord = serde_json::from_value(json).unwrap();
454
-
assert_eq!(card.deck_ref, "at://did:plc:abc/app.malfestio.deck/123");
464
+
assert_eq!(card.deck_ref, "at://did:plc:abc/org.stormlightlabs.malfestio.deck/123");
455
465
assert_eq!(card.front, "What is Rust?");
456
466
assert_eq!(card.back, "A systems programming language");
457
467
assert_eq!(card.card_type, Some("basic".to_string()));
+4
-35
crates/server/src/middleware/auth.rs
+4
-35
crates/server/src/middleware/auth.rs
···
250
250
/// but continues without error if no token or invalid token.
251
251
///
252
252
/// Used by endpoints that need to check permissions but don't require authentication.
253
-
pub async fn optional_auth_middleware(mut req: Request, next: Next) -> Response {
254
-
let auth_header = req.headers().get(http::header::AUTHORIZATION);
255
-
256
-
let token = match auth_header.and_then(|h| h.to_str().ok()).and_then(parse_auth_header) {
257
-
Some(AuthScheme::Bearer(t)) | Some(AuthScheme::DPoP(t)) => t,
258
-
None => {
259
-
return next.run(req).await;
260
-
}
261
-
};
262
-
263
-
let client = reqwest::Client::new();
264
-
let pds_url = std::env::var("PDS_URL").unwrap_or_else(|_| "https://bsky.social".to_string());
265
-
266
-
match client
267
-
.get(format!("{}/xrpc/com.atproto.server.getSession", pds_url))
268
-
.header("Authorization", format!("Bearer {}", token))
269
-
.send()
270
-
.await
271
-
{
272
-
Ok(response) if response.status().is_success() => {
273
-
let body: serde_json::Value = response.json().await.unwrap_or_default();
274
-
let did = body["did"].as_str().unwrap_or("").to_string();
275
-
let handle = body["handle"].as_str().unwrap_or("").to_string();
276
-
277
-
req.extensions_mut().insert(UserContext {
278
-
did,
279
-
handle,
280
-
access_token: token.to_string(),
281
-
pds_url: pds_url.clone(),
282
-
has_dpop: false,
283
-
});
284
-
}
285
-
_ => {}
253
+
pub async fn optional_auth_middleware(State(state): State<SharedState>, req: Request, next: Next) -> Response {
254
+
if req.headers().get(http::header::AUTHORIZATION).is_none() {
255
+
return next.run(req).await;
286
256
}
287
-
288
-
next.run(req).await
257
+
auth_middleware(State(state), req, next).await
289
258
}
290
259
291
260
/// Cleanup expired nonces from the cache.
+4
-4
crates/server/src/pds/client.rs
+4
-4
crates/server/src/pds/client.rs
···
128
128
/// # Arguments
129
129
///
130
130
/// * `did` - The user's DID (repository owner)
131
-
/// * `collection` - The collection NSID (e.g., "app.malfestio.deck")
131
+
/// * `collection` - The collection NSID (e.g., "org.stormlightlabs.malfestio.deck")
132
132
/// * `rkey` - The record key (TID)
133
133
/// * `record` - The record data as JSON
134
134
pub async fn put_record(
···
284
284
fn test_put_record_request_serialization() {
285
285
let request = PutRecordRequest {
286
286
repo: "did:plc:abc123".to_string(),
287
-
collection: "app.malfestio.deck".to_string(),
287
+
collection: "org.stormlightlabs.malfestio.deck".to_string(),
288
288
rkey: "3k5abc123".to_string(),
289
289
record: serde_json::json!({
290
290
"title": "Test Deck",
···
297
297
298
298
let json = serde_json::to_string(&request).unwrap();
299
299
assert!(json.contains("\"repo\":\"did:plc:abc123\""));
300
-
assert!(json.contains("\"collection\":\"app.malfestio.deck\""));
300
+
assert!(json.contains("\"collection\":\"org.stormlightlabs.malfestio.deck\""));
301
301
assert!(json.contains("\"rkey\":\"3k5abc123\""));
302
302
assert!(json.contains("\"validate\":true"));
303
303
}
···
306
306
fn test_delete_record_request_serialization() {
307
307
let request = DeleteRecordRequest {
308
308
repo: "did:plc:abc123".to_string(),
309
-
collection: "app.malfestio.deck".to_string(),
309
+
collection: "org.stormlightlabs.malfestio.deck".to_string(),
310
310
rkey: "3k5abc123".to_string(),
311
311
swap_record: None,
312
312
swap_commit: None,
+24
-18
crates/server/src/pds/records.rs
+24
-18
crates/server/src/pds/records.rs
···
82
82
/// Create a DeckRecord from an internal Deck model.
83
83
pub fn from_deck(deck: &Deck, card_at_uris: Vec<String>) -> Self {
84
84
Self {
85
-
record_type: "app.malfestio.deck".to_string(),
85
+
record_type: "org.stormlightlabs.malfestio.deck".to_string(),
86
86
title: deck.title.clone(),
87
87
description: if deck.description.is_empty() { None } else { Some(deck.description.clone()) },
88
88
tags: deck.tags.clone(),
···
98
98
/// Create a CardRecord from an internal Card model.
99
99
pub fn from_card(card: &Card, deck_at_uri: &str) -> Self {
100
100
Self {
101
-
record_type: "app.malfestio.card".to_string(),
101
+
record_type: "org.stormlightlabs.malfestio.card".to_string(),
102
102
deck_ref: deck_at_uri.to_string(),
103
103
front: card.front.clone(),
104
104
back: card.back.clone(),
···
117
117
/// Create a NoteRecord from an internal Note model.
118
118
pub fn from_note(note: &Note) -> Self {
119
119
Self {
120
-
record_type: "app.malfestio.note".to_string(),
120
+
record_type: "org.stormlightlabs.malfestio.note".to_string(),
121
121
title: note.title.clone(),
122
122
body: note.body.clone(),
123
123
tags: note.tags.clone(),
···
143
143
let record = DeckRecord::from_deck(deck, card_at_uris);
144
144
PreparedRecord {
145
145
rkey: generate_tid(),
146
-
collection: "app.malfestio.deck".to_string(),
146
+
collection: "org.stormlightlabs.malfestio.deck".to_string(),
147
147
record: serde_json::to_value(record).expect("Failed to serialize deck record"),
148
148
}
149
149
}
···
153
153
let record = CardRecord::from_card(card, deck_at_uri);
154
154
PreparedRecord {
155
155
rkey: generate_tid(),
156
-
collection: "app.malfestio.card".to_string(),
156
+
collection: "org.stormlightlabs.malfestio.card".to_string(),
157
157
record: serde_json::to_value(record).expect("Failed to serialize card record"),
158
158
}
159
159
}
···
163
163
let record = NoteRecord::from_note(note);
164
164
PreparedRecord {
165
165
rkey: generate_tid(),
166
-
collection: "app.malfestio.note".to_string(),
166
+
collection: "org.stormlightlabs.malfestio.note".to_string(),
167
167
record: serde_json::to_value(record).expect("Failed to serialize note record"),
168
168
}
169
169
}
···
221
221
let deck = sample_deck();
222
222
let record = DeckRecord::from_deck(&deck, vec![]);
223
223
224
-
assert_eq!(record.record_type, "app.malfestio.deck");
224
+
assert_eq!(record.record_type, "org.stormlightlabs.malfestio.deck");
225
225
assert_eq!(record.title, "Test Deck");
226
226
assert_eq!(record.description, Some("A test deck".to_string()));
227
227
assert_eq!(record.tags.len(), 2);
···
230
230
#[test]
231
231
fn test_deck_record_serialization() {
232
232
let deck = sample_deck();
233
-
let record = DeckRecord::from_deck(&deck, vec!["at://did:plc:abc/app.malfestio.card/tid1".to_string()]);
233
+
let record = DeckRecord::from_deck(
234
+
&deck,
235
+
vec!["at://did:plc:abc/org.stormlightlabs.malfestio.card/tid1".to_string()],
236
+
);
234
237
235
238
let json = serde_json::to_string(&record).unwrap();
236
-
assert!(json.contains("\"$type\":\"app.malfestio.deck\""));
239
+
assert!(json.contains("\"$type\":\"org.stormlightlabs.malfestio.deck\""));
237
240
assert!(json.contains("\"title\":\"Test Deck\""));
238
241
assert!(json.contains("cardRefs"));
239
242
}
···
241
244
#[test]
242
245
fn test_card_record_from_card() {
243
246
let card = sample_card();
244
-
let deck_uri = "at://did:plc:abc123/app.malfestio.deck/tid123";
247
+
let deck_uri = "at://did:plc:abc123/org.stormlightlabs.malfestio.deck/tid123";
245
248
let record = CardRecord::from_card(&card, deck_uri);
246
249
247
-
assert_eq!(record.record_type, "app.malfestio.card");
250
+
assert_eq!(record.record_type, "org.stormlightlabs.malfestio.card");
248
251
assert_eq!(record.deck_ref, deck_uri);
249
252
assert_eq!(record.front, "What is the capital of France?");
250
253
assert_eq!(record.back, "Paris");
···
255
258
let note = sample_note();
256
259
let record = NoteRecord::from_note(¬e);
257
260
258
-
assert_eq!(record.record_type, "app.malfestio.note");
261
+
assert_eq!(record.record_type, "org.stormlightlabs.malfestio.note");
259
262
assert_eq!(record.title, "Test Note");
260
263
assert_eq!(record.visibility, "public");
261
264
}
···
265
268
let deck = sample_deck();
266
269
let prepared = prepare_deck_record(&deck, vec![]);
267
270
268
-
assert_eq!(prepared.collection, "app.malfestio.deck");
271
+
assert_eq!(prepared.collection, "org.stormlightlabs.malfestio.deck");
269
272
assert_eq!(prepared.rkey.len(), 13); // TID length
270
273
assert!(prepared.record.is_object());
271
274
}
···
273
276
#[test]
274
277
fn test_prepare_card_record() {
275
278
let card = sample_card();
276
-
let prepared = prepare_card_record(&card, "at://did:plc:abc/app.malfestio.deck/tid");
279
+
let prepared = prepare_card_record(&card, "at://did:plc:abc/org.stormlightlabs.malfestio.deck/tid");
277
280
278
-
assert_eq!(prepared.collection, "app.malfestio.card");
281
+
assert_eq!(prepared.collection, "org.stormlightlabs.malfestio.card");
279
282
assert_eq!(prepared.rkey.len(), 13);
280
283
}
281
284
···
284
287
let note = sample_note();
285
288
let prepared = prepare_note_record(¬e);
286
289
287
-
assert_eq!(prepared.collection, "app.malfestio.note");
290
+
assert_eq!(prepared.collection, "org.stormlightlabs.malfestio.note");
288
291
assert_eq!(prepared.rkey.len(), 13);
289
292
}
290
293
291
294
#[test]
292
295
fn test_make_at_uri() {
293
-
let uri = make_at_uri("did:plc:abc123", "app.malfestio.deck", "3k5abc123");
294
-
assert_eq!(uri.to_string(), "at://did:plc:abc123/app.malfestio.deck/3k5abc123");
296
+
let uri = make_at_uri("did:plc:abc123", "org.stormlightlabs.malfestio.deck", "3k5abc123");
297
+
assert_eq!(
298
+
uri.to_string(),
299
+
"at://did:plc:abc123/org.stormlightlabs.malfestio.deck/3k5abc123"
300
+
);
295
301
}
296
302
297
303
#[test]
+69
lexicons/README.md
+69
lexicons/README.md
···
32
32
- **Private layer**:
33
33
- review schedule, lapses, grades, per-card performance, streaks
34
34
35
+
## Publishing Lexicons to AT Protocol Network
36
+
37
+
### Prerequisites
38
+
39
+
**Goat CLI**: Install the official AT Protocol CLI tool
40
+
41
+
```bash
42
+
# macOS
43
+
brew install goat
44
+
```
45
+
46
+
### Publishing Workflow
47
+
48
+
1. **Validate schemas locally**:
49
+
50
+
```bash
51
+
goat lexicon lint lexicons/
52
+
```
53
+
54
+
2. **Check DNS configuration**:
55
+
56
+
```bash
57
+
goat lexicon check-dns org.stormlightlabs.malfestio.card
58
+
```
59
+
60
+
3. **Publish to network**:
61
+
62
+
```bash
63
+
goat lexicon publish lexicons/org/stormlightlabs/malfestio/
64
+
```
65
+
66
+
### PDS Validation Modes
67
+
68
+
AT Protocol PDSs support three lexicon validation modes:
69
+
70
+
1. **Explicit validation required**: Record must validate against schema; fails if PDS doesn't know the lexicon
71
+
- This is the current mode causing `Lexicon not found` errors
72
+
- Requires publishing lexicons or using optimistic validation
73
+
74
+
2. **Optimistic validation** (default): Validates if PDS knows the schema, allows creation if unknown
75
+
- Most flexible for custom lexicons during development
76
+
- Set via `validate: undefined` in create/update record calls
77
+
78
+
3. **Explicit no validation**: Skips validation even if PDS knows the schema
79
+
- Set via `validate: false` in create/update record calls
80
+
81
+
### Version Updates
82
+
83
+
When updating lexicon schemas:
84
+
85
+
1. **Minor Updates** (additive only):
86
+
- Add new optional fields
87
+
- Update descriptions
88
+
- Add new `knownValues` (don't remove old ones)
89
+
- Increment patch version in documentation
90
+
91
+
2. **Breaking Changes** (avoid if possible):
92
+
- Create new lexicon with new NSID (e.g., `org.stormlightlabs.malfestio.cardV2`)
93
+
- Maintain both versions during migration period
94
+
- Update code to support both old and new schemas
95
+
- Document migration path
96
+
97
+
3. **Republishing**:
98
+
99
+
```bash
100
+
goat lexicon lint lexicons/
101
+
goat lexicon publish lexicons/org/stormlightlabs/malfestio/
102
+
```
103
+
35
104
## Evolution Rules
36
105
37
106
1. **Additive Changes Only**: You can add new optional fields to existing records.
+1
-1
web/src/components/NoteCard.tsx
+1
-1
web/src/components/NoteCard.tsx
+34
-29
web/src/components/NoteEditor.tsx
+34
-29
web/src/components/NoteEditor.tsx
···
5
5
import { toast } from "$lib/toast";
6
6
import { Button } from "$ui/Button";
7
7
import rehypeShiki from "@shikijs/rehype";
8
+
import { useNavigate } from "@solidjs/router";
8
9
import { Textcomplete } from "@textcomplete/core";
9
10
import { TextareaEditor } from "@textcomplete/textarea";
10
11
import rehypeExternalLinks from "rehype-external-links";
···
20
21
21
22
type EditorTab = "write" | "preview";
22
23
24
+
function getFontName(font: EditorFont | (() => EditorFont)) {
25
+
switch (typeof font === "function" ? font() : font) {
26
+
case "neon":
27
+
return "Monaspace Neon";
28
+
case "argon":
29
+
return "Monaspace Argon";
30
+
case "krypton":
31
+
return "Monaspace Krypton";
32
+
case "radon":
33
+
return "Monaspace Radon";
34
+
case "xenon":
35
+
return "Monaspace Xenon";
36
+
case "google":
37
+
return "Google Sans Code";
38
+
default:
39
+
return "JetBrains Mono";
40
+
}
41
+
}
42
+
23
43
const processor = unified().use(remarkParse).use(remarkRehype).use(rehypeShiki, { theme: "vitesse-dark" }).use(
24
44
rehypeExternalLinks,
25
45
{ target: "_blank", rel: ["nofollow"] },
26
46
).use(rehypeStringify);
27
47
28
48
export function NoteEditor(props: NoteEditorProps) {
49
+
const navigate = useNavigate();
29
50
const [title, setTitle] = createSignal(props.initialTitle || "");
30
51
const [content, setContent] = createSignal(props.initialContent || "");
31
52
const [preview, setPreview] = createSignal("");
···
76
97
textcomplete?.destroy();
77
98
});
78
99
79
-
const fontValue = createMemo(() => {
80
-
switch (editorFont()) {
81
-
case "neon":
82
-
return "Monaspace Neon";
83
-
case "argon":
84
-
return "Monaspace Argon";
85
-
case "krypton":
86
-
return "Monaspace Krypton";
87
-
case "radon":
88
-
return "Monaspace Radon";
89
-
case "xenon":
90
-
return "Monaspace Xenon";
91
-
case "google":
92
-
return "Google Sans Code";
93
-
default:
94
-
return "JetBrains Mono";
95
-
}
96
-
});
100
+
const fontValue = createMemo(() => getFontName(editorFont));
97
101
98
102
const insertAtCursor = (before: string, after: string = "") => {
99
103
if (!textareaRef) return;
···
116
120
const handleCodeBlock = () => insertAtCursor("```\n", "\n```");
117
121
const handleWikilink = () => insertAtCursor("[[", "]]");
118
122
const handleList = () => insertAtCursor("- ");
119
-
120
-
const handleHeading = (level: 1 | 2 | 3 | 4 | 5 | 6) => {
121
-
const prefix = "#".repeat(level) + " ";
122
-
insertAtCursor(prefix);
123
-
};
123
+
const handleHeading = (level: 1 | 2 | 3 | 4 | 5 | 6) => insertAtCursor("#".repeat(level) + " ");
124
124
125
125
const handleKeyDown = (e: KeyboardEvent) => {
126
126
if (e.metaKey || e.ctrlKey) {
···
162
162
163
163
if (res.ok) {
164
164
toast.success("Note saved!");
165
-
if (!props.noteId) {
166
-
setTitle("");
167
-
setContent("");
168
-
setTags("");
169
-
setVisibilityType("Private");
170
-
setSharedWith("");
165
+
if (props.noteId) {
166
+
navigate(`/notes/${props.noteId}`);
167
+
} else {
168
+
try {
169
+
const newNote = await res.json();
170
+
navigate(`/notes/${newNote.id}`);
171
+
} catch {
172
+
navigate("/notes");
173
+
}
171
174
}
172
175
} else {
176
+
const errorText = await res.text();
177
+
console.error("Failed to save note:", res.status, errorText);
173
178
toast.error("Failed to save note");
174
179
}
175
180
} catch (e) {
+3
-2
web/src/lib/model.ts
+3
-2
web/src/lib/model.ts
+10
-3
web/src/pages/Notes.tsx
+10
-3
web/src/pages/Notes.tsx
···
3
3
import { EmptyState } from "$components/ui/EmptyState";
4
4
import { api } from "$lib/api";
5
5
import type { Note } from "$lib/model";
6
-
import { A } from "@solidjs/router";
6
+
import { A, useLocation } from "@solidjs/router";
7
7
import type { Component } from "solid-js";
8
-
import { createMemo, createResource, createSignal, For, Show } from "solid-js";
8
+
import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js";
9
9
10
10
const fetchNotes = async (): Promise<Note[]> => {
11
11
const res = await api.getNotes();
···
16
16
type ViewMode = "grid" | "list";
17
17
18
18
const Notes: Component = () => {
19
-
const [notes] = createResource(fetchNotes);
19
+
const location = useLocation();
20
+
const [notes, { refetch }] = createResource(fetchNotes);
20
21
const [viewMode, setViewMode] = createSignal<ViewMode>("grid");
21
22
const [searchQuery, setSearchQuery] = createSignal("");
23
+
24
+
createEffect(() => {
25
+
if (location.pathname === "/notes") {
26
+
refetch();
27
+
}
28
+
});
22
29
23
30
const filteredNotes = createMemo(() => {
24
31
const allNotes = notes() || [];