srijeda, 11. novembar 2026. · Godina I · Broj 15 · Cijena: 0 dinara
JUGOSLOVENSKI FORUM

Glasilo YU mreže · NEMA GRANICA ZA ZNANJE

Prijava Upis
Pravo na uvid u kod. Otvoren protokol omogućava nezavisne operatere i interoperabilnost. Manifest

Izvor

4d7883a894b3

   1 import {esc} from './md.js';
   2 import {t,datum} from './jezik.js';
   3 import {GRAD} from './gradovi.js';
   4 import {sha} from './auth.js';
   5 const stripes = (cols, y = 0, h = 200) => cols.map((c, i) =>
   6   `<rect x="0" y="${y + (i * h) / cols.length}" width="300" height="${h / cols.length + 0.5}" fill="${c}"/>`).join('');
   7 
   8 const ART = {
   9   'signal-returns': () => `${stripes(['#f2b134', '#e8743b', '#d7263d', '#7a1f4f', '#1d1b4f'])}
  10 <circle cx="150" cy="200" r="70" fill="#f4ead2"/>
  11 ${[40, 70, 100, 130].map((r) => `<path d="M${150 - r} 120a${r} ${r} 0 0 1 ${2 * r} 0" fill="none" stroke="#f4ead2" stroke-width="6" stroke-linecap="round" opacity="${1 - r / 200}"/>`).join('')}
  12 <rect x="146" y="118" width="8" height="60" fill="#1d1b4f"/><circle cx="150" cy="116" r="9" fill="#1d1b4f"/>`,
  13   'hello-yu': () => `<rect width="300" height="200" fill="#0b1433"/>
  14 <rect x="30" y="30" width="240" height="140" rx="10" fill="#111d4a" stroke="#5ef2d6" stroke-width="3"/>
  15 <rect x="30" y="30" width="240" height="22" rx="10" fill="#5ef2d6"/><circle cx="46" cy="41" r="4" fill="#0b1433"/><circle cx="60" cy="41" r="4" fill="#0b1433"/>
  16 <text x="48" y="96" font-family="IBM Plex Mono, monospace" font-size="22" fill="#5ef2d6">&gt; hello.yu</text>
  17 <text x="48" y="126" font-family="IBM Plex Mono, monospace" font-size="12" fill="#f4ead2">HTTPS</text>
  18 <rect x="48" y="138" width="14" height="18" fill="#ffd166"></rect>`,
  19   'galeb-1981': () => `${stripes(['#1b9aaa', '#167d8c', '#0f5d6a'])}
  20 <path d="M40 70c30-30 60-30 90 0 30-30 60-30 90 0-30-12-60-10-90 18-30-28-60-30-90-18z" fill="#f4ead2"/>
  21 <rect x="40" y="118" width="220" height="56" rx="6" fill="#2b2b33"/><rect x="48" y="126" width="204" height="10" rx="2" fill="#d7263d"/>
  22 ${Array.from({ length: 24 }, (_, i) => `<rect x="${50 + (i % 12) * 17}" y="${142 + Math.floor(i / 12) * 14}" width="13" height="10" rx="2" fill="#e7e2d4"/>`).join('')}`,
  23   'orao-tribute': () => `<rect width="300" height="200" fill="#1d1b4f"/>
  24 ${Array.from({ length: 10 }, (_, i) => `<rect x="${40 + i * 22}" y="${60 + Math.abs(4.5 - i) * 16}" width="18" height="18" fill="${i % 2 ? '#ffd166' : '#f2b134'}"/>`).join('')}
  25 ${Array.from({ length: 6 }, (_, i) => `<rect x="${84 + i * 22}" y="${126 - Math.abs(2.5 - i) * 10}" width="18" height="18" fill="#e8743b"/>`).join('')}
  26 <rect x="141" y="150" width="18" height="30" fill="#f4ead2"/>`,
  27   'cassette-loader': () => `${stripes(['#f4ead2', '#efe0b8'])}
  28 <rect x="40" y="40" width="220" height="130" rx="10" fill="#2b2b33"/><rect x="58" y="56" width="184" height="60" rx="6" fill="#f2b134"/>
  29 <rect x="58" y="68" width="184" height="10" fill="#d7263d"/><circle cx="110" cy="96" r="14" fill="#2b2b33"/><circle cx="190" cy="96" r="14" fill="#2b2b33"/>
  30 <rect x="124" y="90" width="52" height="12" rx="3" fill="#f4ead2"/><path d="M80 170l14-26h112l14 26" fill="#3b3b46"/>
  31 <text x="150" y="64" text-anchor="middle" font-family="Oswald, sans-serif" font-size="9" fill="#2b2b33" letter-spacing="2">LOAD ""</text>`,
  32   'nema-granica': () => `<rect width="300" height="200" fill="#d7263d"/>
  33 ${Array.from({ length: 12 }, (_, i) => `<path d="M150 120L${150 + Math.cos(Math.PI + (i * Math.PI) / 11) * 220} ${120 + Math.sin(Math.PI + (i * Math.PI) / 11) * 220}" stroke="#f2b134" stroke-width="7" opacity=".55"/>`).join('')}
  34 <path d="M60 150c30-14 60-14 90 0 30-14 60-14 90 0v-56c-30-14-60-14-90 0-30-14-60-14-90 0z" fill="#f4ead2"/><path d="M150 94v56" stroke="#d7263d" stroke-width="3"/>
  35 <text x="150" y="186" text-anchor="middle" font-family="Oswald, sans-serif" font-size="13" fill="#f4ead2" letter-spacing="3">НЕМА ГРАНИЦА ЗА ЗНАЊЕ</text>`,
  36   'market-yu': () => `<rect width="300" height="200" fill="#f4ead2"/>
  37 ${Array.from({ length: 8 }, (_, i) => `<path d="M${20 + i * 32.5} 40h32.5v40a16.25 16.25 0 0 1-32.5 0z" fill="${i % 2 ? '#f4ead2' : '#1b9aaa'}" stroke="#0f5d6a" stroke-width="2"/>`).join('')}
  38 <rect x="30" y="96" width="240" height="80" fill="#e8743b"/><rect x="30" y="96" width="240" height="10" fill="#2b2b33"/>
  39 <circle cx="80" cy="140" r="16" fill="#d7263d"/><circle cx="120" cy="146" r="12" fill="#f2b134"/><rect x="160" y="124" width="34" height="34" rx="4" fill="#5ef2d6"/><path d="M210 158l20-34 20 34z" fill="#1d1b4f"/>`,
  40   adriatic: () => `${stripes(['#ffd166', '#f7b267', '#f79d65'], 0, 100)}
  41 <circle cx="210" cy="80" r="34" fill="#fff4d6"/><path d="M0 104c40-14 70 0 110-8s70-26 110-12 60 4 80 0v116H0z" fill="#1d6fa3"/>
  42 ${[118, 138, 158, 178].map((y, i) => `<path d="M0 ${y}q25 -10 50 0t50 0 50 0 50 0 50 0 50 0" fill="none" stroke="#9fe3ff" stroke-width="3" opacity="${0.9 - i * 0.18}"/>`).join('')}
  43 <path d="M30 104l30-26 22 16 18-10 20 20z" fill="#2f5a3b"/>`,
  44   mountains: () => `${stripes(['#2a1f5c', '#493585', '#7a5aa6'], 0, 120)}
  45 <circle cx="80" cy="56" r="20" fill="#f4ead2"/>
  46 <path d="M0 150l60-70 40 40 50-78 60 92 40-40 50 56v50H0z" fill="#1b9aaa"/><path d="M150 42l18 28-12-6-6 10-6-10-12 6z" fill="#f4ead2"/>
  47 <path d="M0 170l70-46 50 30 60-40 60 36 60-20v70H0z" fill="#0f5d6a"/>`,
  48   kafana: () => `${stripes(['#3b1f1a', '#4a2820'])}
  49 ${Array.from({ length: 6 }, (_, i) => `<rect x="${i * 50}" y="0" width="25" height="200" fill="#f4ead2" opacity=".05"/>`).join('')}
  50 <path d="M110 90h70l-8 70h-54z" fill="#c9a227"/><path d="M180 104h14a10 10 0 0 1 0 20h-18" fill="none" stroke="#c9a227" stroke-width="7"/>
  51 <path d="M100 160h90" stroke="#f4ead2" stroke-width="6" stroke-linecap="round"/>
  52 <path d="M130 80c-8-12 8-16 0-30M150 80c-8-12 8-16 0-30M170 80c-8-12 8-16 0-30" fill="none" stroke="#f4ead2" stroke-width="3" opacity=".7"/>`,
  53   steward: () => `<rect width="300" height="200" fill="#0b1433"/>
  54 <path d="M60 70l90-40 90 40z" fill="#ffd166"/>
  55 ${[78, 112, 146, 180, 214].map((x) => `<rect x="${x}" y="78" width="10" height="80" fill="#f4ead2"/>`).join('')}
  56 <rect x="54" y="158" width="192" height="12" fill="#ffd166"/><rect x="40" y="172" width="220" height="10" fill="#f2b134"/>
  57 <rect x="128" y="96" width="44" height="34" rx="3" fill="#d7263d"/><path d="M138 112l7 7 14-14" fill="none" stroke="#f4ead2" stroke-width="4"/>`,
  58   federation: () => {
  59     const pts = Array.from({ length: 9 }, (_, i) => [150 + Math.cos((i * 2 * Math.PI) / 9) * 76, 100 + Math.sin((i * 2 * Math.PI) / 9) * 76]);
  60     const lines = pts.flatMap((a, i) => pts.slice(i + 1).filter((_, j) => (j + i) % 2 === 0).map((b) => `<path d="M${a[0]} ${a[1]}L${b[0]} ${b[1]}" stroke="#f4ead2" stroke-width="1.4" opacity=".5"/>`));
  61     return `<rect width="300" height="200" fill="#120f2e"/>${lines.join('')}
  62 ${pts.map(([x, y], i) => `<circle cx="${x}" cy="${y}" r="9" fill="${['#ff5fa2', '#ffd166', '#5ef2d6', '#7a8cff'][i % 4]}"/>`).join('')}
  63 <circle cx="150" cy="100" r="22" fill="#f4ead2"/><circle cx="150" cy="100" r="12" fill="#120f2e"/>`;
  64   },
  65   'twin-script': () => `<rect width="300" height="200" fill="#f4ead2"/><rect x="150" width="150" height="200" fill="#1d1b4f"/>
  66 <text x="75" y="132" text-anchor="middle" font-family="Oswald, sans-serif" font-size="96" font-weight="700" fill="#d7263d">Aa</text>
  67 <text x="225" y="132" text-anchor="middle" font-family="Oswald, sans-serif" font-size="96" font-weight="700" fill="#ffd166">Жж</text>
  68 <text x="75" y="172" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11" fill="#1d1b4f">LATINICA</text>
  69 <text x="225" y="172" text-anchor="middle" font-family="IBM Plex Mono, monospace" font-size="11" fill="#f4ead2">ЋИРИЛИЦА</text>`,
  70   'exit-by-design': () => `${stripes(['#5ef2d6', '#1b9aaa', '#0f5d6a', '#0b1433'])}
  71 <rect x="104" y="40" width="92" height="140" rx="4" fill="#0b1433" stroke="#f4ead2" stroke-width="4"/>
  72 <path d="M112 48l60 10v114l-60 0z" fill="#ffd166"/><circle cx="160" cy="114" r="4" fill="#0b1433"/>
  73 <path d="M206 110h56M246 94l16 16-16 16" fill="none" stroke="#f4ead2" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>`,
  74 };
  75 
  76 const definitions=[
  77 ['signal-returns','Signal se vraća','obična',20],['hello-yu','hello.yu','rijetka',40],['cassette-loader','Učitavanje s kasete','obična',20],['kafana','Noćna smjena u kafani','obična',20],['market','PIJACA.YU','rijetka',40],['galeb-1981','Galeb, u čast','rijetka',40],['orao-tribute','Orao, u čast','rijetka',40],['same-coast','Iste obale','folija',80],['same-mountains','Iste planine','folija',80],['nema-granica','Nema granica za znanje','folija',80],['exit-by-design','Izlaz po zamisli','rijetka',40],['twin-script','Dva pisma','folija',80],['steward','Izabrani povjerenik','hologram',120],['federation','Federacija stvaralaca','hologram',120],['teletekst','Teletekst strana 100','obična',20],['probna-slika','Probna slika','obična',20],['kesica','Sličica iz kesice','obična',0],['zadruga','Zadruga','rijetka',40],['potpis','Potpisani zapis','rijetka',40],['dnevnik','Dnevnik bez brisanja','folija',80],['akademac','Akademac','folija',0],['udarnik','Udarnik mreže','folija',0],['signal-zlatna','Signal se vraća, zlatna','hologram',0],['osnivacka','Osnivačka','osnivačka',0]];
  78 export const EDITIONS=definitions.map(([slug,name,rarity,price],i)=>({slug,name,rarity,price,no:i+1,supply:({'rijetka':1000,folija:300,hologram:100,'osnivačka':100})[rarity]||null,earned:i>=20}));
  79 export const EDITION=Object.fromEntries(EDITIONS.map(e=>[e.slug,e]));
  80 export const RARITY=Object.fromEntries(EDITIONS.map(e=>[e.rarity,{label:e.rarity,supply:e.supply}]));
  81 export const LEVELS={member:{n:2,label:'član'},builder:{n:3,label:'graditelj'},steward:{n:4,label:'ustanova'}};
  82 export const memberNo=n=>'YU-'+String(n).padStart(5,'0');
  83 export function avatar(u,size=48){return u.avatar&&!u.deleted?`<img class="av" src="/avatars/${esc(u.avatar)}" width="${size}" height="${size}" alt="">`:`<svg class="av" viewBox="0 0 16 16" width="${size}" height="${size}" shape-rendering="crispEdges" aria-hidden="true"><rect width="16" height="16" fill="#003893"/><rect x="5" y="3" width="6" height="5" fill="#e7dec3"/><rect x="3" y="10" width="10" height="6" fill="#e7dec3"/></svg>`;}
  84 function shell(front,back,attrs='',cls='') {return `<figure class="karta ${cls}" ${attrs}><div class="karta-tijelo idle"><div class="lice">${front}<div class="folija"></div><div class="iskre"></div><div class="odsjaj"></div></div><div class="nalicje">${back}</div><div class="rub"></div></div><button type="button" data-flip aria-pressed="false">${t('Okreni kartu')}</button> <button type="button" data-png>${t('Sačuvaj kao sliku')}</button></figure>`;}
  85 export function memberCard(u,{fields=['ime','grad','nivo','datum','ugled'],build='',reputation='0 / 0',invite='',signature=''}={}) {
  86  const lines=[fields.includes('ime')?u.display:'',fields.includes('grad')?(GRAD[u.city_id]?.ime||t('negdje na mreži')):'',fields.includes('nivo')?t(['posjetilac','istraživač','član','graditelj','ustanova'][u.nivo??2]):'',fields.includes('datum')?datum(u.created_at):'',fields.includes('ugled')?t('Ugled')+' '+reputation:''].filter(Boolean);
  87  const hash=sha(invite||u.handle),pattern=Array.from({length:81},(_,i)=>parseInt(hash[i%64],16)%2?`<rect x="${i%9}" y="${Math.floor(i/9)}" width="1" height="1"/>`:'').join('');
  88  return shell(`<div class="sloj-tekst">${avatar(u,24)} ${t('YU MREŽA · ČLANSKA KARTA · ЧЛАНСКА КАРТА')}</div><div class="sloj-sredina"><h3>${esc(lines[0]||'')}</h3><span class="lat">${esc(u.handle)}.yu · ${memberNo(u.member_no)}</span></div><div class="sloj-tekst">${lines.slice(1).map(esc).join(' · ')}<p>${t('INSTALIRAJ · GRADI · OBJAVI · POVEŽI')}</p><small>${t('kartaNapomena')}</small></div>`,`<p class="prava">${t('prava')}</p><svg viewBox="0 0 9 9" width="54" height="54" fill="#e7dec3" aria-hidden="true">${pattern}</svg><span class="mono">${esc(signature?sha(signature).slice(0,16):hash.slice(0,16))} · ${esc(build)}</span>`,`data-card-png data-lines="${esc(JSON.stringify([t('YU MREŽA · ČLANSKA KARTA'),...lines,u.handle+'.yu',memberNo(u.member_no),t('kartaNapomena')]))}"`,'hologram');
  89 }
  90 export function collectible(slug,instance={},history=[]) {const e=EDITION[slug];if(!e)return '';const art=ART[slug]||ART['signal-returns'];return shell(`<div class="sloj-tekst">${t('Br.')} ${e.no} · ${t(e.rarity)}</div><div class="sloj-podloga"><svg class="card-art" viewBox="0 0 300 200" aria-hidden="true">${art()}</svg></div><div class="sloj-sredina"><h3>${t(e.name)}</h3><p>${t('Iste obale. Iste planine. Mnogo jezika, jedna mreža.')}</p></div><div class="sloj-tekst">#${instance.serial||'—'} / ${e.supply||t('otvoreno')}</div>`,`<h3>${t('Prva serija')}</h3><p>${t('Serijski broj')} #${instance.serial||'—'} / ${e.supply||t('otvoreno')}</p><p>${instance.minted_at?datum(instance.minted_at):''} · ${t(instance.source||'izdanje')}</p><p class="lat">${history.map(esc).join(' · ')}</p><p class="mono">${esc(instance.sig?.slice(0,16)||'')}</p>${instance.id?`<a href="/slicice/provjera/${instance.id}">${t('Provjeri')}</a>`:''}`,`data-lines="${esc(JSON.stringify([t('Prva serija'),t(e.name),t(e.rarity),'#'+(instance.serial||'0'),t('Iste obale. Iste planine.')]))}"`,'slicica '+e.rarity.replace('č','c'));}
  91