FastComments.com

Add Comments to Your iOS App

Ово је званична iOS библиотека за FastComments.

Уградите виџете за коментарисање уживо, ћаскање и рецензије у вашу iOS апликацију.

Репозиторијум

Погледајте на GitHub

Карактеристике Internal Link

  • Угњежђене структуре коментара са унутрашњим одговорима и пагинацијом
  • Друштвени фид са креирањем постова, реакцијама и медијским прилозима
  • Режим уживо за чет са аутоматским скроловањем и разделницама по датуму
  • Ажурирања у реалном времену преко WebSocket-а (нови коментари, гласови, присутност)
  • Једнократно пријављивање (Simple SSO за тестирање, Secure SSO за продукцију)
  • Уређивање обогаћеног текста са подебљаним, курзивом, кодом и @поменама
  • Гласање са конфигурисаним стиловима (стрелица горе/доле или срца)
  • Модерацијске акције: означи, причврсти, закључај, блокирај
  • Опсежно темење са преднаставкама и потпуним прилагођавањем
  • Прилагођени дугмићи на траци алата за коментаре и креирање постова у фиду
  • Отпремање слика
  • Подршка за ЕУ регион
  • Присутност корисника (индикатори на мрежи/ван мреже)
  • Филтрирање фида по ознакама
  • Подршка за локализацију

Захтеви Internal Link


  • iOS 16+ или macOS 14+
  • Swift 5.9+
  • SwiftUI

Инсталација Internal Link

Додајте FastCommentsUI у ваш пројекат користећи Swift Package Manager.

У Xcode-у: File > Add Package Dependencies, затим унесите repository URL.

Или га додајте у ваш Package.swift:

dependencies: [
    .package(url: "https://github.com/fastcomments/fastcomments-ios.git", from: "1.0.0")
]

Затим додајте product у ваш target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "FastCommentsUI", package: "fastcomments-ios")
    ]
)

Импортујте оба модула где је потребно:

import FastCommentsUI
import FastCommentsSwift

Брзи почетак Internal Link

Минимално подешавање за приказ видџета за коментаре:

import SwiftUI
import FastCommentsUI

struct ContentView: View {
    @StateObject private var sdk = FastCommentsSDK(
        config: FastCommentsWidgetConfig(
            tenantId: "demo",
            urlId: "my-page-1",
            url: "https://example.com/page-1",
            pageTitle: "My Page"
        )
    )

    var body: some View {
        FastCommentsView(sdk: sdk)
            .task {
                try? await sdk.load()
            }
    }
}

Замените "demo" са својим FastComments tenant ID-ом. The urlId идентификује страницу или нит (тему) где се чувају коментари.



Аутентификација (SSO) Internal Link


FastComments подржава три начина аутентификације:

  1. Anonymous -- нема SSO токена; корисници добијају идентитете засноване на сесији
  2. Simple SSO -- токен на клијентској страни за демонстрације и тестирање (није сигурно)
  3. Secure SSO -- серверски потписан токен за продукцију

Simple SSO

Погодно за демонстрације и локално тестирање. Са Simple SSO свако може преузети идентитет било ког корисника, па га не користите у продукцији.

import FastCommentsSwift

let userData = SimpleSSOUserData(
    username: "Jane Doe",
    email: "jane@example.com",
    avatar: "https://example.com/avatar.jpg"
)
let sso = FastCommentsSSO.createSimple(simpleSSOUserData: userData)
let token = try? sso.prepareToSend()

let config = FastCommentsWidgetConfig(
    tenantId: "YOUR_TENANT_ID",
    urlId: "my-page-1",
    sso: token
)
let sdk = FastCommentsSDK(config: config)

SimpleSSOUserData такође подржава опционална поља:

  • id -- ID корисника (ако није постављено, подразумева се email)
  • displayName -- засебно приказано име
  • displayLabel -- прилагођени натпис приказан поред имена (нпр. "VIP")
  • websiteUrl -- веза на корисниковом имену
  • locale -- код локализације
  • isProfileActivityPrivate -- сакриј активност профила (задано: true)

Secure SSO

У продукцији, ваш backend генерише потписани SSO token користећи ваш API secret. iOS апликација преузима тај token са вашег сервера и просљеђује га у конфигурацију.

