Preskoči na sadržaj

Primeri ​

Svi primeri pretpostavljaju ključ u promenljivoj okruženja PROPERTIO_API_KEY i bazni URL https://app.propertio.rs/api. Za razvoj koristite ključ test agencije i https://app.test.propertio.rs/api.

Node.js (skripta, cron) ​

js
// oglasi.mjs: node --env-file=.env oglasi.mjs
import { PropertioClient, formatPrice, formatLocation } from "@propertio/sdk";

const propertio = new PropertioClient({ baseUrl: "https://app.propertio.rs/api", apiKey: process.env.PROPERTIO_API_KEY });
const { items, total } = await propertio.properties.search({ dealType: "sale", pageSize: 5 });
console.log(`${total} oglasa na prodaju`);
for (const p of items) console.log(`${p.reference}  ${formatPrice(p)}  ${formatLocation(p)}  ${p.title}`);

Čist JavaScript u browseru ​

Za jednostavan statički sajt bez build koraka. Ključ je vidljiv u kodu stranice, pa agencija mora da podesi dozvoljene sajtove.

html
<ul id="oglasi"></ul>
<script type="module">
  import { PropertioClient, imageUrl, formatPrice } from "https://esm.sh/@propertio/sdk@0.1.0";

  const propertio = new PropertioClient({ baseUrl: "https://app.propertio.rs/api", apiKey: "pk_…" });
  const { items } = await propertio.properties.search({ pageSize: 12 });
  document.querySelector("#oglasi").innerHTML = items.map((p) => `
    <li>
      <a href="/oglas.html?slug=${encodeURIComponent(p.slug)}">
        <img src="${imageUrl(p, "card") ?? "/bez-slike.svg"}" alt="" width="400" height="267" loading="lazy">
        <strong></strong> <span>${formatPrice(p)}</span>
      </a>
    </li>`).join("");
  // naslov kao tekst, ne kao HTML (oglasi su unos korisnika)
  document.querySelectorAll("#oglasi strong").forEach((el, i) => { el.textContent = items[i].title; });
</script>

Tekst oglasa je unos korisnika

Naslov, opis i ime agenta ubacujte kao tekst (textContent, JSX, Astro {}), nikad kao sirov HTML. Opis čuva prelome reda: prikažite ga sa white-space: pre-line.

Next.js (App Router) ​

Ključ ostaje na serveru: komponente su serverske, upit ide preko server akcije.

ts
// lib/propertio.ts
import "server-only";
import { PropertioClient } from "@propertio/sdk";

export const propertio = new PropertioClient({
  baseUrl: process.env.PROPERTIO_API_URL ?? "https://app.propertio.rs/api",
  apiKey: process.env.PROPERTIO_API_KEY!,
  requestInit: { next: { revalidate: 300 } }, // keš 5 minuta; mnogo kraće od 24 h važenja linkova slika
});
tsx
// app/nekretnine/page.tsx: lista sa filterima iz URL-a
import Link from "next/link";
import { imageUrl, formatPrice, formatLocation, type SearchParams } from "@propertio/sdk";
import { propertio } from "@/lib/propertio";

export default async function Page({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {
  const sp = await searchParams;
  const query: SearchParams = {
    dealType: sp.tip === "izdavanje" ? "rent" : "sale",
    city: sp.grad, priceMax: sp.do ? Number(sp.do) : undefined,
    page: Number(sp.strana ?? 1), pageSize: 12,
  };
  const { items, total } = await propertio.properties.search(query);
  return (
    <main>
      <p>{total} oglasa</p>
      <ul>
        {items.map((p) => (
          <li key={p.id}>
            <Link href={`/nekretnine/${p.slug}`}>
              {imageUrl(p, "card") && <img src={imageUrl(p, "card")!} alt="" width={400} height={267} loading="lazy" />}
              <h2>{p.title}</h2>
              <p>{formatPrice(p)} · {p.areaSqm} m² · {formatLocation(p)}</p>
            </Link>
          </li>
        ))}
      </ul>
    </main>
  );
}
tsx
// app/nekretnine/[slug]/page.tsx: detalj, 404, stari slug i metapodaci
import { notFound, permanentRedirect } from "next/navigation";
import { PropertioError, imageUrl, formatPrice } from "@propertio/sdk";
import { propertio } from "@/lib/propertio";
import { InquiryForm } from "./inquiry-form";

async function load(slug: string) {
  let p;
  try { p = await propertio.properties.get(slug); }
  catch (e) { if (e instanceof PropertioError && e.status === 404) notFound(); throw e; }
  // naslov (pa i slug) je promenjen: stari link i dalje nalazi oglas, preusmeri na novu adresu
  if (p.slug !== slug) permanentRedirect(`/nekretnine/${p.slug}`);
  return p;
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const p = await load((await params).slug);
  return { title: p.title, description: p.description.slice(0, 160), openGraph: { images: imageUrl(p, "large") ? [imageUrl(p, "large")!] : [] } };
}

export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const p = await load((await params).slug);
  return (
    <article>
      <h1>{p.title}</h1>
      <p>{formatPrice(p)} · šifra {p.reference}</p>
      <p style={{ whiteSpace: "pre-line" }}>{p.description}</p>
      {p.agent && <p>Agent: {p.agent.name} {p.agent.phone}</p>}
      <InquiryForm propertyId={p.id} />
    </article>
  );
}
tsx
// app/nekretnine/[slug]/actions.ts: server akcija za upit
"use server";
import { PropertioError } from "@propertio/sdk";
import { propertio } from "@/lib/propertio";

export async function sendInquiry(_prev: unknown, form: FormData) {
  if (form.get("consent") !== "on") return { ok: false, error: "Potrebna je saglasnost za obradu podataka." };
  try {
    await propertio.inquiries.create({
      propertyId: String(form.get("propertyId")),
      name: String(form.get("name") ?? ""),
      phone: String(form.get("phone") ?? "") || null,
      email: String(form.get("email") ?? "") || null,
      message: String(form.get("message") ?? ""),
      consent: true,
      website: String(form.get("website") ?? ""), // honeypot, prosleđen netaknut
    });
    return { ok: true };
  } catch (e) {
    if (e instanceof PropertioError && e.code === "VALIDATION") return { ok: false, error: "Proverite ime, kontakt i saglasnost." };
    if (e instanceof PropertioError && e.code === "RATE_LIMITED") return { ok: false, error: "Previše pokušaja, pokušajte za minut." };
    return { ok: false, error: "Slanje nije uspelo, pokušajte ponovo ili nas pozovite." };
  }
}
tsx
// app/sitemap.ts
import type { MetadataRoute } from "next";
import { propertio } from "@/lib/propertio";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const out: MetadataRoute.Sitemap = [];
  for await (const p of propertio.properties.all()) out.push({ url: `https://www.agencija.rs/nekretnine/${p.slug}`, lastModified: p.updatedAt });
  return out;
}

