feat: integrate uload and picture, unify package naming
- Add uload project with apps/web structure
- Reorganize from flat to monorepo structure
- Remove PocketBase binary and local data
- Update to pnpm and @uload/web namespace
- Add picture project to monorepo
- Remove embedded git repository
- Unify all package names to @{project}/{app} schema:
- @maerchenzauber/* (was @storyteller/*)
- @manacore/* (was manacore-*, manacore)
- @manadeck/* (was web, backend, manadeck)
- @memoro/* (was memoro-web, landing, memoro)
- @picture/* (already unified)
- @uload/web
- Add convenient dev scripts for all apps:
- pnpm dev:{project}:web
- pnpm dev:{project}:landing
- pnpm dev:{project}:mobile
- pnpm dev:{project}:backend
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
4
uload/apps/web/static/icons/apple-touch-icon.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="180" height="180" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="180" height="180" fill="#3B82F6" rx="27"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="90px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 333 B |
4
uload/apps/web/static/icons/icon-128x128.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="128" height="128" fill="#3B82F6" rx="19.2"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="64px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 335 B |
4
uload/apps/web/static/icons/icon-144x144.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" fill="#3B82F6" rx="21.599999999999998"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="72px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 349 B |
4
uload/apps/web/static/icons/icon-152x152.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="152" height="152" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="152" height="152" fill="#3B82F6" rx="22.8"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="76px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 335 B |
4
uload/apps/web/static/icons/icon-192x192.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="192" height="192" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="192" height="192" fill="#3B82F6" rx="28.799999999999997"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="96px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 349 B |
4
uload/apps/web/static/icons/icon-384x384.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="384" height="384" viewBox="0 0 384 384" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="384" height="384" fill="#3B82F6" rx="57.599999999999994"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="192px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 350 B |
4
uload/apps/web/static/icons/icon-512x512.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" fill="#3B82F6" rx="76.8"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="256px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 336 B |
4
uload/apps/web/static/icons/icon-72x72.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="72" height="72" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="72" height="72" fill="#3B82F6" rx="10.799999999999999"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="36px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 343 B |
4
uload/apps/web/static/icons/icon-96x96.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="96" height="96" fill="#3B82F6" rx="14.399999999999999"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="48px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 343 B |
5
uload/apps/web/static/icons/icon-maskable-192x192.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="192" height="192" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="192" height="192" fill="#3B82F6"/>
|
||||
<rect x="19.19999999999999" y="19.19999999999999" width="153.60000000000002" height="153.60000000000002" fill="#3B82F6" rx="23.040000000000003"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="76.80000000000001px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 488 B |
5
uload/apps/web/static/icons/icon-maskable-512x512.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" fill="#3B82F6"/>
|
||||
<rect x="51.19999999999999" y="51.19999999999999" width="409.6" height="409.6" fill="#3B82F6" rx="61.44"/>
|
||||
<text x="50%" y="55%" font-family="system-ui, -apple-system, sans-serif" font-size="204.8px" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">U</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 437 B |
132
uload/apps/web/static/manifest.json
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
{
|
||||
"name": "uLoad - URL Shortener & Link Management",
|
||||
"short_name": "uLoad",
|
||||
"description": "Professional URL shortener with analytics, QR codes, and link management",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#1e1b4b",
|
||||
"theme_color": "#3b82f6",
|
||||
"orientation": "portrait-primary",
|
||||
"scope": "/",
|
||||
"categories": ["productivity", "utilities", "business"],
|
||||
"lang": "en",
|
||||
"dir": "ltr",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-72x72.svg",
|
||||
"sizes": "72x72",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-96x96.svg",
|
||||
"sizes": "96x96",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-128x128.svg",
|
||||
"sizes": "128x128",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-144x144.svg",
|
||||
"sizes": "144x144",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-152x152.svg",
|
||||
"sizes": "152x152",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-192x192.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-384x384.svg",
|
||||
"sizes": "384x384",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512x512.svg",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-maskable-192x192.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-maskable-512x512.svg",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Create Link",
|
||||
"short_name": "New Link",
|
||||
"description": "Create a new short link",
|
||||
"url": "/my",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192x192.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Analytics",
|
||||
"short_name": "Stats",
|
||||
"description": "View link analytics",
|
||||
"url": "/my/links",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192x192.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "QR Codes",
|
||||
"short_name": "QR",
|
||||
"description": "Generate QR codes",
|
||||
"url": "/my/links",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192x192.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"related_applications": [],
|
||||
"prefer_related_applications": false,
|
||||
"share_target": {
|
||||
"action": "/my",
|
||||
"method": "GET",
|
||||
"params": {
|
||||
"url": "url"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
uload/apps/web/static/robots.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
242
uload/apps/web/static/sw.js
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
// uLoad Service Worker für PWA-Funktionalität
|
||||
const CACHE_NAME = 'uload-v1';
|
||||
const OFFLINE_URL = '/offline';
|
||||
|
||||
// Assets die gecacht werden sollen
|
||||
const CACHE_ASSETS = [
|
||||
'/',
|
||||
'/my',
|
||||
'/offline',
|
||||
'/manifest.json'
|
||||
];
|
||||
|
||||
// Install Event - Cache initialisieren
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('Service Worker: Installing');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
console.log('Service Worker: Caching assets');
|
||||
return cache.addAll(CACHE_ASSETS);
|
||||
})
|
||||
.then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
// Activate Event - Alte Caches löschen
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('Service Worker: Activating');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
console.log('Service Worker: Deleting old cache', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
}).then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch Event - Network-first mit Cache-Fallback
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// Nur GET Requests handhaben
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
// Spezielle Behandlung für Navigation Requests
|
||||
if (event.request.mode === 'navigate') {
|
||||
event.respondWith(handleNavigate(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// API Requests - Network-first
|
||||
if (event.request.url.includes('/api/')) {
|
||||
event.respondWith(handleApiRequest(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Static Assets - Cache-first
|
||||
if (isStaticAsset(event.request.url)) {
|
||||
event.respondWith(handleStaticAsset(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: Network-first
|
||||
event.respondWith(handleDefault(event.request));
|
||||
});
|
||||
|
||||
// Navigation Requests handhaben
|
||||
async function handleNavigate(request) {
|
||||
try {
|
||||
// Versuche Network Request
|
||||
const response = await fetch(request);
|
||||
|
||||
// Bei Erfolg: Response cachen und zurückgeben
|
||||
if (response.status === 200) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Bei Netzwerk-Fehler: Cache prüfen
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Offline-Seite anzeigen
|
||||
return caches.match(OFFLINE_URL);
|
||||
}
|
||||
}
|
||||
|
||||
// API Requests handhaben
|
||||
async function handleApiRequest(request) {
|
||||
try {
|
||||
// API Requests immer vom Netzwerk
|
||||
const response = await fetch(request);
|
||||
|
||||
// Bei Erfolg: kurzzeitig cachen (für Read-only Endpoints)
|
||||
if (response.status === 200 && request.method === 'GET') {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
// Clone erstellen da Response nur einmal gelesen werden kann
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Bei Offline: gecachte Response zurückgeben (falls vorhanden)
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Offline-Antwort für API
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Offline - Please try again when online' }),
|
||||
{
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Static Assets handhaben
|
||||
async function handleStaticAsset(request) {
|
||||
// Cache-first für statische Assets
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
|
||||
// Bei Erfolg: Cache aktualisieren
|
||||
if (response.status === 200) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Bei Offline und nicht im Cache: Default-Response
|
||||
return new Response('Asset not available offline', { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
// Default Request handhaben
|
||||
async function handleDefault(request) {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
return response;
|
||||
} catch (error) {
|
||||
const cachedResponse = await caches.match(request);
|
||||
return cachedResponse || new Response('Offline', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfen ob URL ein statisches Asset ist
|
||||
function isStaticAsset(url) {
|
||||
const staticExtensions = ['.css', '.js', '.png', '.jpg', '.jpeg', '.svg', '.ico', '.woff', '.woff2'];
|
||||
return staticExtensions.some(ext => url.includes(ext));
|
||||
}
|
||||
|
||||
// Background Sync für Offline-Aktionen
|
||||
self.addEventListener('sync', (event) => {
|
||||
if (event.tag === 'background-sync') {
|
||||
event.waitUntil(handleBackgroundSync());
|
||||
}
|
||||
});
|
||||
|
||||
async function handleBackgroundSync() {
|
||||
// Hier können Offline-Aktionen synchronisiert werden
|
||||
console.log('Service Worker: Background sync triggered');
|
||||
|
||||
// Beispiel: Gespeicherte Links hochladen
|
||||
try {
|
||||
const pendingLinks = await getPendingLinks();
|
||||
for (const link of pendingLinks) {
|
||||
await syncLink(link);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Background sync failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Push Notifications (für zukünftige Features)
|
||||
self.addEventListener('push', (event) => {
|
||||
if (event.data) {
|
||||
const data = event.data.json();
|
||||
|
||||
const options = {
|
||||
body: data.body,
|
||||
icon: '/icons/icon-192x192.png',
|
||||
badge: '/icons/icon-72x72.png',
|
||||
tag: 'uload-notification',
|
||||
renotify: true,
|
||||
actions: [
|
||||
{
|
||||
action: 'view',
|
||||
title: 'View'
|
||||
},
|
||||
{
|
||||
action: 'dismiss',
|
||||
title: 'Dismiss'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title || 'uLoad', options)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Notification Click Event
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'view') {
|
||||
event.waitUntil(
|
||||
clients.openWindow('/')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper Funktionen für IndexedDB (für Offline-Speicher)
|
||||
function getPendingLinks() {
|
||||
// Implementierung für IndexedDB
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
function syncLink(link) {
|
||||
// Implementierung für Link-Synchronisation
|
||||
return Promise.resolve();
|
||||
}
|
||||