На вашем backendu (користећи FastComments Swift SDK или било који језик):

let userData = SecureSSOUserData(
    id: "user-123",
    email: "user@example.com",
    username: "Display Name",
    avatar: "https://example.com/avatar.jpg"
)
let sso = try FastCommentsSSO.createSecure(apiKey: "YOUR_API_KEY", secureSSOUserData: userData)
let token = try sso.prepareToSend()
// Return this token to your iOS app via your API

У вашој iOS апликацији:

struct MyView: View {
    @StateObject private var sdk = FastCommentsSDK(
        config: FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "my-page-1"
        )
    )
    @State private var isLoadingToken = true

    var body: some View {
        Group {
            if isLoadingToken {
                ProgressView("Loading...")
            } else {
                FastCommentsView(sdk: sdk)
            }
        }
        .task {
            // Fetch the token from your backend
            let token = try? await fetchSSOTokenFromYourBackend()
            // Create a new config with the token, or set it before load
            isLoadingToken = false
            try? await sdk.load()
        }
    }
}

SecureSSOUserData подржава додатна поља:

  • optedInNotifications -- пријава за e-mail обавјештења
  • displayLabel -- прилагођени натпис
  • displayName -- приказано име
  • websiteUrl -- веб адреса
  • groupIds -- чланства у групама
  • isAdmin -- администраторске привилегије
  • isModerator -- модераторске привилегије
  • isProfileActivityPrivate -- приватност активности профила


Коментари у нитима Internal Link

Основна употреба

struct CommentsPage: View {
    @StateObject private var sdk = FastCommentsSDK(
        config: FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "article-42",
            url: "https://example.com/article/42",
            pageTitle: "Article Title"
        )
    )

    var body: some View {
        FastCommentsView(sdk: sdk)
            .task {
                try? await sdk.load()
            }
    }
}

Стилови гласања

Подразумевани стил гласања приказује стрелице горе/доле. Проследите ._1 за стил гласања са срцем:

FastCommentsView(sdk: sdk, voteStyle: ._1)
Стил Изглед
._0 Дугмићи са стрелицама горе/доле и нето бројем
._1 Једно дугме у облику срца са бројем

Повратни позиви догађаја

Користите повратне позиве у облику модификатора да бисте обрадили корисничке интеракције:

FastCommentsView(sdk: sdk)
    .onCommentPosted { comment in
        print("New comment: \(comment.commentHTML)")
    }
    .onReplyClick { renderableComment in
        print("Replying to: \(renderableComment.comment.id)")
    }
    .onUserClick { context, userInfo, source in
        // source is .name or .avatar
        print("Tapped \(userInfo.displayName)")
    }

Примена теме

Проследите тему преко SwiftUI окружења:

FastCommentsView(sdk: sdk)
    .fastCommentsTheme(myTheme)
    .task { try? await sdk.load() }

Или је поставите директно на SDK:

sdk.theme = FastCommentsTheme.modern

Смер сортирања

sdk.defaultSortDirection = .nf  // Newest first (default)
sdk.defaultSortDirection = .of  // Oldest first
sdk.defaultSortDirection = .mr  // Most relevant


Разговор уживо Internal Link

LiveChatView pruža chat u stvarnom vremenu sa automatskim skrolovanjem, separatorima datuma i kompaktim rasporedom. Automatski konfiguriše SDK za sortiranje po najstarijem (oldest-first) i za neposredno prikazivanje uživo.

struct ChatView: View {
    @StateObject private var sdk: FastCommentsSDK = {
        let config = FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "chat-room-1",
            sso: ssoToken  // SSO se preporučuje kako bi korisnici imali imena
        )
        return FastCommentsSDK(config: config)
    }()

    var body: some View {
        LiveChatView(sdk: sdk)
            .onCommentPosted { comment in
                print("Sent: \(comment.commentHTML)")
            }
            .task {
                try? await sdk.load()
            }
    }
}

LiveChatView podržava ove povratne pozive (callbacks):

  • .onCommentPosted -- poziva se kada korisnik pošalje poruku
  • .onCommentDeleted -- poziva se kada je poruka obrisana
  • .onUserClick -- poziva se kada se klikne na korisničko ime ili avatar


Друштвени фид Internal Link