Za trenutno osvežavanje posle izmene oglasa, umesto čekanja revalidate, koristite webhook i revalidatePath.

Astro ​

Statički sajt (SSG) koji se gradi na nekoliko sati, ili SSR sa adapterom. Ključ je samo u build/server kodu.

astro
---
// src/pages/nekretnine/[slug].astro (SSG)
import { PropertioClient, imageUrl, formatPrice, formatLocation } from "@propertio/sdk";

export async function getStaticPaths() {
  const propertio = new PropertioClient({ baseUrl: "https://app.propertio.rs/api", apiKey: import.meta.env.PROPERTIO_API_KEY });
  const paths = [];
  for await (const p of propertio.properties.all()) paths.push({ params: { slug: p.slug }, props: { p } });
  return paths;
}
const { p } = Astro.props;
---
<html lang="sr">
  <head><title>{p.title}</title><meta property="og:image" content={imageUrl(p, "large")} /></head>
  <body>
    <h1>{p.title}</h1>
    <p>{formatPrice(p)} · {p.areaSqm} m² · {formatLocation(p)}</p>
    <img src={imageUrl(p, "large")} alt={p.title} width="1600" height="1066" />
    <p style="white-space: pre-line">{p.description}</p>
  </body>
</html>

Statički sajt i slike

Linkovi slika važe 24 h od izgradnje. Statički sajt pravite ponovo bar jednom dnevno (npr. zakazani build na Netlify/Vercel/GitHub Actions svakih 6 sati, i odmah na webhook), ili slike učitavajte pri prikazu sa servera.

Kontakt forma u Astru: API ruta (src/pages/api/upit.ts, sa SSR adapterom) koja zove inquiries.create, isto kao server akcija iznad.

PHP (WordPress i drugi CMS-ovi) ​

Bez SDK-a, direktno preko HTTP-a:

php
<?php
function propertio(string $path, array $query = [], ?array $body = null): array {
    $url = 'https://app.propertio.rs/api/v1' . $path . ($query ? '?' . http_build_query($query) : '');
    $headers = ['x-api-key: ' . getenv('PROPERTIO_API_KEY'), 'accept: application/json'];
    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15]);
    if ($body !== null) {
        $headers[] = 'content-type: application/json';
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); // POST
    }
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $res = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $json = json_decode($res ?: 'null', true);
    if ($status >= 400) throw new RuntimeException(($json['error'] ?? 'ERROR') . ': ' . ($json['message'] ?? "HTTP $status"), $status);
    return $json;
}

// WordPress: keš 5 minuta
$page = get_transient('propertio_sale') ?: propertio('/properties', ['dealType' => 'sale', 'pageSize' => 12]);
set_transient('propertio_sale', $page, 300);
foreach ($page['items'] as $p) {
    echo '<h2>' . esc_html($p['title']) . '</h2>';
}

Primer sajta (apps/demo-site) ​

U repozitorijumu Propertija, apps/demo-site je mali sajt agencije (Vite + TypeScript, bez frameworka) na SDK-u:

  • lista aktivnih oglasa sa naslovnom slikom, cenom i lokacijom (properties.search, imageUrl, formatPrice);
  • detalj oglasa (properties.get) sa formom za upit (inquiries.create) i dugmetom „Zakaži pokazivanje“ (viewings.request), uz izgovorenu potvrdu (voice.confirmation, sa speechSynthesis kao rezervom);
  • AI asistent za posetioce (assistant.chat) sa linkovima ka oglasima koje je pronašao (listings) i izgovorom odgovora (voice.speak sa speechToken).

Pokretanje lokalno, uz ključ test agencije:

bash
pnpm install
VITE_PROPERTIO_API_URL=https://app.test.propertio.rs/api VITE_PROPERTIO_API_KEY=pk_… pnpm --filter @propertio/demo-site dev

Demo sajt zove API iz browsera i ključ je u kodu stranice. To je u redu za demonstraciju; pravi sajt neka ključ drži na serveru.