Initial commit: snakkimo CMT
React + Vite + Tailwind dashboard with: - Login (JWT via snakkimo auth) - Dashboard with Datenbankverwaltung + Contentverwaltung tiles - Table overview with record counts (total, published, blocked) - Table record viewer with text/status filters and linked field navigation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
56
src/lib/api.js
Normal file
56
src/lib/api.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const BASE = (import.meta.env.VITE_API_BASE || 'https://hyggecraftery.com/api/snakkimo').replace(/\/$/, '');
|
||||
|
||||
export const AUTH_URL = `${BASE}/auth/login`;
|
||||
export const API_URL = `${BASE}/api`;
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem('cmt_token');
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
localStorage.removeItem('cmt_token');
|
||||
localStorage.removeItem('cmt_user');
|
||||
}
|
||||
|
||||
export async function login(email, password) {
|
||||
const res = await fetch(AUTH_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Login fehlgeschlagen');
|
||||
}
|
||||
const data = await res.json();
|
||||
localStorage.setItem('cmt_token', data.token);
|
||||
localStorage.setItem('cmt_user', JSON.stringify(data.user));
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
try { return JSON.parse(localStorage.getItem('cmt_user')); } catch { return null; }
|
||||
}
|
||||
|
||||
export async function apiFetch(path, options = {}) {
|
||||
const token = getToken();
|
||||
const res = await fetch(`${API_URL}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
if (res.status === 401) { logout(); window.location.href = '/login'; return; }
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `HTTP ${res.status}`);
|
||||
}
|
||||
if (res.status === 204) return null;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchAll(endpoint) {
|
||||
return apiFetch(`${endpoint}?limit=500&offset=0`);
|
||||
}
|
||||
75
src/lib/tables.js
Normal file
75
src/lib/tables.js
Normal file
@@ -0,0 +1,75 @@
|
||||
export const TABLES = {
|
||||
words: {
|
||||
label: 'Wörter',
|
||||
icon: '📝',
|
||||
endpoint: '/words',
|
||||
statusField: 'status',
|
||||
primaryLabel: 'titel_de',
|
||||
columns: ['titel_de', 'titel_en', 'status', 'difficulty_level', 'created_at'],
|
||||
linkedFields: { picture_ids: 'pictures', category_ids: 'categories' },
|
||||
},
|
||||
pictures: {
|
||||
label: 'Bilder',
|
||||
icon: '🖼️',
|
||||
endpoint: '/pictures',
|
||||
statusField: 'status',
|
||||
primaryLabel: 'design',
|
||||
columns: ['design', 'status', 'picture_link', 'blurhash', 'created_at'],
|
||||
linkedFields: {},
|
||||
},
|
||||
objects: {
|
||||
label: 'Objekte',
|
||||
icon: '📦',
|
||||
endpoint: '/objects',
|
||||
statusField: 'status',
|
||||
primaryLabel: 'notes',
|
||||
columns: ['id', 'status', 'notes', 'created_at'],
|
||||
linkedFields: { word_ids: 'words', picture_ids: 'pictures', pair_ids: 'pairs' },
|
||||
},
|
||||
pairs: {
|
||||
label: 'Pairs',
|
||||
icon: '🔗',
|
||||
endpoint: '/pairs',
|
||||
statusField: 'status',
|
||||
primaryLabel: 'id',
|
||||
columns: ['id', 'answer_type', 'difficulty_level', 'status', 'question_id', 'positive_statement_id', 'created_at'],
|
||||
linkedFields: { question_id: 'questions', positive_statement_id: 'statements', negative_statement_id: 'statements' },
|
||||
},
|
||||
questions: {
|
||||
label: 'Fragen',
|
||||
icon: '❓',
|
||||
endpoint: '/questions',
|
||||
statusField: 'status',
|
||||
primaryLabel: 'sentence_de',
|
||||
columns: ['sentence_de', 'sentence_en', 'status', 'created_at'],
|
||||
linkedFields: {},
|
||||
},
|
||||
statements: {
|
||||
label: 'Statements',
|
||||
icon: '💬',
|
||||
endpoint: '/statements',
|
||||
statusField: 'status',
|
||||
primaryLabel: 'positive_sentence_de',
|
||||
columns: ['positive_sentence_de', 'negative_sentence_de', 'status', 'created_at'],
|
||||
linkedFields: {},
|
||||
},
|
||||
categories: {
|
||||
label: 'Kategorien',
|
||||
icon: '🏷️',
|
||||
endpoint: '/categories',
|
||||
statusField: null,
|
||||
primaryLabel: 'name_de',
|
||||
columns: ['name_de', 'name_en', 'name_sv', 'created_at'],
|
||||
linkedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const STATUS_COLORS = {
|
||||
published: 'bg-green-100 text-green-800',
|
||||
blocked: 'bg-red-100 text-red-800',
|
||||
draft: 'bg-gray-100 text-gray-700',
|
||||
uploaded: 'bg-blue-100 text-blue-800',
|
||||
requested: 'bg-yellow-100 text-yellow-800',
|
||||
translated: 'bg-indigo-100 text-indigo-800',
|
||||
generated: 'bg-purple-100 text-purple-800',
|
||||
};
|
||||
Reference in New Issue
Block a user