Feed систем је посебан SDK (FastCommentsFeedSDK) са својим приказом.

Учитавање и приказ фида

struct FeedPage: View {
    @StateObject private var sdk: FastCommentsFeedSDK = {
        let config = FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "my-feed",
            sso: ssoToken
        )
        return FastCommentsFeedSDK(config: config)
    }()

    @State private var commentsPost: FeedPost?

    var body: some View {
        FastCommentsFeedView(sdk: sdk)
            .onPostSelected { post in
                commentsPost = post
            }
            .onCommentsRequested { post in
                commentsPost = post
            }
            .onSharePost { post in
                // Прикажи листу за дељење
            }
            .onUserClick { context, userInfo, source in
                // Отвори кориснички профил
            }
            .onMediaClick { mediaItem, index in
                // Прикажи приказивач слика преко целог екрана
            }
            .task {
                try? await sdk.load()
            }
    }
}

Приказ фида аутоматски укључује повлачење за освежавање и бесконачно скроловање.

Креирање постова

Користите FeedPostCreateView да прикажете форму за креирање поста:

@State private var showCreatePost = false

// Унутар тела вашег приказа:
.sheet(isPresented: $showCreatePost) {
    FeedPostCreateView(
        sdk: sdk,
        onPostCreated: { post in
            showCreatePost = false
            Task { try? await sdk.refresh() }
        },
        onCancelled: {
            showCreatePost = false
        }
    )
}

Реакције на постове

SDK обрађује реакције са оптимистичким ажурирањима:

try await sdk.reactPost(postId: post.id, reactionType: "l")

// Check reaction state
let hasLiked = sdk.hasUserReacted(postId: post.id, reactType: "l")
let likeCount = sdk.getLikeCount(postId: post.id)

Отварање коментара на посту

Користите CommentsSheet за приказ коментара за пост у фиду. Он унутар себе креира инстанцу FastCommentsSDK користећи конфигурацију feed SDK-а:

.sheet(item: $commentsPost) { post in
    CommentsSheet(post: post, feedSDK: sdk, onUserClick: { context, userInfo, source in
        // Обради клик на корисника
    })
}

Напомена: FeedPost мора да имплементира Identifiable за .sheet(item:). Додајте ову екстензију:

extension FeedPost: @retroactive Identifiable {}

Филтрирање фида по таговима

Имплементирајте протокол TagSupplier да бисте филтрирали постове у фиду по таговима:

struct TeamTagSupplier: TagSupplier {
    func getTags(currentUser: UserSessionInfo?) -> [String]? {
        guard let user = currentUser else { return nil }
        return ["team:\(user.id ?? "")", "public"]
    }
}

sdk.tagSupplier = TeamTagSupplier()

Вратите nil за нефилтрирани глобални фид.

Чување и враћање стања фида

Сачувајте стање пагинације кроз животни циклус приказа:

let state = sdk.savePaginationState()
// Later...
sdk.restorePaginationState(state)

Брисање постова

sdk.onPostDeleted = { postId in
    print("Post \(postId) was deleted")
}


Прилагођавање изгледа Internal Link

Преднаподешавања тема

Доступна су четири унапред дефинисана пресета:

// Системске подразумеване вриједности
sdk.theme = FastCommentsTheme.default

// Картице са сенкама и великим заобљеним ивицама
sdk.theme = FastCommentsTheme.modern

// Равне, без сенки, мали радијус ивица, без линија нити
sdk.theme = FastCommentsTheme.minimal

// Подеси све боје акција на једну боју бренда
sdk.theme = FastCommentsTheme.allPrimary(.indigo)

Стилови приказа коментара

var theme = FastCommentsTheme()
theme.commentStyle = .flat    // Равна листа са раздвајачима (подразумевано)
theme.commentStyle = .card    // Заобљене картице са сенкама
theme.commentStyle = .bubble  // Стил балонића

Боје

Сва својства боја су опционална. Непостављене вриједности враћају се на разумне системске подразумеване вриједности.

var theme = FastCommentsTheme()

// Боје бренда
theme.primaryColor = .indigo
theme.primaryLightColor = .indigo.opacity(0.6)
theme.primaryDarkColor = Color(red: 0.2, green: 0.1, blue: 0.5)

