add kirby-loop plugin with French translations
All checks were successful
Deploy / Deploy to Production (push) Successful in 6s
All checks were successful
Deploy / Deploy to Production (push) Successful in 6s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ea5f0c462
commit
ab7fd8b2ea
74 changed files with 16423 additions and 2 deletions
111
site/plugins/loop/frontend/src/store/api.svelte.ts
Normal file
111
site/plugins/loop/frontend/src/store/api.svelte.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import type { Comment, CommentPayload, Reply, ReplyPayload } from '../types';
|
||||
|
||||
export const store: { comments: Comment[] } = $state({
|
||||
comments: []
|
||||
});
|
||||
|
||||
const apiPrefix = 'loop';
|
||||
const KirbyLoop = document.querySelector('kirby-loop');
|
||||
const csrfToken = KirbyLoop?.getAttribute('csrf-token') || '';
|
||||
const apiBase = KirbyLoop?.getAttribute('apibase') || '/';
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken || ''
|
||||
};
|
||||
|
||||
const buildApiUrl = (endpoint: string): string => {
|
||||
const url = new URL(`${apiBase}/${apiPrefix}/${endpoint}`, window.location.origin);
|
||||
|
||||
// Add token query params from current page if they exist
|
||||
const currentParams = new URLSearchParams(window.location.search);
|
||||
const token = currentParams.get('token') || currentParams.get('_token');
|
||||
if (token) {
|
||||
url.searchParams.set(currentParams.has('token') ? 'token' : '_token', token);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export const getComments = async (pageId: string): Promise<boolean> => {
|
||||
const url = buildApiUrl(`comments/${pageId}`);
|
||||
const response = await fetch(url, {
|
||||
headers
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.status === 'ok') {
|
||||
store.comments = data.comments;
|
||||
}
|
||||
return data.status === 'ok';
|
||||
}
|
||||
|
||||
export const addComment = async (comment: CommentPayload) => {
|
||||
const url = buildApiUrl('comment/new');
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(comment)
|
||||
});
|
||||
const data: { comment: Comment, status: string } = await response.json();
|
||||
if (data.status === 'ok') {
|
||||
store.comments = [data.comment, ...store.comments];
|
||||
}
|
||||
}
|
||||
|
||||
export const resolveComment = async (comment: Comment) => {
|
||||
const url = buildApiUrl('comment/resolve');
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ id: comment.id })
|
||||
});
|
||||
const data: { success: boolean } = await response.json();
|
||||
if (data.success) {
|
||||
const commentIndex = store.comments.findIndex(c => c.id === comment.id);
|
||||
if (commentIndex !== -1) {
|
||||
store.comments[commentIndex].status = 'RESOLVED';
|
||||
}
|
||||
}
|
||||
return data.success;
|
||||
}
|
||||
|
||||
export const unresolveComment = async (comment: Comment) => {
|
||||
const url = buildApiUrl('comment/unresolve');
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ id: comment.id })
|
||||
});
|
||||
const data: { success: boolean } = await response.json();
|
||||
if (data.success) {
|
||||
const commentIndex = store.comments.findIndex(c => c.id === comment.id);
|
||||
if (commentIndex !== -1) {
|
||||
store.comments[commentIndex].status = 'OPEN';
|
||||
}
|
||||
}
|
||||
return data.success;
|
||||
}
|
||||
|
||||
export const setGuestName = async (name: string) => {
|
||||
const response = await fetch(buildApiUrl('guest/name'), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export const addReply = async (reply: ReplyPayload) => {
|
||||
const url = buildApiUrl('comment/reply');
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(reply)
|
||||
});
|
||||
const data: { reply: Reply, status: string } = await response.json();
|
||||
if (data.status === 'ok') {
|
||||
const parent = store.comments.find(c => c.id === data.reply.parentId)
|
||||
if (parent) parent.replies = [...parent.replies, data.reply];
|
||||
}
|
||||
}
|
||||
|
||||
export default store;
|
||||
11
site/plugins/loop/frontend/src/store/form.svelte.ts
Normal file
11
site/plugins/loop/frontend/src/store/form.svelte.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type { FormData } from '../types';
|
||||
|
||||
export const formData: FormData = $state({
|
||||
text: "",
|
||||
parentId: null
|
||||
});
|
||||
|
||||
export const reset = () => {
|
||||
formData.text = ""
|
||||
formData.parentId = null
|
||||
}
|
||||
19
site/plugins/loop/frontend/src/store/translations.svelte.ts
Normal file
19
site/plugins/loop/frontend/src/store/translations.svelte.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
let translations = $state<Record<string, string>>({});
|
||||
|
||||
export const t = (key: string, fallback?: string): string => {
|
||||
return translations[key] || fallback || key;
|
||||
};
|
||||
|
||||
export const tt = (key: string, fallback: string, replacements: Record<string, string>): string => {
|
||||
let text = translations[key] || fallback || key;
|
||||
|
||||
for (const [placeholder, value] of Object.entries(replacements)) {
|
||||
text = text.replace(`{${placeholder}}`, value);
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
export const setTranslations = (newTranslations: Record<string, string>) => {
|
||||
translations = newTranslations;
|
||||
};
|
||||
34
site/plugins/loop/frontend/src/store/ui.svelte.ts
Normal file
34
site/plugins/loop/frontend/src/store/ui.svelte.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
export const panel = $state({
|
||||
open: false,
|
||||
currentCommentId: 0,
|
||||
showResolvedOnly: false,
|
||||
pulseMarkerId: 0
|
||||
});
|
||||
export const overlay = $state({ open: false });
|
||||
|
||||
// Guest name management
|
||||
let guestNameValue = $state("");
|
||||
|
||||
export const guestName = {
|
||||
get value() {
|
||||
return guestNameValue;
|
||||
},
|
||||
set(name: string) {
|
||||
guestNameValue = name;
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem('loop-guest-name', name);
|
||||
}
|
||||
},
|
||||
get() {
|
||||
if (!guestNameValue && typeof window !== 'undefined') {
|
||||
guestNameValue = sessionStorage.getItem('loop-guest-name') || "";
|
||||
}
|
||||
return guestNameValue;
|
||||
},
|
||||
clear() {
|
||||
guestNameValue = "";
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.removeItem('loop-guest-name');
|
||||
}
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue