GaleriPin
Featured
Web Development

GaleriPin

webdesignNext.JsUIUX

web s přepínáním vzhledu, pro sdílení a komentování fotek (aktuální verze nemá nastavenou konfiguraci na wloudinary, tudíž všechen odkaz je buď externí nebo uložený v Mongo v rámci limitů)

// Jak to funguje

Co aplikace skutečně řeší

Uživatel vytvoří pin, přidá titul, popis a tagy, cizí pin uloží do boardu, okomentuje ho a sdílí přes vlastní URL. Vyhledávání nehledá jen v názvu, ale i v popisech, tazích, boardech a u uživatelů.

Pin v GaleryPin není jen obrázek, je to propojený dokument. Čtyři vrstvy níže ukazují, co všechno se k němu váže.

CONTENT

Pins + metadata

Obsah není jen obrázek; je propojený s vlastníkem, tagy a boardy.

MEDIA

Cloudinary

Upload je oddělený do storage vrstvy, takže není natvrdo spojený s UI.

SOCIAL

Boards + comments

Ukládání cizího obsahu a interakce vyžadují vazby mezi dokumenty.

DISCOVERY

Search

Vyhledávání spojuje více typů textových dat do jednoho výsledku.

Vazby mezi obsahem, médii, sociálními prvky a vyhledáváním určují, jak vypadá backend. Uvidíš to hned v kapitole o architektuře.

// Jak to funguje

Architektura bez zbytečného backendu navíc

Next.js UI→Catch-all API route→MongoDB→Cloudinary

Backend je schovaný uvnitř Next.js v jednom catch-all route. Nasazení je díky tomu jednodušší, ale na jednom místě se sejde hodně zodpovědností: autentizace, databáze, CORS i upload.

// Kousek kódu

Databázové připojení a čištění odpovědi

KÓD 01 · MONGO + RESPONSE CLEANUPapp/api/[[...path]]/route.js
async function connectToMongo() {
  if (db) return db
  if (!connectPromise) {
    connectPromise = (async () => {
      client = new MongoClient(process.env.MONGO_URL)
      await client.connect()
      db = client.db(process.env.DB_NAME || 'galeripin')
      return db
    })()
  }
  return connectPromise
}

const clean = (doc) => {
  if (!doc) return doc
  const { _id, passwordHash, securityAnswerHash, resetToken, resetExpires, ...rest } = doc
  return rest
}
Co to děláInicializuje MongoDB pouze jednou a před odesláním odpovědi odstraňuje citlivá pole z dokumentu.
Proč na tom záležíJe to významné, protože API pracuje přímo s Mongo dokumenty. Nestačí tedy jen „najít uživatele“ — je potřeba zajistit, aby se password hash, reset token nebo interní `_id` nedostaly do klienta.
// Kousek kódu

Upload jako samostatná vrstva

KÓD 02 · CLOUDINARY STORAGElib/storage.js
export async function uploadImage(dataUrl) {
  const cloud = process.env.CLOUDINARY_CLOUD_NAME
  const apiKey = process.env.CLOUDINARY_API_KEY
  const apiSecret = process.env.CLOUDINARY_API_SECRET
  const folder = process.env.CLOUDINARY_FOLDER || 'galeripin'

  if (cloud && apiKey && apiSecret) {
    const timestamp = Math.floor(Date.now() / 1000)
    const paramsToSign = `folder=${folder}&timestamp=${timestamp}`
    const signature = crypto.createHash('sha1').update(paramsToSign + apiSecret).digest('hex')

    const form = new FormData()
    form.append('file', dataUrl)
    form.append('api_key', apiKey)
    form.append('timestamp', String(timestamp))
    form.append('folder', folder)
    form.append('signature', signature)
Co to děláPokud jsou dostupné Cloudinary credentials, připraví podepsaný upload; jinak projekt používá MongoDB fallback.
Proč na tom záležíVýznam je v oddělení storage provideru od zbytku aplikace. Změna způsobu ukládání médií tak nemusí znamenat přepis všech míst, která upload používají.
// Kousek kódu

Registrace a souhlas s podmínkami

KÓD 03 · REGISTER VALIDATIONapp/api/[[...path]]/route.js
if (route === '/auth/register' && method === 'POST') {
  const body = await request.json()
  const acceptedTerms = body.acceptedTerms === true
  if (!acceptedTerms) {
    return cors(NextResponse.json(
      { error: 'Pro registraci musíš souhlasit s podmínkami používání' },
      { status: 400 }
    ))
  }
  const username = (body.username || '').trim().toLowerCase().replace(/[^a-z0-9_.]/g, '')
  const password = body.password || ''
  const email = (body.email || '').trim().toLowerCase() || null
  if (username.length < 2) return cors(NextResponse.json(
    { error: 'Přezdívka musí mít alespoň 2 znaky (a-z, 0-9, _ .)' },
    { status: 400 }
  ))
Co to děláServer kontroluje souhlas s podmínkami, normalizuje username a validuje jeho základní podobu.
Proč na tom záležíValidace je na serveru, takže není závislá na tom, co zrovna vykreslí frontend. To je přesně ten typ pravidla, který musí platit i při přímém API requestu.
// Jak to funguje

UI vrstva

Frontend používá React Query pro serverový stav, ThemeProvider pro dark/light mode a Sonner pro notifikace. Pro appku, kde se pořád pracuje s daty a přitom má být interakce svižná, mi to přijde rozumná kombinace.

Frontend řeší data, vzhled i zpětnou vazbu pro uživatele a každá z těch částí má svou knihovnu.

DATA

React Query

Cache a refetch nejsou rozptýlené po každém komponentu.

THEME

next-themes

Dark/light režim je řešený centrálně.

FEEDBACK

Sonner

Výsledky operací se propisují do uživatelských notifikací.

Díky tomu není cache, motiv ani notifikace rozeseté po komponentách, ale řeší se to centrálně. Míň chaosu, víc klidu.

// Jak to funguje

Co bylo technicky nejcitlivější

Největší riziko nebyl grid obrázků. Byl to fakt, že se tu potkávají účty, soubory a veřejně sdílený obsah. Proto jsou zajímavější věci jako čištění API odpovědí, hashování hesel, storage abstrakce nebo registrace než vzhled karty pinu.

// Jak to funguje

Další směr

V repu už jsou připravené follows, likes, notifikace, AI recommendations nebo moderace. Proto je důležitější základ než jednorázový design: nové sociální vazby budou přibývat nad stejným datovým modelem.

// Pod kapotou

Z čeho je projekt postavený

Žádná technologie tu není jen pro parádu. Tady je rychlý přehled, kdo za co odpovídá.

FRONTEND + API

Next.js 15

DATABÁZE

MongoDB

UKLÁDÁNÍ MÉDIÍ

Cloudinary

SERVEROVÝ STAV

React Query

UI KOMPONENTY

Tailwind + shadcn/ui

HASHOVÁNÍ HESEL

PBKDF2

// Shrnutí

Co je na projektu podstatné?

Nejde mi jen o to, jak to vypadá. Baví mě hlavně to, co je pod tím: kudy data tečou, co se validuje, co může selhat a proč jsem to udělala právě takhle.

Napiš mi
Registrace