// Позадине
theme.commentBackgroundColor = Color(.secondarySystemGroupedBackground)
theme.containerBackgroundColor = Color(.systemGroupedBackground)

// Дугмићи акција
theme.actionButtonColor = .indigo
theme.replyButtonColor = .indigo
theme.toggleRepliesButtonColor = .indigo.opacity(0.8)
theme.loadMoreButtonTextColor = .indigo

// Гласови
theme.voteActiveColor = .red
theme.voteCountColor = .primary
theme.voteCountZeroColor = .secondary
theme.voteDividerColor = Color(.separator)

// Линкови
theme.linkColor = .indigo
theme.linkColorPressed = .indigo.opacity(0.5)

// Дијалози
theme.dialogHeaderBackgroundColor = .indigo
theme.dialogHeaderTextColor = .white

// Трака за унос
theme.inputBarBackgroundColor = Color(.systemBackground)
theme.inputBarBorderColor = Color(.separator)

// Остало
theme.onlineIndicatorColor = .green
theme.separatorColor = Color(.separator)
theme.badgeBackgroundColor = .gray.opacity(0.2)
theme.threadLineColor = .indigo.opacity(0.15)

Типографија

theme.commenterNameFont = .subheadline.weight(.bold)
theme.bodyFont = .body
theme.captionFont = .caption
theme.actionFont = .caption.weight(.medium)

Распоред и размак

theme.cornerRadius = .large       // Опције: .none, .small, .medium, .large
theme.commentSpacing = 4          // Тачке између редова коментара
theme.nestingIndent = 20          // Тачке увлачења по нивоу уградње
theme.avatarSize = 36             // Пречник аватара за коренске коментаре
theme.replyAvatarSize = 28        // Пречник аватара за унутрашње одговоре

Визуелни ефекти

theme.showShadows = true          // Благе сенке на картицама
theme.showThreadLine = true       // Вертикална линија која повезује уграђене одговоре
theme.animateVotes = true         // Опружна (spring) анимација при промјенама гласа

Примјена тема

Два приступа:

// Преко SwiftUI окружења (препоручено за хијерархију приказа)
FastCommentsView(sdk: sdk)
    .fastCommentsTheme(theme)

// Директно на SDK
sdk.theme = theme


Прилагођени тастери на траци са алаткама Internal Link

Дугмад траке алата за коментаре

Имплементирајте протокол CustomToolbarButton да бисте додали дугмад у траку са алаткама за унос коментара:

struct EmojiButton: CustomToolbarButton {
    let id = "emoji"
    let iconSystemName = "face.smiling"       // Назив SF симбола
    let contentDescription = "Add Emoji"
    let badgeText: String? = nil              // Опционо: број на значки

    func onClick(text: Binding<String>) {
        text.wrappedValue += "\u{1F44D}"
    }

    // Optional overrides (default to true)
    func isEnabled() -> Bool { true }
    func isVisible() -> Bool { true }
}

Проследите прилагођена дугмад при креирању view-а:

FastCommentsView(
    sdk: sdk,
    customToolbarButtons: [EmojiButton(), CodeBlockButton()]
)

Или их додајте глобално на SDK (примењује се на све инстанце):

sdk.addGlobalCustomToolbarButton(EmojiButton())
sdk.removeGlobalCustomToolbarButton(id: "emoji")
sdk.clearGlobalCustomToolbarButtons()

Дугмад траке алата за фид

Имплементирајте FeedCustomToolbarButton за форму за креирање објаве:

struct HashtagButton: FeedCustomToolbarButton {
    let id = "hashtag"
    let iconSystemName = "number"
    let contentDescription = "Add Hashtag"

    func onClick(content: Binding<String>) {
        content.wrappedValue += "#"
    }
}

Проследите их у приказ за креирање:

FeedPostCreateView(
    sdk: sdk,
    customToolbarButtons: [HashtagButton()],
    onPostCreated: { _ in },
    onCancelled: { }
)

Или их поставите глобално у feed SDK:

sdk.globalFeedToolbarButtons = [HashtagButton()]


Модерација Internal Link

Radnje dostupne svim korisnicima

  • Prijavi/Opozovi prijavu -- prijavi komentar na pregled
