Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ jobs:
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy build --project-name=ramolibrelab --branch=main
command: pages deploy --project-name=ramolibrelab --branch=main
28 changes: 28 additions & 0 deletions functions/api/lab/[id].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const TTL_SECONDS = 60 * 60 * 24 * 180; // 180 días

export async function onRequestGet(context) {
const { params, env } = context;
const id = params.id;

if (!id || !/^[A-Za-z0-9]{4,12}$/.test(id)) {
return Response.json({ error: 'invalid_id' }, { status: 400 });
}

const raw = await env.LAB_KV.get(id);
if (raw === null) {
return Response.json({ error: 'not_found' }, { status: 404 });
}

const response = new Response(raw, {
status: 200,
headers: { 'content-type': 'application/json' }
});

context.waitUntil(
env.LAB_KV.put(id, raw, { expirationTtl: TTL_SECONDS }).catch((err) => {
console.error('[lab] TTL renewal failed', err);
})
);

return response;
}
52 changes: 52 additions & 0 deletions functions/api/lab/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';
const ID_LENGTH = 6;
const MAX_PAYLOAD_BYTES = 100_000;
const TTL_SECONDS = 60 * 60 * 24 * 180; // 180 días

function generateId() {
const bytes = new Uint8Array(ID_LENGTH);
crypto.getRandomValues(bytes);
let id = '';
for (let i = 0; i < ID_LENGTH; i++) {
id += ALPHABET[bytes[i] % ALPHABET.length];
}
return id;
}

export async function onRequestPost(context) {
const { request, env } = context;

let payload;
try {
payload = await request.json();
} catch {
return Response.json({ error: 'invalid_json' }, { status: 400 });
}

const serialized = JSON.stringify(payload);
if (serialized.length > MAX_PAYLOAD_BYTES) {
return Response.json({ error: 'payload_too_large' }, { status: 413 });
}

let id;
for (let attempt = 0; attempt < 3; attempt++) {
const candidate = generateId();
const existing = await env.LAB_KV.get(candidate);
if (!existing) {
id = candidate;
break;
}
}

if (!id) {
return Response.json({ error: 'id_generation_failed' }, { status: 500 });
}

try {
await env.LAB_KV.put(id, serialized, { expirationTtl: TTL_SECONDS });
} catch {
return Response.json({ error: 'kv_write_failed' }, { status: 507 });
}

return Response.json({ id }, { status: 201 });
}
51 changes: 51 additions & 0 deletions src/lib/share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { decodeUrlData, encodeUrlData } from '$lib/utils/url_data';
import type { Simulacion } from '$lib/state/simulaciones.svelte';

export async function shareLab(labData: unknown): Promise<string> {
const origin = window.location.origin;

try {
const res = await fetch('/api/lab', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(labData)
});

if (!res.ok) throw new Error(`status ${res.status}`);

const { id } = (await res.json()) as { id: string };
return `${origin}/?s=${id}`;
} catch (err) {
console.warn('[share] fallback a Base64 por fallo de red/KV', err);
const payload = encodeUrlData(labData);
return `${origin}/?share=${payload}`;
}
}

export async function hydrateFromUrl(): Promise<Simulacion | null> {
const params = new URLSearchParams(window.location.search);

const shortId = params.get('s');
if (shortId) {
try {
const res = await fetch(`/api/lab/${shortId}`);
if (res.ok) {
return (await res.json()) as Simulacion;
}
} catch (err) {
console.warn('[share] error hidratando desde enlace corto', err);
}
return null;
}

const shared = params.get('share');
if (shared) {
try {
return decodeUrlData<Simulacion>(shared);
} catch (err) {
console.warn('[share] enlace share inválido', err);
}
}

return null;
}
28 changes: 7 additions & 21 deletions src/routes/(app)/_components/Sidebar.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<script lang="ts">
import { SuiteFavicons } from '@ramo-libre/ui-themes';
import { VERSION } from '$lib/utils/version';
import { decodeUrlData, encodeUrlData } from '$lib/utils/url_data';
import type { Simulacion } from '$lib/state/simulaciones.svelte';
import { shareLab, hydrateFromUrl } from '$lib/share';
import { db } from '$lib/state/index.svelte';
import { onMount } from 'svelte';
import { Link2, FolderOpen, Settings, Check } from '@lucide/svelte'; // Importamos Check
Expand All @@ -18,29 +17,16 @@
// Runa para controlar la microinteracción de copiado
let copied = $state(false);

onMount(() => {
const url = new URL(window.location.href);
const payload = url.searchParams.get('share');
if (!payload) return;
try {
const shared = decodeUrlData<Simulacion>(payload);
if (shared && shared.id) {
db.simulaciones.loadActual(shared);
}
} catch (error) {
console.warn('Sidebar: invalid share payload', error);
}
onMount(async () => {
const lab = await hydrateFromUrl();
if (lab) db.simulaciones.loadActual(lab);
});

function handleShareLink() {
// Si ya está en estado animado, evitamos clicks redundantes
async function handleShareLink() {
if (copied) return;

const url = new URL(window.location.href);
const payload = encodeUrlData(db.simulaciones.actual);
url.searchParams.set('share', payload);

navigator.clipboard.writeText(url.toString()).then(() => {
const url = await shareLab(db.simulaciones.actual);
navigator.clipboard.writeText(url).then(() => {
copied = true;

// Retornar al estado original tras 2 segundos
Expand Down
8 changes: 8 additions & 0 deletions wrangler.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name = "ramolibrelab"
compatibility_date = "2026-07-23"
pages_build_output_dir = "build"

[[kv_namespaces]]
binding = "LAB_KV"
id = "f2f5863c15c84287bf3221a857f5f3b1"
preview_id = "404143a88e464c519370c47aa65f7827"
Loading