try await sdk.flagComment(commentId: commentId)
try await sdk.unflagComment(commentId: commentId)
  • Blokiraj/Odblokiraj -- sakrij sve komentare od korisnika (po gledaocu)
try await sdk.blockUser(commentId: commentId)
try await sdk.unblockUser(commentId: commentId)

Radnje samo za administratore

  • Zakači/Odkači -- zakači komentar na vrh rasprave
try await sdk.pinComment(commentId: commentId)
try await sdk.unpinComment(commentId: commentId)
  • Zaključaj/Otključaj -- spriječi nove odgovore na komentar
try await sdk.lockComment(commentId: commentId)
try await sdk.unlockComment(commentId: commentId)

Sve radnje moderacije su takođe dostupne kroz kontekstni meni komentara u korisničkom sučelju. Administratorske radnje se pojavljuju samo kada je trenutni korisnik administrator sajta (podešeno putem SSO isAdmin zastavice ili konfiguracije nadzorne ploče).



Ажурирања у реалном времену Internal Link

Након позива sdk.load(), SDK аутоматски се претплаћује на WebSocket догађаје за конфигурисани urlId. Следећи догађаји се обрађују:

  • Нови коментари, измене и брисања
  • Гласови (нови и уклоњени)
  • Промјене статуса пинова, закључавања, пријављивања и блокирања
  • Присутност корисника (придруживање/напуштање)
  • Отварање/затварање теме
  • Додељивање значки
  • Ажурирања конфигурације сервера

Контрола живог приказа

Подразумевано, нови коментари од других корисника се појављују одмах:

sdk.showLiveRightAway = true   // Подразумевано: прикажи одмах

Поставите ово на false да бисте кеширали нове коментаре иза дугмета "N нових коментара", дајући кориснику могућност да одлучи када ће их открити:

sdk.showLiveRightAway = false

Присутност корисника

Индикатори онлајн/офлајн се аутоматски појављују на аватарима корисника када сервер омогући праћење присутности. На клијенту није потребна додатна конфигурација.



Пагинација Internal Link

Величина странице

// Коментари: подразумјевано 30
sdk.pageSize = 50

// Feed: подразумјевано 10
feedSDK.pageSize = 20

Учитавање више коментара

UI аутоматски приказује контроле пагинације. Такође можете покренути пагинацију програмски:

// Учитај следећу страницу
try await sdk.loadMore()

// Учитај све преостале (онемогућено ако има >2000 коментара због перформанси)
try await sdk.loadAll()

// Провјери стање
sdk.hasMore            // Да ли постоје још странице
sdk.shouldShowLoadAll()
sdk.getCountRemainingToShow()

Пагинација подкоментара

Угнеждени одговори се учитавају на захтјев. Када корисник прошири нит, првих 5 подкоментара се учитава. Контрола "учитај више одговора" се појави ако их има још. Ово се аутоматски рјешава у UI.

Стање и опсервабилност Internal Link

Oba FastCommentsSDK i FastCommentsFeedSDK su klase ObservableObject sa @Published svojstvima. Možete ih posmatrati u svojim SwiftUI prikazima za reaktivna ažuriranja UI-ja.

Objavljena svojstva FastCommentsSDK

Svojstvo Tip Opis
commentCountOnServer Int Ukupan broj komentara na serveru
newRootCommentCount Int Privremeno pohranjeni novi komentari (kada je showLiveRightAway false)
currentUser UserSessionInfo? Trenutni autentifikovani korisnik
isSiteAdmin Bool Da li je trenutni korisnik administrator sajta
isClosed Bool Da li je nit komentara zatvorena
hasBillingIssue Bool Da li postoji problem sa naplatom
isLoading Bool Da li je u toku mrežni zahtev
hasMore Bool Da li postoje dodatne stranice komentara
blockingErrorMessage String? Greška koja sprečava funkcionisanje UI-ja
warningMessage String? Upozoravajuća poruka koja ne blokira
isDemo Bool Da li radi u demo režimu
commentsVisible Bool Prekidač vidljivosti komentara
toolbarEnabled Bool Da li je alatna traka za formatiranje prikazana

Objavljena svojstva FastCommentsFeedSDK

Svojstvo Tip Opis
feedPosts [FeedPost] Trenutno učitani postovi feeda
hasMore Bool Da li postoje dodatne stranice
currentUser UserSessionInfo? Trenutni autentifikovani korisnik
blockingErrorMessage String? Blokirajuća poruka o grešci
isLoading Bool Da li je u toku mrežni zahtev
newPostsCount Int Broj novih postova od posljednjeg učitavanja

Comment Tree

Stablo komentara je dostupno putem sdk.commentsTree:

// Ravna lista vidljivih čvorova za renderovanje
sdk.commentsTree.visibleNodes

// Pronađi komentar po ID-u
sdk.commentsTree.commentsById["comment-id"]

Регион ЕУ Internal Link


Да бисте користили ЕУ центар података, подесите поље region у вашој конфигурацији:

let config = FastCommentsWidgetConfig(
    tenantId: "YOUR_TENANT_ID",
    urlId: "my-page",
    region: "eu"
)

Ово усмјерава све API захтјеве и WebSocket везе на eu.fastcomments.com.



Чишћење Internal Link

Када завршите са SDK инстанцом (нпр. када се приказ затвара), позовите cleanup() да затворите WebSocket везу и откажете позадинске задатке:

sdk.cleanup()

За приказе које управља SwiftUI-јев @StateObject, ово се обично позива у .onDisappear или када се приказ деалокира.



Отпремање слика Internal Link

Коментари

let imageUrl = try await sdk.uploadImage(imageData: jpegData, filename: "photo.jpg")

Враћа URL отпремљене слике као низ.

Објаве фида

let mediaItem = try await feedSDK.uploadImage(imageData: jpegData, filename: "photo.jpg")

// Отпреми више слика паралелно
let mediaItems = try await feedSDK.uploadImages(images: [
    (jpegData1, "photo1.jpg"),
    (jpegData2, "photo2.jpg")
])


Помена корисника Internal Link


Претражите кориснике за аутоматско допуњавање при помињању (@mention):

let results = try await sdk.searchUsers(query: "jan")
// Враћа [UserSearchResult] са userId, username, avatar, итд.

Уграђени CommentInputBar аутоматски обавља допуњавање при помињању (@mention).



Уређивање и брисање коментара Internal Link

Измени

try await sdk.editComment(commentId: commentId, newText: "Updated text")

Сервер поново рендерује HTML. Локални коментар се аутоматски ажурира.

Избриши

try await sdk.deleteComment(commentId: commentId)

Брисање коментара такође уклања његове потомке из локалног стабла.

Обе радње су доступне преко контекстног менија коментара у корисничком интерфејсу када је тренутни корисник аутор коментара (или администратор сајта).

Обрада грешака Internal Link

SDK методе бацају FastCommentsError, који имплементира LocalizedError:

do {
    try await sdk.load()
} catch let error as FastCommentsError {
    print(error.translatedError ?? error.reason ?? "Unknown error")
} catch {
    print(error.localizedDescription)
}

FastCommentsError својства:

  • code -- код грешке из API-ја
  • reason -- опис грешке на енглеском
  • translatedError -- локализована порука о грешци обезбеђена од стране сервера

Blocking errors are also surfaced automatically via sdk.blockingErrorMessage, which the built-in views display to the user.



Локализација Internal Link


Прослиједите код локала у конфигурацији да бисте локализовали текстове које обезбјеђује сервер:

let config = FastCommentsWidgetConfig(
    tenantId: "YOUR_TENANT_ID",
    urlId: "my-page",
    locale: "fr_fr"
)

Низови корисничког интерфејса на клијентској страни користе локализацију засновану на iOS bundle-у.



Пример апликације Internal Link

Repozitorij sadrži kompletan primjer aplikacije u ExampleApp/ koji prikazuje:

  • Komentari u nitima sa SSO i prilagođenim temama
  • Društveni feed sa kreiranjem objava i filtriranjem po oznakama
  • Čat uživo
  • Jednostavni i sigurni SSO tokovi
  • Prilagođena dugmad na alatnoj traci (komentari i feed)

Trebate pomoć?

Ako naiđete na bilo kakve probleme ili imate pitanja u vezi iOS biblioteke, molimo vas:

Doprinosi

Doprinosi su dobrodošli! Posjetite GitHub repozitorij za smjernice o doprinosu.