diff --git a/.env b/.env deleted file mode 100644 index eeaf5c3..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -JWT_SIGNING_SECRET=supersecret \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bf51fbb --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +JWT_SECRET=default-secret-key +TLS_CA_PATH= +TLS_CERT_PATH= +TLS_KEY_PATH= +TLS_REQUIRE_AUTH=false + +ENABLE_DOCS=true +PORT=8080 +TLS_PORT=8443 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..73f290f --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,23 @@ +name: Lint + +on: + push: + pull_request: + +jobs: + lint: + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '~1.25' + + - name: Run linter + uses: golangci/golangci-lint-action@v7 + with: + version: v2.8.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b8495c1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: Test + +on: + push: + pull_request: + branches: + - main +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.25.5' + + - name: Test + run: go test -v ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore index f4a42ce..b4a30a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ public .DS_Store /.history -Makefile \ No newline at end of file +Makefile +certs +.env +coverage.txt \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..2058ecb --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,47 @@ +version: "2" +run: + allow-parallel-runners: true +linters: + default: none + enable: + - copyloopvar + - dupl + - errcheck + - ginkgolinter + - goconst + - gocyclo + - govet + - ineffassign + - lll + - misspell + - nakedret + - prealloc + - revive + - staticcheck + - unconvert + - unparam + - unused + settings: + revive: + rules: + - name: comment-spacings + exclusions: + generated: lax + rules: + - linters: + - dupl + - lll + path: /* + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 32aa743..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/okapi-example.iml b/.idea/okapi-example.iml deleted file mode 100644 index 5e764c4..0000000 --- a/.idea/okapi-example.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 275ce81..95c2df4 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,167 @@ # Okapi Example -A simple example demonstrating the Okapi API Framework +A comprehensive example application showcasing the Go Okapi API Framework's core features and best practices. -Okapi is a modern, minimalist HTTP web framework for Go, inspired by FastAPI's elegance. Designed for simplicity, performance, and developer happiness, it helps you build fast, scalable, and well-documented APIs with minimal boilerplate. +Okapi is a modern, minimalist HTTP web framework for Go, inspired by FastAPI's elegance. Built for simplicity, performance, and developer happiness, it enables you to build fast, scalable, and well-documented APIs with minimal boilerplate. -* [Okapi](https://github.com/jkaninda/okapi) -* [Source Code](https://github.com/jkaninda/okapi-example) -* [Docker Hub](https://hub.docker.com/r/jkaninda/okapi-example) +## 🔗 Links -## Prerequisites +- **Framework**: [Okapi on GitHub](https://github.com/jkaninda/okapi) +- **Source Code**: [okapi-example](https://github.com/jkaninda/okapi-example) +- **Docker**: [jkaninda/okapi-example](https://hub.docker.com/r/jkaninda/okapi-example) -- Go installed -- Git installed +## ✨ What's Included -## Features +This example demonstrates: -- Basic Okapi implementation example -- Okapi middlewares -- Okapi Route Definition -- Ready-to-run code structure +- **Core Framework Usage** - Basic Okapi implementation patterns +- **Middleware Integration** - Custom and built-in middleware examples +- **Route Organization** - Structured route definitions and grouping +- **Real-time Communication** - Server-Sent Events (SSE) and WebSocket implementations +- **Template Rendering** - HTML template integration +- **API Documentation** - Automatic Swagger/OpenAPI generation +- **Production-Ready Structure** - Clean, maintainable code organization -## Getting Started +## 🚀 Quick Start -### Clone the Repository +### Prerequisites + +- Go 1.21 or higher +- Git + +### Local Development ```shell +# Clone the repository git clone https://github.com/jkaninda/okapi-example cd okapi-example -``` - -### Install Dependencies -```shell +# Install dependencies go mod tidy -``` -### Run the Application - -```shell +# Run the application go run . ``` -### Using Docker +The server will start at `http://localhost:8080` + +### Docker Deployment ```shell -docker run --rm --name okapi-example -p 8080:8080 jkaninda/okapi-example +docker run --rm --name okapi-example \ + -p 8080:8080 \ + -e JWT_SIGNING_SECRET=your-secret-key \ + jkaninda/okapi-example +``` + +### With HTTPS/TLS: + +```sh +docker run --rm --name okapi-example \ + -p 8080:8080 \ + -p 8443:8443 \ + -e JWT_SECRET=your-secret-key \ + -e TLS_CERT_PATH=/certs/server.crt \ + -e TLS_KEY_PATH=/certs/server.key \ + -v /path/to/certs:/certs:ro \ + jkaninda/okapi-example ``` -Use `JWT_SIGNING_SECRET` environment variable if you want to change JWT secret, default: `supersecret` -Visit [`http://localhost:8080`](http://localhost:8080) to see the response: +## ⚙️ Configuration + +Configure the application using environment variables: + +### Security + +| Variable | Description | Default | +| ------------ | ------------------------ | -------------------- | +| `JWT_SECRET` | JWT token signing secret | `default-secret-key` | + +### TLS/HTTPS -```json -{"message": "Welcome to the Okapi Web Framework!"} +| Variable | Description | Default | +| ------------------ | ----------------------------------------- | -------------------------- | +| `TLS_CERT_PATH` | Path to TLS certificate file | _(empty - TLS disabled)_ | +| `TLS_KEY_PATH` | Path to TLS private key file | _(empty - TLS disabled)_ | +| `TLS_CA_PATH` | Path to CA certificate for mutual TLS | _(empty - no client auth)_ | +| `TLS_REQUIRE_AUTH` | Require client certificate authentication | `false` | + +### Server + +| Variable | Description | Default | +| ------------- | ------------------------------- | ------- | +| `PORT` | HTTP server port | `8080` | +| `TLS_PORT` | HTTPS server port | `8443` | +| `ENABLE_DOCS` | Enable Swagger UI documentation | `true` | + +### Example Configuration + +Create a `.env` file in the project root: + +```dotenv +# Security +JWT_SECRET=your-secret-key + +# TLS Configuration (optional) +#TLS_CERT_PATH=/path/to/cert.pem +#TLS_KEY_PATH=/path/to/key.pem +#TLS_CA_PATH=/path/to/ca.pem +#TLS_REQUIRE_AUTH=false + +# Server Configuration +ENABLE_DOCS=true +PORT=8080 +TLS_PORT=8443 ``` -Visit [`http://localhost:8080/docs/`](http://localhost:8080/docs/) to see the documentation +## 📚 Explore the Example + +Once running, visit: -## Project Structure +- **Home Page**: [http://localhost:8080](http://localhost:8080) - Welcome page with example overview +- **API Documentation**: [http://localhost:8080/docs/](http://localhost:8080/docs/) - Interactive Swagger UI + +## 📁 Project Structure ``` -. -├── main.go # Main application file -├── middlewares # Middlewares package -├── controllers # Controllers package -├── routes # Routes package -├── models # Models package -└── README.md # Project documentation +okapi-example/ +├── main.go # Application entry point and server setup +├── routes/ # API route definitions and handlers +├── middlewares/ # Custom middleware implementations +├── services/ # Business logic and service layer +├── models/ # Data models and structures +├── session/ # Session management utilities +└── README.md ``` -### Swagger UI Preview +### Architecture Highlights + +- **Layered Design**: Clear separation between routes, services, and models +- **Middleware Pipeline**: Reusable middleware for authentication, logging, and CORS +- **Service Layer**: Business logic and service layer +- **Session Management**: Built-in session handling utilities + +## 📸 Screenshots + +### Home Page + +![Home Page - Okapi Example](https://raw.githubusercontent.com/jkaninda/okapi-example/main/home-page.png) + +### Interactive API Documentation + +Okapi automatically generates comprehensive Swagger UI documentation for all your routes, making API exploration and testing effortless. -Okapi automatically generates Swagger UI for all routes: +![Swagger UI - Auto-generated API documentation](https://raw.githubusercontent.com/jkaninda/okapi-example/main/swagger.png) +## 🎯 Next Steps -![Okapi Swagger Interface](https://raw.githubusercontent.com/jkaninda/okapi-example/main/swagger.png) +After exploring this example: ---- -## License +1. **Read the Documentation**: Visit the [Okapi docs](https://github.com/jkaninda/okapi) for detailed guides +2. **Customize Routes**: Modify the routes in the `routes/` directory to fit your needs +3. **Add Features**: Extend the example with database integration, authentication, etc. +4. **Deploy**: Use the included Docker setup for production deployments -[MIT](LICENSE) - Feel free to use and modify this example. +## 📄 License +[MIT](LICENSE) - Feel free to use, modify, and distribute this example for any purpose. diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..02427da --- /dev/null +++ b/config/config.go @@ -0,0 +1,159 @@ +package config + +import ( + "fmt" + "strings" + + goutils "github.com/jkaninda/go-utils" + "github.com/jkaninda/okapi" + "github.com/jkaninda/okapi-example/session" + "github.com/jkaninda/okapi-example/utils" + "github.com/joho/godotenv" +) + +type Config struct { + Database DatabaseConfig + Redis RedisConfig + Server ServerConfig + Cors CorsConfig + JWT JWTConfig + Log LogConfig + SessionManager *session.SessionManager +} + +type DatabaseConfig struct { + dbHost string + dbUser string + dbPassword string + dbName string + dbPort int + dbSslMode string +} + +type RedisConfig struct { + URL string +} + +type ServerConfig struct { + port int + tlsPort int + environment string + enableDocs bool + tls Tls +} +type Tls struct { + Cert string + Key string + CA string + RequireAuth bool +} +type CorsConfig struct { + AllowedOrigins []string +} + +type JWTConfig struct { + Secret string +} + +type LogConfig struct { + Level string +} + +func New() *Config { + // Load .env file if it exists + _ = godotenv.Load() + return &Config{ + Database: DatabaseConfig{ + dbHost: goutils.Env("DB_HOST", "localhost"), + dbUser: goutils.Env("DB_USER", ""), + dbPassword: goutils.Env("DB_PASSWORD", ""), + dbName: goutils.Env("DB_NAME", "account"), + dbPort: goutils.EnvInt("DB_PORT", 5432), + dbSslMode: goutils.Env("DB_SSL_MODE", "disable"), + }, + Redis: RedisConfig{ + URL: goutils.Env("REDIS_URL", "redis://localhost:6379/0"), + }, + Server: ServerConfig{ + enableDocs: goutils.EnvBool("ENABLE_DOCS", true), + port: goutils.EnvInt("PORT", 8080), + tlsPort: goutils.EnvInt("TLS_PORT", 8443), + environment: goutils.Env("ENVIRONMENT", "development"), + tls: Tls{ + Cert: goutils.Env("TLS_CERT_PATH", ""), + Key: goutils.Env("TLS_KEY_PATH", ""), + // mTLS + CA: goutils.Env("TLS_CA_PATH", ""), + RequireAuth: goutils.EnvBool("TLS_REQUIRE_AUTH", false), + }, + }, + + Cors: CorsConfig{ + AllowedOrigins: strings.Split(goutils.Env("CORS_ALLOWED_ORIGINS", "http://localhost:8080"), ","), + }, + JWT: JWTConfig{ + Secret: goutils.Env("JWT_SECRET", "default-secret-key"), + }, + Log: LogConfig{ + Level: goutils.Env("LOG_LEVEL", "info"), + }, + SessionManager: session.New(), + } +} +func (c *Config) validate() error { + if c.Redis.URL == "" { + return fmt.Errorf("REDIS_URL is required") + } + if c.Server.port == 0 { + return fmt.Errorf("PORT is required") + } + return nil +} +func (c *Config) Initialize(app *okapi.Okapi) error { + if err := c.validate(); err != nil { + return err + } + + // Init Doc + if c.Server.enableDocs { + app.WithOpenAPIDocs(okapi.OpenAPI{ + Title: utils.AppName, + Version: utils.AppVersion, + License: okapi.License{ + Name: "MIT", + }, + Contact: okapi.Contact{ + Name: "Jonas Kaninda", + Email: "me@jkaninda.dev", + URL: "https://github.com/jkaninda/okapi"}, + SecuritySchemes: okapi.SecuritySchemes{ + { + Name: "bearerAuth", + Type: "http", + Scheme: "bearer", + BearerFormat: "JWT", + }, + }, + }) + } + if len(c.Cors.AllowedOrigins) > 0 { + app.WithCORS(okapi.Cors{AllowedOrigins: c.Cors.AllowedOrigins}) + } + // Init TLS server + if len(c.Server.tls.Cert) > 0 && len(c.Server.tls.Key) > 0 { + tls, err := goutils.LoadTLSConfig(c.Server.tls.Cert, c.Server.tls.Key, c.Server.tls.CA, c.Server.tls.RequireAuth) + if err != nil { + panic(err) + } else { + tlsAdd := fmt.Sprintf(":%d", c.Server.tlsPort) + // Add tls server + app.With(okapi.WithTLSServer(tlsAdd, tls)) + // Or use a single server + // app.With(okapi.WithTLS(tls)) + } + } + + addr := fmt.Sprintf(":%d", c.Server.port) + app.With(okapi.WithAddr(addr)) + return nil +} diff --git a/controllers/controller.go b/controllers/controller.go deleted file mode 100644 index 98d1214..0000000 --- a/controllers/controller.go +++ /dev/null @@ -1,167 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2025 Jonas Kaninda - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package controllers - -import ( - "encoding/json" - "fmt" - "github.com/jkaninda/logger" - "github.com/jkaninda/okapi" - "github.com/jkaninda/okapi-example/middlewares" - "github.com/jkaninda/okapi-example/models" - "net/http" - "os" - "strconv" -) - -type BookController struct{} -type HomeController struct{} -type AuthController struct{} - -var ( - books = []*models.Book{ - {Id: 1, Title: "Book One", Price: 100}, - {Id: 2, Title: "Book Two", Price: 200}, - {Id: 3, Title: "Book Three", Price: 300}, - } -) - -// ****************** Controllers ***************** - -func (hc *HomeController) Home(c okapi.Context) error { - return c.OK(okapi.M{"message": "Welcome to the Okapi Web Framework!"}) -} - -func (hc *HomeController) WhoAmI(c okapi.Context) error { - email := c.Header("current_user_email") - if email == "" { - logger.Warn("no email found") - } - return c.OK(models.WhoAmIResponse{ - Host: c.Request().Host, - RealIp: c.RealIP(), - CurrentUser: models.UserInfo{ - Name: c.Header("current_user_name"), - Email: email, - Role: c.Header("current_user_role"), - }, - }) -} -func (bc *BookController) GetBooks(c okapi.Context) error { - _, err := bc.readBooksFromFile() - if err != nil { - logger.Error("Error reading books from file", "error", err) - return c.ErrorInternalServerError(models.ErrorResponse{Success: false, Status: http.StatusInternalServerError, Details: err.Error()}) - } - return c.OK(books) -} - -func (bc *BookController) CreateBook(c okapi.Context) error { - // Simulate creating a book in a database - book := &models.Book{} - err := c.Bind(book) - if err != nil { - return c.ErrorBadRequest(models.ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()}) - } - book.Id = len(books) + 1 - books = append(books, book) - response := models.Response{ - Success: true, - Message: "Book created successfully", - Data: *book, - } - return c.OK(response) -} -func (bc *BookController) GetBook(c okapi.Context) error { - id := c.Param("id") - i, err := strconv.Atoi(id) - if err != nil { - return c.ErrorBadRequest(models.ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()}) - } - // Simulate a fetching book from a database - - _, err = bc.readBooksFromFile() - if err != nil { - logger.Error("Error reading books from file", "error", err) - return c.ErrorInternalServerError(models.ErrorResponse{Success: false, Status: http.StatusInternalServerError, Details: err.Error()}) - } - for _, book := range books { - if book.Id == i { - return c.OK(book) - } - } - return c.AbortNotFound("Book not found") -} - -// ******************** AuthController ***************** - -func (bc *AuthController) Login(c okapi.Context) error { - authRequest := &models.AuthRequest{} - err := c.Bind(authRequest) - if err != nil { - return c.ErrorBadRequest(models.ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()}) - } - // Validate the authRequest and generate a JWT token - authResponse, err := middlewares.Login(authRequest) - if err != nil { - return c.ErrorUnauthorized(authResponse) - } - return c.OK(authResponse) -} -func (bc *AuthController) WhoAmI(c okapi.Context) error { - //Get User Information from the context, shared by the JWT middleware using forwardClaims - email := c.GetString("email") - if email == "" { - return c.AbortUnauthorized("Unauthorized", fmt.Errorf("user not authenticated")) - } - - c.Response().Header().Set("X-Okapi-User", email) - c.Response().Header().Set("X-Okapi-User-Name", c.GetString("name")) - c.Response().Header().Set("X-Okapi-Role", c.GetString("role")) - // Respond with the current user information - return c.OK(models.UserInfo{ - Email: email, - Role: c.GetString("role"), - Name: c.GetString("name"), - }, - ) -} - -func (bc *BookController) readBooksFromFile() ([]*models.Book, error) { - if books != nil && len(books) > 3 { - return books, nil - } - booksFile, err := os.ReadFile("data/books.json") - if err != nil { - logger.Error("Error reading books file", "error", err) - return nil, fmt.Errorf("failed to read books data: %w", err) - } - err = json.Unmarshal(booksFile, &books) - if err != nil { - logger.Error("Error unmarshalling books data", "error", err) - return nil, fmt.Errorf("failed to parse books data: %w", err) - } - return books, nil -} diff --git a/data/book.go b/data/book.go new file mode 100644 index 0000000..804624b --- /dev/null +++ b/data/book.go @@ -0,0 +1,1424 @@ +package data + +import ( + "encoding/json" + "fmt" + + "github.com/jkaninda/logger" + "github.com/jkaninda/okapi-example/models" +) + +func Books() ([]*models.Book, error) { + books := []*models.Book{} + err := json.Unmarshal([]byte(booksData), &books) + if err != nil { + logger.Error("Error unmarshalling books data", "error", err) + return nil, fmt.Errorf("failed to parse books data: %w", err) + } + return books, nil +} + +const booksData = ` +[ + { + "id": 1, + "title": "The Kubernetes Bible", + "price": 100, + "year": 2022, + "author": "Nassim Kebbani, Piotr Tylenda", + "country": "USA", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+1", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n", + "pages": 652, + "createdAt": "2025-07-25T17:31:54.239027+02:00", + "updatedAt": "2025-07-25T17:31:54.239027+02:00" + }, + { + "id": 2, + "title": "Kubernetes - An Enterprise Guide", + "price": 110, + "year": 2022, + "author": "Marc Boorshtein, Scott Surovich", + "country": "USA", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+2", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n", + "pages": 652, + "createdAt": "2025-07-25T17:31:54.239027+02:00", + "updatedAt": "2025-07-25T17:31:54.239027+02:00" + }, + { + "id": 3, + "title": "System Design Interview Vol1", + "price": 120, + "year": 2022, + "author": "Alex U & Sahn Lam", + "country": "USA", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+3", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n", + "pages": 652, + "createdAt": "2025-07-25T17:31:54.239027+02:00", + "updatedAt": "2025-07-25T17:31:54.239027+02:00" + }, + { + "id": 4, + "title": "System Design Interview Vol2", + "price": 130, + "year": 2022, + "author": "Alex U & Sahn Lam", + "country": "USA", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+4", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n", + "pages": 652, + "createdAt": "2025-07-25T17:31:54.239027+02:00", + "updatedAt": "2025-07-25T17:31:54.239027+02:00" + }, + { + "id": 5, + "title": "Things Fall Apart", + "price": 140, + "year": 1958, + "author": "Chinua Achebe", + "country": "Nigeria", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+5", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n", + "pages": 209, + "createdAt": "2025-07-25T17:31:54.239028+02:00", + "updatedAt": "2025-07-25T17:31:54.239028+02:00" + }, + { + "id": 6, + "title": "Fairy tales", + "price": 150, + "year": 1836, + "author": "Hans Christian Andersen", + "country": "Denmark", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+6", + "language": "Danish", + "link": "https://en.wikipedia.org/wiki/Fairy_Tales_Told_for_Children._First_Collection.\n", + "pages": 784, + "createdAt": "2025-07-25T17:31:54.239028+02:00", + "updatedAt": "2025-07-25T17:31:54.239028+02:00" + }, + { + "id": 7, + "title": "The Divine Comedy", + "price": 160, + "year": 1315, + "author": "Dante Alighieri", + "country": "Italy", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+7", + "language": "Italian", + "link": "https://en.wikipedia.org/wiki/Divine_Comedy\n", + "pages": 928, + "createdAt": "2025-07-25T17:31:54.239028+02:00", + "updatedAt": "2025-07-25T17:31:54.239028+02:00" + }, + { + "id": 8, + "title": "The Epic Of Gilgamesh", + "price": 170, + "year": -1700, + "author": "Unknown", + "country": "Sumer and Akkadian Empire", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+8", + "language": "Akkadian", + "link": "https://en.wikipedia.org/wiki/Epic_of_Gilgamesh\n", + "pages": 160, + "createdAt": "2025-07-25T17:31:54.239028+02:00", + "updatedAt": "2025-07-25T17:31:54.239028+02:00" + }, + { + "id": 9, + "title": "The Book Of Job", + "price": 180, + "year": -600, + "author": "Unknown", + "country": "Achaemenid Empire", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+9", + "language": "Hebrew", + "link": "https://en.wikipedia.org/wiki/Book_of_Job\n", + "pages": 176, + "createdAt": "2025-07-25T17:31:54.239028+02:00", + "updatedAt": "2025-07-25T17:31:54.239029+02:00" + }, + { + "id": 10, + "title": "One Thousand and One Nights", + "price": 190, + "year": 1200, + "author": "Unknown", + "country": "India/Iran/Iraq/Egypt/Tajikistan", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+10", + "language": "Arabic", + "link": "https://en.wikipedia.org/wiki/One_Thousand_and_One_Nights\n", + "pages": 288, + "createdAt": "2025-07-25T17:31:54.239029+02:00", + "updatedAt": "2025-07-25T17:31:54.239029+02:00" + }, + { + "id": 11, + "title": "Nj\u00e1l's Saga", + "price": 200, + "year": 1350, + "author": "Unknown", + "country": "Iceland", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+11", + "language": "Old Norse", + "link": "https://en.wikipedia.org/wiki/Nj%C3%A1ls_saga\n", + "pages": 384, + "createdAt": "2025-07-25T17:31:54.239029+02:00", + "updatedAt": "2025-07-25T17:31:54.239029+02:00" + }, + { + "id": 12, + "title": "Pride and Prejudice", + "price": 210, + "year": 1813, + "author": "Jane Austen", + "country": "United Kingdom", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+12", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Pride_and_Prejudice\n", + "pages": 226, + "createdAt": "2025-07-25T17:31:54.239029+02:00", + "updatedAt": "2025-07-25T17:31:54.239029+02:00" + }, + { + "id": 13, + "title": "Le P\u00e8re Goriot", + "price": 220, + "year": 1835, + "author": "Honor\u00e9 de Balzac", + "country": "France", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+13", + "language": "French", + "link": "https://en.wikipedia.org/wiki/Le_P%C3%A8re_Goriot\n", + "pages": 443, + "createdAt": "2025-07-25T17:31:54.239029+02:00", + "updatedAt": "2025-07-25T17:31:54.239029+02:00" + }, + { + "id": 14, + "title": "Molloy, Malone Dies, The Unnamable, the trilogy", + "price": 230, + "year": 1952, + "author": "Samuel Beckett", + "country": "Republic of Ireland", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+14", + "language": "French, English", + "link": "https://en.wikipedia.org/wiki/Molloy_(novel)\n", + "pages": 256, + "createdAt": "2025-07-25T17:31:54.239029+02:00", + "updatedAt": "2025-07-25T17:31:54.23903+02:00" + }, + { + "id": 15, + "title": "The Decameron", + "price": 240, + "year": 1351, + "author": "Giovanni Boccaccio", + "country": "Italy", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+15", + "language": "Italian", + "link": "https://en.wikipedia.org/wiki/The_Decameron\n", + "pages": 1024, + "createdAt": "2025-07-25T17:31:54.23903+02:00", + "updatedAt": "2025-07-25T17:31:54.23903+02:00" + }, + { + "id": 16, + "title": "Ficciones", + "price": 250, + "year": 1965, + "author": "Jorge Luis Borges", + "country": "Argentina", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+16", + "language": "Spanish", + "link": "https://en.wikipedia.org/wiki/Ficciones\n", + "pages": 224, + "createdAt": "2025-07-25T17:31:54.23903+02:00", + "updatedAt": "2025-07-25T17:31:54.23903+02:00" + }, + { + "id": 17, + "title": "Wuthering Heights", + "price": 260, + "year": 1847, + "author": "Emily Bront\u00eb", + "country": "United Kingdom", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+17", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Wuthering_Heights\n", + "pages": 342, + "createdAt": "2025-07-25T17:31:54.23903+02:00", + "updatedAt": "2025-07-25T17:31:54.23903+02:00" + }, + { + "id": 18, + "title": "The Stranger", + "price": 270, + "year": 1942, + "author": "Albert Camus", + "country": "Algeria, French Empire", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+18", + "language": "French", + "link": "https://en.wikipedia.org/wiki/The_Stranger_(novel)\n", + "pages": 185, + "createdAt": "2025-07-25T17:31:54.23903+02:00", + "updatedAt": "2025-07-25T17:31:54.23903+02:00" + }, + { + "id": 19, + "title": "Poems", + "price": 280, + "year": 1952, + "author": "Paul Celan", + "country": "Romania, France", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+19", + "language": "German", + "link": "\n", + "pages": 320, + "createdAt": "2025-07-25T17:31:54.23903+02:00", + "updatedAt": "2025-07-25T17:31:54.23903+02:00" + }, + { + "id": 20, + "title": "Journey to the End of the Night", + "price": 290, + "year": 1932, + "author": "Louis-Ferdinand C\u00e9line", + "country": "France", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+20", + "language": "French", + "link": "https://en.wikipedia.org/wiki/Journey_to_the_End_of_the_Night\n", + "pages": 505, + "createdAt": "2025-07-25T17:31:54.23903+02:00", + "updatedAt": "2025-07-25T17:31:54.239031+02:00" + }, + { + "id": 21, + "title": "Don Quijote De La Mancha", + "price": 300, + "year": 1610, + "author": "Miguel de Cervantes", + "country": "Spain", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+21", + "language": "Spanish", + "link": "https://en.wikipedia.org/wiki/Don_Quixote\n", + "pages": 1056, + "createdAt": "2025-07-25T17:31:54.239031+02:00", + "updatedAt": "2025-07-25T17:31:54.239031+02:00" + }, + { + "id": 22, + "title": "The Canterbury Tales", + "price": 310, + "year": 1450, + "author": "Geoffrey Chaucer", + "country": "England", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+22", + "language": "English", + "link": "https://en.wikipedia.org/wiki/The_Canterbury_Tales\n", + "pages": 544, + "createdAt": "2025-07-25T17:31:54.239031+02:00", + "updatedAt": "2025-07-25T17:31:54.239031+02:00" + }, + { + "id": 23, + "title": "Stories", + "price": 320, + "year": 1886, + "author": "Anton Chekhov", + "country": "Russia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+23", + "language": "Russian", + "link": "https://en.wikipedia.org/wiki/List_of_short_stories_by_Anton_Chekhov\n", + "pages": 194, + "createdAt": "2025-07-25T17:31:54.239031+02:00", + "updatedAt": "2025-07-25T17:31:54.239031+02:00" + }, + { + "id": 24, + "title": "Nostromo", + "price": 330, + "year": 1904, + "author": "Joseph Conrad", + "country": "United Kingdom", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+24", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Nostromo\n", + "pages": 320, + "createdAt": "2025-07-25T17:31:54.239031+02:00", + "updatedAt": "2025-07-25T17:31:54.239031+02:00" + }, + { + "id": 25, + "title": "Great Expectations", + "price": 340, + "year": 1861, + "author": "Charles Dickens", + "country": "United Kingdom", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+25", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Great_Expectations\n", + "pages": 194, + "createdAt": "2025-07-25T17:31:54.239031+02:00", + "updatedAt": "2025-07-25T17:31:54.239031+02:00" + }, + { + "id": 26, + "title": "Jacques the Fatalist", + "price": 350, + "year": 1796, + "author": "Denis Diderot", + "country": "France", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+26", + "language": "French", + "link": "https://en.wikipedia.org/wiki/Jacques_the_Fatalist\n", + "pages": 596, + "createdAt": "2025-07-25T17:31:54.239031+02:00", + "updatedAt": "2025-07-25T17:31:54.239031+02:00" + }, + { + "id": 27, + "title": "Berlin Alexanderplatz", + "price": 360, + "year": 1929, + "author": "Alfred D\u00f6blin", + "country": "Germany", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+27", + "language": "German", + "link": "https://en.wikipedia.org/wiki/Berlin_Alexanderplatz\n", + "pages": 600, + "createdAt": "2025-07-25T17:31:54.239032+02:00", + "updatedAt": "2025-07-25T17:31:54.239032+02:00" + }, + { + "id": 28, + "title": "Crime and Punishment", + "price": 370, + "year": 1866, + "author": "Fyodor Dostoevsky", + "country": "Russia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+28", + "language": "Russian", + "link": "https://en.wikipedia.org/wiki/Crime_and_Punishment\n", + "pages": 551, + "createdAt": "2025-07-25T17:31:54.239032+02:00", + "updatedAt": "2025-07-25T17:31:54.239032+02:00" + }, + { + "id": 29, + "title": "The Idiot", + "price": 380, + "year": 1869, + "author": "Fyodor Dostoevsky", + "country": "Russia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+29", + "language": "Russian", + "link": "https://en.wikipedia.org/wiki/The_Idiot\n", + "pages": 656, + "createdAt": "2025-07-25T17:31:54.239032+02:00", + "updatedAt": "2025-07-25T17:31:54.239032+02:00" + }, + { + "id": 30, + "title": "The Possessed", + "price": 390, + "year": 1872, + "author": "Fyodor Dostoevsky", + "country": "Russia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+30", + "language": "Russian", + "link": "https://en.wikipedia.org/wiki/Demons_(Dostoyevsky_novel)\n", + "pages": 768, + "createdAt": "2025-07-25T17:31:54.239032+02:00", + "updatedAt": "2025-07-25T17:31:54.239032+02:00" + }, + { + "id": 31, + "title": "The Brothers Karamazov", + "price": 400, + "year": 1880, + "author": "Fyodor Dostoevsky", + "country": "Russia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+31", + "language": "Russian", + "link": "https://en.wikipedia.org/wiki/The_Brothers_Karamazov\n", + "pages": 824, + "createdAt": "2025-07-25T17:31:54.239032+02:00", + "updatedAt": "2025-07-25T17:31:54.239032+02:00" + }, + { + "id": 32, + "title": "Middlemarch", + "price": 410, + "year": 1871, + "author": "George Eliot", + "country": "United Kingdom", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+32", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Middlemarch\n", + "pages": 800, + "createdAt": "2025-07-25T17:31:54.239032+02:00", + "updatedAt": "2025-07-25T17:31:54.239032+02:00" + }, + { + "id": 33, + "title": "Invisible Man", + "price": 420, + "year": 1952, + "author": "Ralph Ellison", + "country": "United States", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+33", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Invisible_Man\n", + "pages": 581, + "createdAt": "2025-07-25T17:31:54.239032+02:00", + "updatedAt": "2025-07-25T17:31:54.239033+02:00" + }, + { + "id": 34, + "title": "Medea", + "price": 430, + "year": -431, + "author": "Euripides", + "country": "Greece", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+34", + "language": "Greek", + "link": "https://en.wikipedia.org/wiki/Medea_(play)\n", + "pages": 104, + "createdAt": "2025-07-25T17:31:54.239033+02:00", + "updatedAt": "2025-07-25T17:31:54.239033+02:00" + }, + { + "id": 35, + "title": "Absalom, Absalom!", + "price": 440, + "year": 1936, + "author": "William Faulkner", + "country": "United States", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+35", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Absalom,_Absalom!\n", + "pages": 313, + "createdAt": "2025-07-25T17:31:54.239033+02:00", + "updatedAt": "2025-07-25T17:31:54.239033+02:00" + }, + { + "id": 36, + "title": "The Sound and the Fury", + "price": 450, + "year": 1929, + "author": "William Faulkner", + "country": "United States", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+36", + "language": "English", + "link": "https://en.wikipedia.org/wiki/The_Sound_and_the_Fury\n", + "pages": 326, + "createdAt": "2025-07-25T17:31:54.239033+02:00", + "updatedAt": "2025-07-25T17:31:54.239033+02:00" + }, + { + "id": 37, + "title": "Madame Bovary", + "price": 460, + "year": 1857, + "author": "Gustave Flaubert", + "country": "France", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+37", + "language": "French", + "link": "https://en.wikipedia.org/wiki/Madame_Bovary\n", + "pages": 528, + "createdAt": "2025-07-25T17:31:54.239033+02:00", + "updatedAt": "2025-07-25T17:31:54.239033+02:00" + }, + { + "id": 38, + "title": "Sentimental Education", + "price": 470, + "year": 1869, + "author": "Gustave Flaubert", + "country": "France", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+38", + "language": "French", + "link": "https://en.wikipedia.org/wiki/Sentimental_Education\n", + "pages": 606, + "createdAt": "2025-07-25T17:31:54.239033+02:00", + "updatedAt": "2025-07-25T17:31:54.239033+02:00" + }, + { + "id": 39, + "title": "Gypsy Ballads", + "price": 480, + "year": 1928, + "author": "Federico Garc\u00eda Lorca", + "country": "Spain", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+39", + "language": "Spanish", + "link": "https://en.wikipedia.org/wiki/Gypsy_Ballads\n", + "pages": 218, + "createdAt": "2025-07-25T17:31:54.239033+02:00", + "updatedAt": "2025-07-25T17:31:54.239034+02:00" + }, + { + "id": 40, + "title": "One Hundred Years of Solitude", + "price": 490, + "year": 1967, + "author": "Gabriel Garc\u00eda M\u00e1rquez", + "country": "Colombia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+40", + "language": "Spanish", + "link": "https://en.wikipedia.org/wiki/One_Hundred_Years_of_Solitude\n", + "pages": 417, + "createdAt": "2025-07-25T17:31:54.239034+02:00", + "updatedAt": "2025-07-25T17:31:54.239034+02:00" + }, + { + "id": 41, + "title": "Love in the Time of Cholera", + "price": 500, + "year": 1985, + "author": "Gabriel Garc\u00eda M\u00e1rquez", + "country": "Colombia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+41", + "language": "Spanish", + "link": "https://en.wikipedia.org/wiki/Love_in_the_Time_of_Cholera\n", + "pages": 368, + "createdAt": "2025-07-25T17:31:54.239034+02:00", + "updatedAt": "2025-07-25T17:31:54.239034+02:00" + }, + { + "id": 42, + "title": "Faust", + "price": 510, + "year": 1832, + "author": "Johann Wolfgang von Goethe", + "country": "Saxe-Weimar", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+42", + "language": "German", + "link": "https://en.wikipedia.org/wiki/Goethe%27s_Faust\n", + "pages": 158, + "createdAt": "2025-07-25T17:31:54.239034+02:00", + "updatedAt": "2025-07-25T17:31:54.239034+02:00" + }, + { + "id": 43, + "title": "Dead Souls", + "price": 520, + "year": 1842, + "author": "Nikolai Gogol", + "country": "Russia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+43", + "language": "Russian", + "link": "https://en.wikipedia.org/wiki/Dead_Souls\n", + "pages": 432, + "createdAt": "2025-07-25T17:31:54.239034+02:00", + "updatedAt": "2025-07-25T17:31:54.239034+02:00" + }, + { + "id": 44, + "title": "The Tin Drum", + "price": 530, + "year": 1959, + "author": "G\u00fcnter Grass", + "country": "Germany", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+44", + "language": "German", + "link": "https://en.wikipedia.org/wiki/The_Tin_Drum\n", + "pages": 600, + "createdAt": "2025-07-25T17:31:54.239034+02:00", + "updatedAt": "2025-07-25T17:31:54.239034+02:00" + }, + { + "id": 45, + "title": "The Devil to Pay in the Backlands", + "price": 540, + "year": 1956, + "author": "Jo\u00e3o Guimar\u00e3es Rosa", + "country": "Brazil", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+45", + "language": "Portuguese", + "link": "https://en.wikipedia.org/wiki/The_Devil_to_Pay_in_the_Backlands\n", + "pages": 494, + "createdAt": "2025-07-25T17:31:54.239034+02:00", + "updatedAt": "2025-07-25T17:31:54.239034+02:00" + }, + { + "id": 46, + "title": "Hunger", + "price": 550, + "year": 1890, + "author": "Knut Hamsun", + "country": "Norway", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+46", + "language": "Norwegian", + "link": "https://en.wikipedia.org/wiki/Hunger_(Hamsun_novel)\n", + "pages": 176, + "createdAt": "2025-07-25T17:31:54.239035+02:00", + "updatedAt": "2025-07-25T17:31:54.239035+02:00" + }, + { + "id": 47, + "title": "The Old Man and the Sea", + "price": 560, + "year": 1952, + "author": "Ernest Hemingway", + "country": "United States", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+47", + "language": "English", + "link": "https://en.wikipedia.org/wiki/The_Old_Man_and_the_Sea\n", + "pages": 128, + "createdAt": "2025-07-25T17:31:54.239035+02:00", + "updatedAt": "2025-07-25T17:31:54.239035+02:00" + }, + { + "id": 48, + "title": "Iliad", + "price": 570, + "year": -735, + "author": "Homer", + "country": "Greece", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+48", + "language": "Greek", + "link": "https://en.wikipedia.org/wiki/Iliad\n", + "pages": 608, + "createdAt": "2025-07-25T17:31:54.239035+02:00", + "updatedAt": "2025-07-25T17:31:54.239035+02:00" + }, + { + "id": 49, + "title": "Odyssey", + "price": 580, + "year": -800, + "author": "Homer", + "country": "Greece", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+49", + "language": "Greek", + "link": "https://en.wikipedia.org/wiki/Odyssey\n", + "pages": 374, + "createdAt": "2025-07-25T17:31:54.239035+02:00", + "updatedAt": "2025-07-25T17:31:54.239035+02:00" + }, + { + "id": 50, + "title": "A Doll's House", + "price": 590, + "year": 1879, + "author": "Henrik Ibsen", + "country": "Norway", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+50", + "language": "Norwegian", + "link": "https://en.wikipedia.org/wiki/A_Doll%27s_House\n", + "pages": 68, + "createdAt": "2025-07-25T17:31:54.239035+02:00", + "updatedAt": "2025-07-25T17:31:54.239035+02:00" + }, + { + "id": 51, + "title": "Ulysses", + "price": 600, + "year": 1922, + "author": "James Joyce", + "country": "Irish Free State", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+51", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Ulysses_(novel)\n", + "pages": 228, + "createdAt": "2025-07-25T17:31:54.239035+02:00", + "updatedAt": "2025-07-25T17:31:54.239035+02:00" + }, + { + "id": 52, + "title": "Stories", + "price": 610, + "year": 1924, + "author": "Franz Kafka", + "country": "Czechoslovakia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+52", + "language": "German", + "link": "https://en.wikipedia.org/wiki/Franz_Kafka_bibliography#Short_stories\n", + "pages": 488, + "createdAt": "2025-07-25T17:31:54.239035+02:00", + "updatedAt": "2025-07-25T17:31:54.239036+02:00" + }, + { + "id": 53, + "title": "The Trial", + "price": 620, + "year": 1925, + "author": "Franz Kafka", + "country": "Czechoslovakia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+53", + "language": "German", + "link": "https://en.wikipedia.org/wiki/The_Trial\n", + "pages": 160, + "createdAt": "2025-07-25T17:31:54.239036+02:00", + "updatedAt": "2025-07-25T17:31:54.239036+02:00" + }, + { + "id": 54, + "title": "The Castle", + "price": 630, + "year": 1926, + "author": "Franz Kafka", + "country": "Czechoslovakia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+54", + "language": "German", + "link": "https://en.wikipedia.org/wiki/The_Castle_(novel)\n", + "pages": 352, + "createdAt": "2025-07-25T17:31:54.239036+02:00", + "updatedAt": "2025-07-25T17:31:54.239036+02:00" + }, + { + "id": 55, + "title": "The recognition of Shakuntala", + "price": 640, + "year": 150, + "author": "K\u0101lid\u0101sa", + "country": "India", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+55", + "language": "Sanskrit", + "link": "https://en.wikipedia.org/wiki/Abhij%C3%B1%C4%81na%C5%9B%C4%81kuntalam\n", + "pages": 147, + "createdAt": "2025-07-25T17:31:54.239036+02:00", + "updatedAt": "2025-07-25T17:31:54.239036+02:00" + }, + { + "id": 56, + "title": "The Sound of the Mountain", + "price": 650, + "year": 1954, + "author": "Yasunari Kawabata", + "country": "Japan", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+56", + "language": "Japanese", + "link": "https://en.wikipedia.org/wiki/The_Sound_of_the_Mountain\n", + "pages": 288, + "createdAt": "2025-07-25T17:31:54.239036+02:00", + "updatedAt": "2025-07-25T17:31:54.239036+02:00" + }, + { + "id": 57, + "title": "Zorba the Greek", + "price": 660, + "year": 1946, + "author": "Nikos Kazantzakis", + "country": "Greece", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+57", + "language": "Greek", + "link": "https://en.wikipedia.org/wiki/Zorba_the_Greek\n", + "pages": 368, + "createdAt": "2025-07-25T17:31:54.239036+02:00", + "updatedAt": "2025-07-25T17:31:54.239036+02:00" + }, + { + "id": 58, + "title": "Sons and Lovers", + "price": 670, + "year": 1913, + "author": "D. H. Lawrence", + "country": "United Kingdom", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+58", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Sons_and_Lovers\n", + "pages": 432, + "createdAt": "2025-07-25T17:31:54.239036+02:00", + "updatedAt": "2025-07-25T17:31:54.239037+02:00" + }, + { + "id": 59, + "title": "Independent People", + "price": 680, + "year": 1934, + "author": "Halld\u00f3r Laxness", + "country": "Iceland", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+59", + "language": "Icelandic", + "link": "https://en.wikipedia.org/wiki/Independent_People\n", + "pages": 470, + "createdAt": "2025-07-25T17:31:54.239037+02:00", + "updatedAt": "2025-07-25T17:31:54.239037+02:00" + }, + { + "id": 60, + "title": "Poems", + "price": 690, + "year": 1818, + "author": "Giacomo Leopardi", + "country": "Italy", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+60", + "language": "Italian", + "link": "\n", + "pages": 184, + "createdAt": "2025-07-25T17:31:54.239037+02:00", + "updatedAt": "2025-07-25T17:31:54.239037+02:00" + }, + { + "id": 61, + "title": "The Golden Notebook", + "price": 700, + "year": 1962, + "author": "Doris Lessing", + "country": "United Kingdom", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+61", + "language": "English", + "link": "https://en.wikipedia.org/wiki/The_Golden_Notebook\n", + "pages": 688, + "createdAt": "2025-07-25T17:31:54.239037+02:00", + "updatedAt": "2025-07-25T17:31:54.239037+02:00" + }, + { + "id": 62, + "title": "Pippi Longstocking", + "price": 710, + "year": 1945, + "author": "Astrid Lindgren", + "country": "Sweden", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+62", + "language": "Swedish", + "link": "https://en.wikipedia.org/wiki/Pippi_Longstocking\n", + "pages": 160, + "createdAt": "2025-07-25T17:31:54.239037+02:00", + "updatedAt": "2025-07-25T17:31:54.239037+02:00" + }, + { + "id": 63, + "title": "Diary of a Madman", + "price": 720, + "year": 1918, + "author": "Lu Xun", + "country": "China", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+63", + "language": "Chinese", + "link": "https://en.wikipedia.org/wiki/A_Madman%27s_Diary\n", + "pages": 389, + "createdAt": "2025-07-25T17:31:54.239037+02:00", + "updatedAt": "2025-07-25T17:31:54.239037+02:00" + }, + { + "id": 64, + "title": "Children of Gebelawi", + "price": 730, + "year": 1959, + "author": "Naguib Mahfouz", + "country": "Egypt", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+64", + "language": "Arabic", + "link": "https://en.wikipedia.org/wiki/Children_of_Gebelawi\n", + "pages": 355, + "createdAt": "2025-07-25T17:31:54.239037+02:00", + "updatedAt": "2025-07-25T17:31:54.239037+02:00" + }, + { + "id": 65, + "title": "Buddenbrooks", + "price": 740, + "year": 1901, + "author": "Thomas Mann", + "country": "Germany", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+65", + "language": "German", + "link": "https://en.wikipedia.org/wiki/Buddenbrooks\n", + "pages": 736, + "createdAt": "2025-07-25T17:31:54.239038+02:00", + "updatedAt": "2025-07-25T17:31:54.239038+02:00" + }, + { + "id": 66, + "title": "The Magic Mountain", + "price": 750, + "year": 1924, + "author": "Thomas Mann", + "country": "Germany", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+66", + "language": "German", + "link": "https://en.wikipedia.org/wiki/The_Magic_Mountain\n", + "pages": 720, + "createdAt": "2025-07-25T17:31:54.239038+02:00", + "updatedAt": "2025-07-25T17:31:54.239038+02:00" + }, + { + "id": 67, + "title": "Moby Dick", + "price": 760, + "year": 1851, + "author": "Herman Melville", + "country": "United States", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+67", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Moby-Dick\n", + "pages": 378, + "createdAt": "2025-07-25T17:31:54.239038+02:00", + "updatedAt": "2025-07-25T17:31:54.239038+02:00" + }, + { + "id": 68, + "title": "Essays", + "price": 770, + "year": 1595, + "author": "Michel de Montaigne", + "country": "France", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+68", + "language": "French", + "link": "https://en.wikipedia.org/wiki/Essays_(Montaigne)\n", + "pages": 404, + "createdAt": "2025-07-25T17:31:54.239038+02:00", + "updatedAt": "2025-07-25T17:31:54.239038+02:00" + }, + { + "id": 69, + "title": "History", + "price": 780, + "year": 1974, + "author": "Elsa Morante", + "country": "Italy", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+69", + "language": "Italian", + "link": "https://en.wikipedia.org/wiki/History_(novel)\n", + "pages": 600, + "createdAt": "2025-07-25T17:31:54.239038+02:00", + "updatedAt": "2025-07-25T17:31:54.239038+02:00" + }, + { + "id": 70, + "title": "Beloved", + "price": 790, + "year": 1987, + "author": "Toni Morrison", + "country": "United States", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+70", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Beloved_(novel)\n", + "pages": 321, + "createdAt": "2025-07-25T17:31:54.239038+02:00", + "updatedAt": "2025-07-25T17:31:54.239038+02:00" + }, + { + "id": 71, + "title": "The Tale of Genji", + "price": 800, + "year": 1006, + "author": "Murasaki Shikibu", + "country": "Japan", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+71", + "language": "Japanese", + "link": "https://en.wikipedia.org/wiki/The_Tale_of_Genji\n", + "pages": 1360, + "createdAt": "2025-07-25T17:31:54.239038+02:00", + "updatedAt": "2025-07-25T17:31:54.239039+02:00" + }, + { + "id": 72, + "title": "The Man Without Qualities", + "price": 810, + "year": 1931, + "author": "Robert Musil", + "country": "Austria", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+72", + "language": "German", + "link": "https://en.wikipedia.org/wiki/The_Man_Without_Qualities\n", + "pages": 365, + "createdAt": "2025-07-25T17:31:54.239039+02:00", + "updatedAt": "2025-07-25T17:31:54.239039+02:00" + }, + { + "id": 73, + "title": "Lolita", + "price": 820, + "year": 1955, + "author": "Vladimir Nabokov", + "country": "Russia/United States", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+73", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Lolita\n", + "pages": 317, + "createdAt": "2025-07-25T17:31:54.239039+02:00", + "updatedAt": "2025-07-25T17:31:54.239039+02:00" + }, + { + "id": 74, + "title": "Nineteen Eighty-Four", + "price": 830, + "year": 1949, + "author": "George Orwell", + "country": "United Kingdom", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+74", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Nineteen_Eighty-Four\n", + "pages": 272, + "createdAt": "2025-07-25T17:31:54.239039+02:00", + "updatedAt": "2025-07-25T17:31:54.239039+02:00" + }, + { + "id": 75, + "title": "Metamorphoses", + "price": 840, + "year": 100, + "author": "Ovid", + "country": "Roman Empire", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+75", + "language": "Classical Latin", + "link": "https://en.wikipedia.org/wiki/Metamorphoses\n", + "pages": 576, + "createdAt": "2025-07-25T17:31:54.239039+02:00", + "updatedAt": "2025-07-25T17:31:54.239039+02:00" + }, + { + "id": 76, + "title": "The Book of Disquiet", + "price": 850, + "year": 1928, + "author": "Fernando Pessoa", + "country": "Portugal", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+76", + "language": "Portuguese", + "link": "https://en.wikipedia.org/wiki/The_Book_of_Disquiet\n", + "pages": 272, + "createdAt": "2025-07-25T17:31:54.239039+02:00", + "updatedAt": "2025-07-25T17:31:54.239039+02:00" + }, + { + "id": 77, + "title": "Tales", + "price": 860, + "year": 1950, + "author": "Edgar Allan Poe", + "country": "United States", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+77", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Edgar_Allan_Poe_bibliography#Tales\n", + "pages": 842, + "createdAt": "2025-07-25T17:31:54.239039+02:00", + "updatedAt": "2025-07-25T17:31:54.23904+02:00" + }, + { + "id": 78, + "title": "In Search of Lost Time", + "price": 870, + "year": 1920, + "author": "Marcel Proust", + "country": "France", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+78", + "language": "French", + "link": "https://en.wikipedia.org/wiki/In_Search_of_Lost_Time\n", + "pages": 2408, + "createdAt": "2025-07-25T17:31:54.23904+02:00", + "updatedAt": "2025-07-25T17:31:54.23904+02:00" + }, + { + "id": 79, + "title": "Gargantua and Pantagruel", + "price": 880, + "year": 1533, + "author": "Fran\u00e7ois Rabelais", + "country": "France", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+79", + "language": "French", + "link": "https://en.wikipedia.org/wiki/Gargantua_and_Pantagruel\n", + "pages": 623, + "createdAt": "2025-07-25T17:31:54.23904+02:00", + "updatedAt": "2025-07-25T17:31:54.23904+02:00" + }, + { + "id": 80, + "title": "Pedro P\u00e1ramo", + "price": 890, + "year": 1955, + "author": "Juan Rulfo", + "country": "Mexico", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+80", + "language": "Spanish", + "link": "https://en.wikipedia.org/wiki/Pedro_P%C3%A1ramo\n", + "pages": 124, + "createdAt": "2025-07-25T17:31:54.23904+02:00", + "updatedAt": "2025-07-25T17:31:54.23904+02:00" + }, + { + "id": 81, + "title": "The Masnavi", + "price": 900, + "year": 1236, + "author": "Rumi", + "country": "Sultanate of Rum", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+81", + "language": "Persian", + "link": "https://en.wikipedia.org/wiki/Masnavi\n", + "pages": 438, + "createdAt": "2025-07-25T17:31:54.23904+02:00", + "updatedAt": "2025-07-25T17:31:54.23904+02:00" + }, + { + "id": 82, + "title": "Midnight's Children", + "price": 910, + "year": 1981, + "author": "Salman Rushdie", + "country": "United Kingdom, India", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+82", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Midnight%27s_Children\n", + "pages": 536, + "createdAt": "2025-07-25T17:31:54.23904+02:00", + "updatedAt": "2025-07-25T17:31:54.23904+02:00" + }, + { + "id": 83, + "title": "Bostan", + "price": 920, + "year": 1257, + "author": "Saadi", + "country": "Persia, Persian Empire", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+83", + "language": "Persian", + "link": "https://en.wikipedia.org/wiki/Bustan_(book)\n", + "pages": 298, + "createdAt": "2025-07-25T17:31:54.23904+02:00", + "updatedAt": "2025-07-25T17:31:54.23904+02:00" + }, + { + "id": 84, + "title": "Season of Migration to the North", + "price": 930, + "year": 1966, + "author": "Tayeb Salih", + "country": "Sudan", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+84", + "language": "Arabic", + "link": "https://en.wikipedia.org/wiki/Season_of_Migration_to_the_North\n", + "pages": 139, + "createdAt": "2025-07-25T17:31:54.239041+02:00", + "updatedAt": "2025-07-25T17:31:54.239041+02:00" + }, + { + "id": 85, + "title": "Blindness", + "price": 940, + "year": 1995, + "author": "Jos\u00e9 Saramago", + "country": "Portugal", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+85", + "language": "Portuguese", + "link": "https://en.wikipedia.org/wiki/Blindness_(novel)\n", + "pages": 352, + "createdAt": "2025-07-25T17:31:54.239041+02:00", + "updatedAt": "2025-07-25T17:31:54.239041+02:00" + }, + { + "id": 86, + "title": "Hamlet", + "price": 950, + "year": 1603, + "author": "William Shakespeare", + "country": "England", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+86", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Hamlet\n", + "pages": 432, + "createdAt": "2025-07-25T17:31:54.239041+02:00", + "updatedAt": "2025-07-25T17:31:54.239041+02:00" + }, + { + "id": 87, + "title": "King Lear", + "price": 960, + "year": 1608, + "author": "William Shakespeare", + "country": "England", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+87", + "language": "English", + "link": "https://en.wikipedia.org/wiki/King_Lear\n", + "pages": 384, + "createdAt": "2025-07-25T17:31:54.239041+02:00", + "updatedAt": "2025-07-25T17:31:54.239041+02:00" + }, + { + "id": 88, + "title": "Othello", + "price": 970, + "year": 1609, + "author": "William Shakespeare", + "country": "England", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+88", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Othello\n", + "pages": 314, + "createdAt": "2025-07-25T17:31:54.239041+02:00", + "updatedAt": "2025-07-25T17:31:54.239041+02:00" + }, + { + "id": 89, + "title": "Oedipus the King", + "price": 980, + "year": -430, + "author": "Sophocles", + "country": "Greece", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+89", + "language": "Greek", + "link": "https://en.wikipedia.org/wiki/Oedipus_the_King\n", + "pages": 88, + "createdAt": "2025-07-25T17:31:54.239041+02:00", + "updatedAt": "2025-07-25T17:31:54.239041+02:00" + }, + { + "id": 90, + "title": "The Red and the Black", + "price": 990, + "year": 1830, + "author": "Stendhal", + "country": "France", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+90", + "language": "French", + "link": "https://en.wikipedia.org/wiki/The_Red_and_the_Black\n", + "pages": 576, + "createdAt": "2025-07-25T17:31:54.239041+02:00", + "updatedAt": "2025-07-25T17:31:54.239042+02:00" + }, + { + "id": 91, + "title": "The Life And Opinions of Tristram Shandy", + "price": 1000, + "year": 1760, + "author": "Laurence Sterne", + "country": "England", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+91", + "language": "English", + "link": "https://en.wikipedia.org/wiki/The_Life_and_Opinions_of_Tristram_Shandy,_Gentleman\n", + "pages": 640, + "createdAt": "2025-07-25T17:31:54.239042+02:00", + "updatedAt": "2025-07-25T17:31:54.239042+02:00" + }, + { + "id": 92, + "title": "Confessions of Zeno", + "price": 1010, + "year": 1923, + "author": "Italo Svevo", + "country": "Italy", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+92", + "language": "Italian", + "link": "https://en.wikipedia.org/wiki/Zeno%27s_Conscience\n", + "pages": 412, + "createdAt": "2025-07-25T17:31:54.239042+02:00", + "updatedAt": "2025-07-25T17:31:54.239042+02:00" + }, + { + "id": 93, + "title": "Gulliver's Travels", + "price": 1020, + "year": 1726, + "author": "Jonathan Swift", + "country": "Ireland", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+93", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Gulliver%27s_Travels\n", + "pages": 178, + "createdAt": "2025-07-25T17:31:54.239042+02:00", + "updatedAt": "2025-07-25T17:31:54.239042+02:00" + }, + { + "id": 94, + "title": "War and Peace", + "price": 1030, + "year": 1867, + "author": "Leo Tolstoy", + "country": "Russia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+94", + "language": "Russian", + "link": "https://en.wikipedia.org/wiki/War_and_Peace\n", + "pages": 1296, + "createdAt": "2025-07-25T17:31:54.239042+02:00", + "updatedAt": "2025-07-25T17:31:54.239042+02:00" + }, + { + "id": 95, + "title": "Anna Karenina", + "price": 1040, + "year": 1877, + "author": "Leo Tolstoy", + "country": "Russia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+95", + "language": "Russian", + "link": "https://en.wikipedia.org/wiki/Anna_Karenina\n", + "pages": 864, + "createdAt": "2025-07-25T17:31:54.239042+02:00", + "updatedAt": "2025-07-25T17:31:54.239042+02:00" + }, + { + "id": 96, + "title": "The Death of Ivan Ilyich", + "price": 1050, + "year": 1886, + "author": "Leo Tolstoy", + "country": "Russia", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+96", + "language": "Russian", + "link": "https://en.wikipedia.org/wiki/The_Death_of_Ivan_Ilyich\n", + "pages": 92, + "createdAt": "2025-07-25T17:31:54.239042+02:00", + "updatedAt": "2025-07-25T17:31:54.239043+02:00" + }, + { + "id": 97, + "title": "The Adventures of Huckleberry Finn", + "price": 1060, + "year": 1884, + "author": "Mark Twain", + "country": "United States", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+97", + "language": "English", + "link": "https://en.wikipedia.org/wiki/Adventures_of_Huckleberry_Finn\n", + "pages": 224, + "createdAt": "2025-07-25T17:31:54.239043+02:00", + "updatedAt": "2025-07-25T17:31:54.239043+02:00" + }, + { + "id": 98, + "title": "Ramayana", + "price": 1070, + "year": -450, + "author": "Valmiki", + "country": "India", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+98", + "language": "Sanskrit", + "link": "https://en.wikipedia.org/wiki/Ramayana\n", + "pages": 152, + "createdAt": "2025-07-25T17:31:54.239043+02:00", + "updatedAt": "2025-07-25T17:31:54.239043+02:00" + }, + { + "id": 99, + "title": "The Aeneid", + "price": 1080, + "year": -23, + "author": "Virgil", + "country": "Roman Empire", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+99", + "language": "Classical Latin", + "link": "https://en.wikipedia.org/wiki/Aeneid\n", + "pages": 442, + "createdAt": "2025-07-25T17:31:54.239043+02:00", + "updatedAt": "2025-07-25T17:31:54.239043+02:00" + }, + { + "id": 100, + "title": "Mahabharata", + "price": 1090, + "year": -700, + "author": "Vyasa", + "country": "India", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+100", + "language": "Sanskrit", + "link": "https://en.wikipedia.org/wiki/Mahabharata\n", + "pages": 276, + "createdAt": "2025-07-25T17:31:54.239043+02:00", + "updatedAt": "2025-07-25T17:31:54.239043+02:00" + } +] +` diff --git a/data/books.json b/data/books.json index 8b2b656..87109c4 100644 --- a/data/books.json +++ b/data/books.json @@ -6,7 +6,7 @@ "year": 2022, "author": "Nassim Kebbani, Piotr Tylenda", "country": "USA", - "imageLink": "images/things-fall-apart.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+1", "language": "English", "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n", "pages": 652, @@ -20,7 +20,7 @@ "year": 2022, "author": "Marc Boorshtein, Scott Surovich", "country": "USA", - "imageLink": "images/things-fall-apart.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+2", "language": "English", "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n", "pages": 652, @@ -34,7 +34,7 @@ "year": 2022, "author": "Alex U & Sahn Lam", "country": "USA", - "imageLink": "images/things-fall-apart.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+3", "language": "English", "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n", "pages": 652, @@ -48,7 +48,7 @@ "year": 2022, "author": "Alex U & Sahn Lam", "country": "USA", - "imageLink": "images/things-fall-apart.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+4", "language": "English", "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n", "pages": 652, @@ -62,7 +62,7 @@ "year": 1958, "author": "Chinua Achebe", "country": "Nigeria", - "imageLink": "images/things-fall-apart.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+5", "language": "English", "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n", "pages": 209, @@ -76,7 +76,7 @@ "year": 1836, "author": "Hans Christian Andersen", "country": "Denmark", - "imageLink": "images/fairy-tales.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+6", "language": "Danish", "link": "https://en.wikipedia.org/wiki/Fairy_Tales_Told_for_Children._First_Collection.\n", "pages": 784, @@ -90,7 +90,7 @@ "year": 1315, "author": "Dante Alighieri", "country": "Italy", - "imageLink": "images/the-divine-comedy.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+7", "language": "Italian", "link": "https://en.wikipedia.org/wiki/Divine_Comedy\n", "pages": 928, @@ -104,7 +104,7 @@ "year": -1700, "author": "Unknown", "country": "Sumer and Akkadian Empire", - "imageLink": "images/the-epic-of-gilgamesh.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+8", "language": "Akkadian", "link": "https://en.wikipedia.org/wiki/Epic_of_Gilgamesh\n", "pages": 160, @@ -118,7 +118,7 @@ "year": -600, "author": "Unknown", "country": "Achaemenid Empire", - "imageLink": "images/the-book-of-job.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+9", "language": "Hebrew", "link": "https://en.wikipedia.org/wiki/Book_of_Job\n", "pages": 176, @@ -132,7 +132,7 @@ "year": 1200, "author": "Unknown", "country": "India/Iran/Iraq/Egypt/Tajikistan", - "imageLink": "images/one-thousand-and-one-nights.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+10", "language": "Arabic", "link": "https://en.wikipedia.org/wiki/One_Thousand_and_One_Nights\n", "pages": 288, @@ -141,12 +141,12 @@ }, { "id": 11, - "title": "Njál's Saga", + "title": "Nj\u00e1l's Saga", "price": 200, "year": 1350, "author": "Unknown", "country": "Iceland", - "imageLink": "images/njals-saga.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+11", "language": "Old Norse", "link": "https://en.wikipedia.org/wiki/Nj%C3%A1ls_saga\n", "pages": 384, @@ -160,7 +160,7 @@ "year": 1813, "author": "Jane Austen", "country": "United Kingdom", - "imageLink": "images/pride-and-prejudice.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+12", "language": "English", "link": "https://en.wikipedia.org/wiki/Pride_and_Prejudice\n", "pages": 226, @@ -169,12 +169,12 @@ }, { "id": 13, - "title": "Le Père Goriot", + "title": "Le P\u00e8re Goriot", "price": 220, "year": 1835, - "author": "Honoré de Balzac", + "author": "Honor\u00e9 de Balzac", "country": "France", - "imageLink": "images/le-pere-goriot.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+13", "language": "French", "link": "https://en.wikipedia.org/wiki/Le_P%C3%A8re_Goriot\n", "pages": 443, @@ -188,7 +188,7 @@ "year": 1952, "author": "Samuel Beckett", "country": "Republic of Ireland", - "imageLink": "images/molloy-malone-dies-the-unnamable.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+14", "language": "French, English", "link": "https://en.wikipedia.org/wiki/Molloy_(novel)\n", "pages": 256, @@ -202,7 +202,7 @@ "year": 1351, "author": "Giovanni Boccaccio", "country": "Italy", - "imageLink": "images/the-decameron.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+15", "language": "Italian", "link": "https://en.wikipedia.org/wiki/The_Decameron\n", "pages": 1024, @@ -216,7 +216,7 @@ "year": 1965, "author": "Jorge Luis Borges", "country": "Argentina", - "imageLink": "images/ficciones.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+16", "language": "Spanish", "link": "https://en.wikipedia.org/wiki/Ficciones\n", "pages": 224, @@ -228,9 +228,9 @@ "title": "Wuthering Heights", "price": 260, "year": 1847, - "author": "Emily Brontë", + "author": "Emily Bront\u00eb", "country": "United Kingdom", - "imageLink": "images/wuthering-heights.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+17", "language": "English", "link": "https://en.wikipedia.org/wiki/Wuthering_Heights\n", "pages": 342, @@ -244,7 +244,7 @@ "year": 1942, "author": "Albert Camus", "country": "Algeria, French Empire", - "imageLink": "images/l-etranger.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+18", "language": "French", "link": "https://en.wikipedia.org/wiki/The_Stranger_(novel)\n", "pages": 185, @@ -258,7 +258,7 @@ "year": 1952, "author": "Paul Celan", "country": "Romania, France", - "imageLink": "images/poems-paul-celan.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+19", "language": "German", "link": "\n", "pages": 320, @@ -270,9 +270,9 @@ "title": "Journey to the End of the Night", "price": 290, "year": 1932, - "author": "Louis-Ferdinand Céline", + "author": "Louis-Ferdinand C\u00e9line", "country": "France", - "imageLink": "images/voyage-au-bout-de-la-nuit.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+20", "language": "French", "link": "https://en.wikipedia.org/wiki/Journey_to_the_End_of_the_Night\n", "pages": 505, @@ -286,7 +286,7 @@ "year": 1610, "author": "Miguel de Cervantes", "country": "Spain", - "imageLink": "images/don-quijote-de-la-mancha.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+21", "language": "Spanish", "link": "https://en.wikipedia.org/wiki/Don_Quixote\n", "pages": 1056, @@ -300,7 +300,7 @@ "year": 1450, "author": "Geoffrey Chaucer", "country": "England", - "imageLink": "images/the-canterbury-tales.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+22", "language": "English", "link": "https://en.wikipedia.org/wiki/The_Canterbury_Tales\n", "pages": 544, @@ -314,7 +314,7 @@ "year": 1886, "author": "Anton Chekhov", "country": "Russia", - "imageLink": "images/stories-of-anton-chekhov.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+23", "language": "Russian", "link": "https://en.wikipedia.org/wiki/List_of_short_stories_by_Anton_Chekhov\n", "pages": 194, @@ -328,7 +328,7 @@ "year": 1904, "author": "Joseph Conrad", "country": "United Kingdom", - "imageLink": "images/nostromo.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+24", "language": "English", "link": "https://en.wikipedia.org/wiki/Nostromo\n", "pages": 320, @@ -342,7 +342,7 @@ "year": 1861, "author": "Charles Dickens", "country": "United Kingdom", - "imageLink": "images/great-expectations.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+25", "language": "English", "link": "https://en.wikipedia.org/wiki/Great_Expectations\n", "pages": 194, @@ -356,7 +356,7 @@ "year": 1796, "author": "Denis Diderot", "country": "France", - "imageLink": "images/jacques-the-fatalist.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+26", "language": "French", "link": "https://en.wikipedia.org/wiki/Jacques_the_Fatalist\n", "pages": 596, @@ -368,9 +368,9 @@ "title": "Berlin Alexanderplatz", "price": 360, "year": 1929, - "author": "Alfred Döblin", + "author": "Alfred D\u00f6blin", "country": "Germany", - "imageLink": "images/berlin-alexanderplatz.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+27", "language": "German", "link": "https://en.wikipedia.org/wiki/Berlin_Alexanderplatz\n", "pages": 600, @@ -384,7 +384,7 @@ "year": 1866, "author": "Fyodor Dostoevsky", "country": "Russia", - "imageLink": "images/crime-and-punishment.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+28", "language": "Russian", "link": "https://en.wikipedia.org/wiki/Crime_and_Punishment\n", "pages": 551, @@ -398,7 +398,7 @@ "year": 1869, "author": "Fyodor Dostoevsky", "country": "Russia", - "imageLink": "images/the-idiot.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+29", "language": "Russian", "link": "https://en.wikipedia.org/wiki/The_Idiot\n", "pages": 656, @@ -412,7 +412,7 @@ "year": 1872, "author": "Fyodor Dostoevsky", "country": "Russia", - "imageLink": "images/the-possessed.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+30", "language": "Russian", "link": "https://en.wikipedia.org/wiki/Demons_(Dostoyevsky_novel)\n", "pages": 768, @@ -426,7 +426,7 @@ "year": 1880, "author": "Fyodor Dostoevsky", "country": "Russia", - "imageLink": "images/the-brothers-karamazov.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+31", "language": "Russian", "link": "https://en.wikipedia.org/wiki/The_Brothers_Karamazov\n", "pages": 824, @@ -440,7 +440,7 @@ "year": 1871, "author": "George Eliot", "country": "United Kingdom", - "imageLink": "images/middlemarch.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+32", "language": "English", "link": "https://en.wikipedia.org/wiki/Middlemarch\n", "pages": 800, @@ -454,7 +454,7 @@ "year": 1952, "author": "Ralph Ellison", "country": "United States", - "imageLink": "images/invisible-man.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+33", "language": "English", "link": "https://en.wikipedia.org/wiki/Invisible_Man\n", "pages": 581, @@ -468,7 +468,7 @@ "year": -431, "author": "Euripides", "country": "Greece", - "imageLink": "images/medea.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+34", "language": "Greek", "link": "https://en.wikipedia.org/wiki/Medea_(play)\n", "pages": 104, @@ -482,7 +482,7 @@ "year": 1936, "author": "William Faulkner", "country": "United States", - "imageLink": "images/absalom-absalom.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+35", "language": "English", "link": "https://en.wikipedia.org/wiki/Absalom,_Absalom!\n", "pages": 313, @@ -496,7 +496,7 @@ "year": 1929, "author": "William Faulkner", "country": "United States", - "imageLink": "images/the-sound-and-the-fury.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+36", "language": "English", "link": "https://en.wikipedia.org/wiki/The_Sound_and_the_Fury\n", "pages": 326, @@ -510,7 +510,7 @@ "year": 1857, "author": "Gustave Flaubert", "country": "France", - "imageLink": "images/madame-bovary.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+37", "language": "French", "link": "https://en.wikipedia.org/wiki/Madame_Bovary\n", "pages": 528, @@ -524,7 +524,7 @@ "year": 1869, "author": "Gustave Flaubert", "country": "France", - "imageLink": "images/l-education-sentimentale.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+38", "language": "French", "link": "https://en.wikipedia.org/wiki/Sentimental_Education\n", "pages": 606, @@ -536,9 +536,9 @@ "title": "Gypsy Ballads", "price": 480, "year": 1928, - "author": "Federico García Lorca", + "author": "Federico Garc\u00eda Lorca", "country": "Spain", - "imageLink": "images/gypsy-ballads.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+39", "language": "Spanish", "link": "https://en.wikipedia.org/wiki/Gypsy_Ballads\n", "pages": 218, @@ -550,9 +550,9 @@ "title": "One Hundred Years of Solitude", "price": 490, "year": 1967, - "author": "Gabriel García Márquez", + "author": "Gabriel Garc\u00eda M\u00e1rquez", "country": "Colombia", - "imageLink": "images/one-hundred-years-of-solitude.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+40", "language": "Spanish", "link": "https://en.wikipedia.org/wiki/One_Hundred_Years_of_Solitude\n", "pages": 417, @@ -564,9 +564,9 @@ "title": "Love in the Time of Cholera", "price": 500, "year": 1985, - "author": "Gabriel García Márquez", + "author": "Gabriel Garc\u00eda M\u00e1rquez", "country": "Colombia", - "imageLink": "images/love-in-the-time-of-cholera.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+41", "language": "Spanish", "link": "https://en.wikipedia.org/wiki/Love_in_the_Time_of_Cholera\n", "pages": 368, @@ -580,7 +580,7 @@ "year": 1832, "author": "Johann Wolfgang von Goethe", "country": "Saxe-Weimar", - "imageLink": "images/faust.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+42", "language": "German", "link": "https://en.wikipedia.org/wiki/Goethe%27s_Faust\n", "pages": 158, @@ -594,7 +594,7 @@ "year": 1842, "author": "Nikolai Gogol", "country": "Russia", - "imageLink": "images/dead-souls.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+43", "language": "Russian", "link": "https://en.wikipedia.org/wiki/Dead_Souls\n", "pages": 432, @@ -606,9 +606,9 @@ "title": "The Tin Drum", "price": 530, "year": 1959, - "author": "Günter Grass", + "author": "G\u00fcnter Grass", "country": "Germany", - "imageLink": "images/the-tin-drum.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+44", "language": "German", "link": "https://en.wikipedia.org/wiki/The_Tin_Drum\n", "pages": 600, @@ -620,9 +620,9 @@ "title": "The Devil to Pay in the Backlands", "price": 540, "year": 1956, - "author": "João Guimarães Rosa", + "author": "Jo\u00e3o Guimar\u00e3es Rosa", "country": "Brazil", - "imageLink": "images/the-devil-to-pay-in-the-backlands.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+45", "language": "Portuguese", "link": "https://en.wikipedia.org/wiki/The_Devil_to_Pay_in_the_Backlands\n", "pages": 494, @@ -636,7 +636,7 @@ "year": 1890, "author": "Knut Hamsun", "country": "Norway", - "imageLink": "images/hunger.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+46", "language": "Norwegian", "link": "https://en.wikipedia.org/wiki/Hunger_(Hamsun_novel)\n", "pages": 176, @@ -650,7 +650,7 @@ "year": 1952, "author": "Ernest Hemingway", "country": "United States", - "imageLink": "images/the-old-man-and-the-sea.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+47", "language": "English", "link": "https://en.wikipedia.org/wiki/The_Old_Man_and_the_Sea\n", "pages": 128, @@ -664,7 +664,7 @@ "year": -735, "author": "Homer", "country": "Greece", - "imageLink": "images/the-iliad-of-homer.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+48", "language": "Greek", "link": "https://en.wikipedia.org/wiki/Iliad\n", "pages": 608, @@ -678,7 +678,7 @@ "year": -800, "author": "Homer", "country": "Greece", - "imageLink": "images/the-odyssey-of-homer.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+49", "language": "Greek", "link": "https://en.wikipedia.org/wiki/Odyssey\n", "pages": 374, @@ -692,7 +692,7 @@ "year": 1879, "author": "Henrik Ibsen", "country": "Norway", - "imageLink": "images/a-Dolls-house.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+50", "language": "Norwegian", "link": "https://en.wikipedia.org/wiki/A_Doll%27s_House\n", "pages": 68, @@ -706,7 +706,7 @@ "year": 1922, "author": "James Joyce", "country": "Irish Free State", - "imageLink": "images/ulysses.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+51", "language": "English", "link": "https://en.wikipedia.org/wiki/Ulysses_(novel)\n", "pages": 228, @@ -720,7 +720,7 @@ "year": 1924, "author": "Franz Kafka", "country": "Czechoslovakia", - "imageLink": "images/stories-of-franz-kafka.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+52", "language": "German", "link": "https://en.wikipedia.org/wiki/Franz_Kafka_bibliography#Short_stories\n", "pages": 488, @@ -734,7 +734,7 @@ "year": 1925, "author": "Franz Kafka", "country": "Czechoslovakia", - "imageLink": "images/the-trial.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+53", "language": "German", "link": "https://en.wikipedia.org/wiki/The_Trial\n", "pages": 160, @@ -748,7 +748,7 @@ "year": 1926, "author": "Franz Kafka", "country": "Czechoslovakia", - "imageLink": "images/the-castle.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+54", "language": "German", "link": "https://en.wikipedia.org/wiki/The_Castle_(novel)\n", "pages": 352, @@ -760,9 +760,9 @@ "title": "The recognition of Shakuntala", "price": 640, "year": 150, - "author": "Kālidāsa", + "author": "K\u0101lid\u0101sa", "country": "India", - "imageLink": "images/the-recognition-of-shakuntala.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+55", "language": "Sanskrit", "link": "https://en.wikipedia.org/wiki/Abhij%C3%B1%C4%81na%C5%9B%C4%81kuntalam\n", "pages": 147, @@ -776,7 +776,7 @@ "year": 1954, "author": "Yasunari Kawabata", "country": "Japan", - "imageLink": "images/the-sound-of-the-mountain.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+56", "language": "Japanese", "link": "https://en.wikipedia.org/wiki/The_Sound_of_the_Mountain\n", "pages": 288, @@ -790,7 +790,7 @@ "year": 1946, "author": "Nikos Kazantzakis", "country": "Greece", - "imageLink": "images/zorba-the-greek.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+57", "language": "Greek", "link": "https://en.wikipedia.org/wiki/Zorba_the_Greek\n", "pages": 368, @@ -804,7 +804,7 @@ "year": 1913, "author": "D. H. Lawrence", "country": "United Kingdom", - "imageLink": "images/sons-and-lovers.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+58", "language": "English", "link": "https://en.wikipedia.org/wiki/Sons_and_Lovers\n", "pages": 432, @@ -816,9 +816,9 @@ "title": "Independent People", "price": 680, "year": 1934, - "author": "Halldór Laxness", + "author": "Halld\u00f3r Laxness", "country": "Iceland", - "imageLink": "images/independent-people.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+59", "language": "Icelandic", "link": "https://en.wikipedia.org/wiki/Independent_People\n", "pages": 470, @@ -832,7 +832,7 @@ "year": 1818, "author": "Giacomo Leopardi", "country": "Italy", - "imageLink": "images/poems-giacomo-leopardi.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+60", "language": "Italian", "link": "\n", "pages": 184, @@ -846,7 +846,7 @@ "year": 1962, "author": "Doris Lessing", "country": "United Kingdom", - "imageLink": "images/the-golden-notebook.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+61", "language": "English", "link": "https://en.wikipedia.org/wiki/The_Golden_Notebook\n", "pages": 688, @@ -860,7 +860,7 @@ "year": 1945, "author": "Astrid Lindgren", "country": "Sweden", - "imageLink": "images/pippi-longstocking.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+62", "language": "Swedish", "link": "https://en.wikipedia.org/wiki/Pippi_Longstocking\n", "pages": 160, @@ -874,7 +874,7 @@ "year": 1918, "author": "Lu Xun", "country": "China", - "imageLink": "images/diary-of-a-madman.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+63", "language": "Chinese", "link": "https://en.wikipedia.org/wiki/A_Madman%27s_Diary\n", "pages": 389, @@ -888,7 +888,7 @@ "year": 1959, "author": "Naguib Mahfouz", "country": "Egypt", - "imageLink": "images/children-of-gebelawi.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+64", "language": "Arabic", "link": "https://en.wikipedia.org/wiki/Children_of_Gebelawi\n", "pages": 355, @@ -902,7 +902,7 @@ "year": 1901, "author": "Thomas Mann", "country": "Germany", - "imageLink": "images/buddenbrooks.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+65", "language": "German", "link": "https://en.wikipedia.org/wiki/Buddenbrooks\n", "pages": 736, @@ -916,7 +916,7 @@ "year": 1924, "author": "Thomas Mann", "country": "Germany", - "imageLink": "images/the-magic-mountain.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+66", "language": "German", "link": "https://en.wikipedia.org/wiki/The_Magic_Mountain\n", "pages": 720, @@ -930,7 +930,7 @@ "year": 1851, "author": "Herman Melville", "country": "United States", - "imageLink": "images/moby-dick.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+67", "language": "English", "link": "https://en.wikipedia.org/wiki/Moby-Dick\n", "pages": 378, @@ -944,7 +944,7 @@ "year": 1595, "author": "Michel de Montaigne", "country": "France", - "imageLink": "images/essais.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+68", "language": "French", "link": "https://en.wikipedia.org/wiki/Essays_(Montaigne)\n", "pages": 404, @@ -958,7 +958,7 @@ "year": 1974, "author": "Elsa Morante", "country": "Italy", - "imageLink": "images/history.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+69", "language": "Italian", "link": "https://en.wikipedia.org/wiki/History_(novel)\n", "pages": 600, @@ -972,7 +972,7 @@ "year": 1987, "author": "Toni Morrison", "country": "United States", - "imageLink": "images/beloved.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+70", "language": "English", "link": "https://en.wikipedia.org/wiki/Beloved_(novel)\n", "pages": 321, @@ -986,7 +986,7 @@ "year": 1006, "author": "Murasaki Shikibu", "country": "Japan", - "imageLink": "images/the-tale-of-genji.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+71", "language": "Japanese", "link": "https://en.wikipedia.org/wiki/The_Tale_of_Genji\n", "pages": 1360, @@ -1000,7 +1000,7 @@ "year": 1931, "author": "Robert Musil", "country": "Austria", - "imageLink": "images/the-man-without-qualities.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+72", "language": "German", "link": "https://en.wikipedia.org/wiki/The_Man_Without_Qualities\n", "pages": 365, @@ -1014,7 +1014,7 @@ "year": 1955, "author": "Vladimir Nabokov", "country": "Russia/United States", - "imageLink": "images/lolita.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+73", "language": "English", "link": "https://en.wikipedia.org/wiki/Lolita\n", "pages": 317, @@ -1028,7 +1028,7 @@ "year": 1949, "author": "George Orwell", "country": "United Kingdom", - "imageLink": "images/nineteen-eighty-four.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+74", "language": "English", "link": "https://en.wikipedia.org/wiki/Nineteen_Eighty-Four\n", "pages": 272, @@ -1042,7 +1042,7 @@ "year": 100, "author": "Ovid", "country": "Roman Empire", - "imageLink": "images/the-metamorphoses-of-ovid.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+75", "language": "Classical Latin", "link": "https://en.wikipedia.org/wiki/Metamorphoses\n", "pages": 576, @@ -1056,7 +1056,7 @@ "year": 1928, "author": "Fernando Pessoa", "country": "Portugal", - "imageLink": "images/the-book-of-disquiet.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+76", "language": "Portuguese", "link": "https://en.wikipedia.org/wiki/The_Book_of_Disquiet\n", "pages": 272, @@ -1070,7 +1070,7 @@ "year": 1950, "author": "Edgar Allan Poe", "country": "United States", - "imageLink": "images/tales-and-poems-of-edgar-allan-poe.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+77", "language": "English", "link": "https://en.wikipedia.org/wiki/Edgar_Allan_Poe_bibliography#Tales\n", "pages": 842, @@ -1084,7 +1084,7 @@ "year": 1920, "author": "Marcel Proust", "country": "France", - "imageLink": "images/a-la-recherche-du-temps-perdu.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+78", "language": "French", "link": "https://en.wikipedia.org/wiki/In_Search_of_Lost_Time\n", "pages": 2408, @@ -1096,9 +1096,9 @@ "title": "Gargantua and Pantagruel", "price": 880, "year": 1533, - "author": "François Rabelais", + "author": "Fran\u00e7ois Rabelais", "country": "France", - "imageLink": "images/gargantua-and-pantagruel.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+79", "language": "French", "link": "https://en.wikipedia.org/wiki/Gargantua_and_Pantagruel\n", "pages": 623, @@ -1107,12 +1107,12 @@ }, { "id": 80, - "title": "Pedro Páramo", + "title": "Pedro P\u00e1ramo", "price": 890, "year": 1955, "author": "Juan Rulfo", "country": "Mexico", - "imageLink": "images/pedro-paramo.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+80", "language": "Spanish", "link": "https://en.wikipedia.org/wiki/Pedro_P%C3%A1ramo\n", "pages": 124, @@ -1126,7 +1126,7 @@ "year": 1236, "author": "Rumi", "country": "Sultanate of Rum", - "imageLink": "images/the-masnavi.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+81", "language": "Persian", "link": "https://en.wikipedia.org/wiki/Masnavi\n", "pages": 438, @@ -1140,7 +1140,7 @@ "year": 1981, "author": "Salman Rushdie", "country": "United Kingdom, India", - "imageLink": "images/midnights-children.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+82", "language": "English", "link": "https://en.wikipedia.org/wiki/Midnight%27s_Children\n", "pages": 536, @@ -1154,7 +1154,7 @@ "year": 1257, "author": "Saadi", "country": "Persia, Persian Empire", - "imageLink": "images/bostan.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+83", "language": "Persian", "link": "https://en.wikipedia.org/wiki/Bustan_(book)\n", "pages": 298, @@ -1168,7 +1168,7 @@ "year": 1966, "author": "Tayeb Salih", "country": "Sudan", - "imageLink": "images/season-of-migration-to-the-north.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+84", "language": "Arabic", "link": "https://en.wikipedia.org/wiki/Season_of_Migration_to_the_North\n", "pages": 139, @@ -1180,9 +1180,9 @@ "title": "Blindness", "price": 940, "year": 1995, - "author": "José Saramago", + "author": "Jos\u00e9 Saramago", "country": "Portugal", - "imageLink": "images/blindness.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+85", "language": "Portuguese", "link": "https://en.wikipedia.org/wiki/Blindness_(novel)\n", "pages": 352, @@ -1196,7 +1196,7 @@ "year": 1603, "author": "William Shakespeare", "country": "England", - "imageLink": "images/hamlet.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+86", "language": "English", "link": "https://en.wikipedia.org/wiki/Hamlet\n", "pages": 432, @@ -1210,7 +1210,7 @@ "year": 1608, "author": "William Shakespeare", "country": "England", - "imageLink": "images/king-lear.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+87", "language": "English", "link": "https://en.wikipedia.org/wiki/King_Lear\n", "pages": 384, @@ -1224,7 +1224,7 @@ "year": 1609, "author": "William Shakespeare", "country": "England", - "imageLink": "images/othello.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+88", "language": "English", "link": "https://en.wikipedia.org/wiki/Othello\n", "pages": 314, @@ -1238,7 +1238,7 @@ "year": -430, "author": "Sophocles", "country": "Greece", - "imageLink": "images/oedipus-the-king.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+89", "language": "Greek", "link": "https://en.wikipedia.org/wiki/Oedipus_the_King\n", "pages": 88, @@ -1252,7 +1252,7 @@ "year": 1830, "author": "Stendhal", "country": "France", - "imageLink": "images/le-rouge-et-le-noir.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+90", "language": "French", "link": "https://en.wikipedia.org/wiki/The_Red_and_the_Black\n", "pages": 576, @@ -1266,7 +1266,7 @@ "year": 1760, "author": "Laurence Sterne", "country": "England", - "imageLink": "images/the-life-and-opinions-of-tristram-shandy.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+91", "language": "English", "link": "https://en.wikipedia.org/wiki/The_Life_and_Opinions_of_Tristram_Shandy,_Gentleman\n", "pages": 640, @@ -1280,7 +1280,7 @@ "year": 1923, "author": "Italo Svevo", "country": "Italy", - "imageLink": "images/confessions-of-zeno.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+92", "language": "Italian", "link": "https://en.wikipedia.org/wiki/Zeno%27s_Conscience\n", "pages": 412, @@ -1294,7 +1294,7 @@ "year": 1726, "author": "Jonathan Swift", "country": "Ireland", - "imageLink": "images/gullivers-travels.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+93", "language": "English", "link": "https://en.wikipedia.org/wiki/Gulliver%27s_Travels\n", "pages": 178, @@ -1308,7 +1308,7 @@ "year": 1867, "author": "Leo Tolstoy", "country": "Russia", - "imageLink": "images/war-and-peace.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+94", "language": "Russian", "link": "https://en.wikipedia.org/wiki/War_and_Peace\n", "pages": 1296, @@ -1322,7 +1322,7 @@ "year": 1877, "author": "Leo Tolstoy", "country": "Russia", - "imageLink": "images/anna-karenina.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+95", "language": "Russian", "link": "https://en.wikipedia.org/wiki/Anna_Karenina\n", "pages": 864, @@ -1336,7 +1336,7 @@ "year": 1886, "author": "Leo Tolstoy", "country": "Russia", - "imageLink": "images/the-death-of-ivan-ilyich.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+96", "language": "Russian", "link": "https://en.wikipedia.org/wiki/The_Death_of_Ivan_Ilyich\n", "pages": 92, @@ -1350,7 +1350,7 @@ "year": 1884, "author": "Mark Twain", "country": "United States", - "imageLink": "images/the-adventures-of-huckleberry-finn.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+97", "language": "English", "link": "https://en.wikipedia.org/wiki/Adventures_of_Huckleberry_Finn\n", "pages": 224, @@ -1364,7 +1364,7 @@ "year": -450, "author": "Valmiki", "country": "India", - "imageLink": "images/ramayana.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+98", "language": "Sanskrit", "link": "https://en.wikipedia.org/wiki/Ramayana\n", "pages": 152, @@ -1378,7 +1378,7 @@ "year": -23, "author": "Virgil", "country": "Roman Empire", - "imageLink": "images/the-aeneid.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+99", "language": "Classical Latin", "link": "https://en.wikipedia.org/wiki/Aeneid\n", "pages": 442, @@ -1392,67 +1392,11 @@ "year": -700, "author": "Vyasa", "country": "India", - "imageLink": "images/the-mahab-harata.jpg", + "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+100", "language": "Sanskrit", "link": "https://en.wikipedia.org/wiki/Mahabharata\n", "pages": 276, "createdAt": "2025-07-25T17:31:54.239043+02:00", "updatedAt": "2025-07-25T17:31:54.239043+02:00" - }, - { - "id": 101, - "title": "Leaves of Grass", - "price": 1100, - "year": 1855, - "author": "Walt Whitman", - "country": "United States", - "imageLink": "images/leaves-of-grass.jpg", - "language": "English", - "link": "https://en.wikipedia.org/wiki/Leaves_of_Grass\n", - "pages": 152, - "createdAt": "2025-07-25T17:31:54.239043+02:00", - "updatedAt": "2025-07-25T17:31:54.239043+02:00" - }, - { - "id": 102, - "title": "Mrs Dalloway", - "price": 1110, - "year": 1925, - "author": "Virginia Woolf", - "country": "United Kingdom", - "imageLink": "images/mrs-dalloway.jpg", - "language": "English", - "link": "https://en.wikipedia.org/wiki/Mrs_Dalloway\n", - "pages": 216, - "createdAt": "2025-07-25T17:31:54.239043+02:00", - "updatedAt": "2025-07-25T17:31:54.239043+02:00" - }, - { - "id": 103, - "title": "To the Lighthouse", - "price": 1120, - "year": 1927, - "author": "Virginia Woolf", - "country": "United Kingdom", - "imageLink": "images/to-the-lighthouse.jpg", - "language": "English", - "link": "https://en.wikipedia.org/wiki/To_the_Lighthouse\n", - "pages": 209, - "createdAt": "2025-07-25T17:31:54.239044+02:00", - "updatedAt": "2025-07-25T17:31:54.239044+02:00" - }, - { - "id": 104, - "title": "Memoirs of Hadrian", - "price": 1130, - "year": 1951, - "author": "Marguerite Yourcenar", - "country": "France/Belgium", - "imageLink": "images/memoirs-of-hadrian.jpg", - "language": "French", - "link": "https://en.wikipedia.org/wiki/Memoirs_of_Hadrian\n", - "pages": 408, - "createdAt": "2025-07-25T17:31:54.239044+02:00", - "updatedAt": "2025-07-25T17:31:54.239044+02:00" } ] \ No newline at end of file diff --git a/go.mod b/go.mod index 0e6860b..2668a7e 100644 --- a/go.mod +++ b/go.mod @@ -1,37 +1,31 @@ module github.com/jkaninda/okapi-example -go 1.24.5 +go 1.25.5 require ( github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/jkaninda/go-utils v0.1.4 github.com/jkaninda/logger v0.0.5 - github.com/jkaninda/okapi v0.0.18 + github.com/jkaninda/okapi v0.2.0 + github.com/jkaninda/okapi-ws v0.0.0-20260117135650-dd9a12fcc236 + github.com/joho/godotenv v1.5.1 ) require ( - github.com/KyleBanks/depth v1.2.1 // indirect - github.com/getkin/kin-openapi v0.132.0 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/jkaninda/go-utils v0.1.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/swaggo/files v1.0.1 // indirect - github.com/swaggo/http-swagger v1.3.4 // indirect - github.com/swaggo/swag v1.16.6 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/tools v0.36.0 // indirect - google.golang.org/protobuf v1.36.7 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 675d97f..365cd3d 100644 --- a/go.sum +++ b/go.sum @@ -1,53 +1,43 @@ -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= -github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/jkaninda/go-utils v0.1.1 h1:PMrtXR9d51YzHo85y9Z6YVL0YyBURbRTPemHVbFDqZg= -github.com/jkaninda/go-utils v0.1.1/go.mod h1:pf0/U6k4JbxlablM2G4eSTZdQ2LFshfAsCK5Q8qNfGo= -github.com/jkaninda/logger v0.0.3 h1:WaGem831HfeSE//ZZKuJTFv9Jaof9xyAH2PArT5tKps= -github.com/jkaninda/logger v0.0.3/go.mod h1:ZUXJ2BdxDPG6e8t6mbKhc2ZFaFi2Iuy/4ukquFrPkFE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jkaninda/go-utils v0.1.4 h1:ZdNlI+yLWc4/S0qKcCNQIPj+6lHSdJcGaxtRADAifAU= +github.com/jkaninda/go-utils v0.1.4/go.mod h1:Aa54jEAcDykc3CnOdreqZG80UfSZOvrYecyusu+oPb4= github.com/jkaninda/logger v0.0.5 h1:fTHKgDsHtuN8rkSBvwe6QStfi8yIdm8O3r0g99dRDTY= github.com/jkaninda/logger v0.0.5/go.mod h1:ZUXJ2BdxDPG6e8t6mbKhc2ZFaFi2Iuy/4ukquFrPkFE= -github.com/jkaninda/okapi v0.0.3 h1:Bxceqe/4mUIQR+YvGWOC5h3io/PX56Uj0jNWaeh9A/g= -github.com/jkaninda/okapi v0.0.3/go.mod h1:be2CsFhhvLh7sQ0d+4/1P/IbAyYB32i0sJpdfeS+wWY= -github.com/jkaninda/okapi v0.0.5 h1:tdpDvOSw8dR4b1yBGUo1do6mrv6y0d+hEFev447MqPY= -github.com/jkaninda/okapi v0.0.5/go.mod h1:AR5cmx3muUi2hkMZTsA2FJ7ABfKKfMttx2h1Dv+D6AM= -github.com/jkaninda/okapi v0.0.6 h1:usUuZMB/bZzPM9dgb21GhyqC3TTSr+qgH/DfdTPh+NM= -github.com/jkaninda/okapi v0.0.6/go.mod h1:AR5cmx3muUi2hkMZTsA2FJ7ABfKKfMttx2h1Dv+D6AM= -github.com/jkaninda/okapi v0.0.7 h1:jFPDfTR9BEKWoLnSdZxfmSyOp4EetXCKjw+uqurTKXI= -github.com/jkaninda/okapi v0.0.7/go.mod h1:EnmxlW7HZLH5tYMoG8gpub+1S5YUPwEnVo97q45GcrY= -github.com/jkaninda/okapi v0.0.8 h1:gEdFRgueaRXEv4aZzk62p98xqctyLHIYC5u8R1jBNoY= -github.com/jkaninda/okapi v0.0.8/go.mod h1:EnmxlW7HZLH5tYMoG8gpub+1S5YUPwEnVo97q45GcrY= -github.com/jkaninda/okapi v0.0.10 h1:uqgz38jd1hanhIa2GMW7IsxiL6nWAdWgWF81yEN4WBc= -github.com/jkaninda/okapi v0.0.10/go.mod h1:EnmxlW7HZLH5tYMoG8gpub+1S5YUPwEnVo97q45GcrY= -github.com/jkaninda/okapi v0.0.11 h1:JglVjhJq4NzXrAftm1IdTW9m4jxmitKUq9767Gf8d5A= -github.com/jkaninda/okapi v0.0.11/go.mod h1:EnmxlW7HZLH5tYMoG8gpub+1S5YUPwEnVo97q45GcrY= -github.com/jkaninda/okapi v0.0.14 h1:6MP9AbTqpVSPIDunt7iC/uK0jXEmEZ7fiExTwEgFRgo= -github.com/jkaninda/okapi v0.0.14/go.mod h1:EnmxlW7HZLH5tYMoG8gpub+1S5YUPwEnVo97q45GcrY= -github.com/jkaninda/okapi v0.0.17 h1:zGap8pLwdbjBPF1ws8Bcgoo0/3CLXTv+NZnUrpz/XAc= -github.com/jkaninda/okapi v0.0.17/go.mod h1:lGcmGDxXBE4UDOKjNAnveUIxdG4Ddnc0BByI+hL/vZs= -github.com/jkaninda/okapi v0.0.18 h1:7wfidIPpSH8B3fRqZu5+IEnjuGKZq/69Rn99NcQgFrM= -github.com/jkaninda/okapi v0.0.18/go.mod h1:X5rrqoVA+6si4kFhr8/rkWjHSHgG0C/TsyJwZfoogho= +github.com/jkaninda/okapi v0.1.3-rc1 h1:ClpTUEIRmgQmHKprecka2GboN06gj/4MyxXz2bKSwFs= +github.com/jkaninda/okapi v0.1.3-rc1/go.mod h1:VORIE1RY2jhNsodcHF2kLNjAXwp4G7EBXf//VRZP2lc= +github.com/jkaninda/okapi v0.1.3-rc2 h1:D0INvElPvRKqVZ+VXkowmq4ZnGsk8K7ZTLpyzG/7BkA= +github.com/jkaninda/okapi v0.1.3-rc2/go.mod h1:VORIE1RY2jhNsodcHF2kLNjAXwp4G7EBXf//VRZP2lc= +github.com/jkaninda/okapi v0.2.0 h1:sWWysiEvuxqXMbEwjDpY1CTsgIBs130p8LX+7IISeTE= +github.com/jkaninda/okapi v0.2.0/go.mod h1:VORIE1RY2jhNsodcHF2kLNjAXwp4G7EBXf//VRZP2lc= +github.com/jkaninda/okapi-ws v0.0.0-20260117135650-dd9a12fcc236 h1:hs0ybM54mAR9wVeDM32CAvNwT+LEGjP098AHlmsEV3g= +github.com/jkaninda/okapi-ws v0.0.0-20260117135650-dd9a12fcc236/go.mod h1:wC/XqK1Ti05kWx+aZ3wfB2Q8zDi/drg9Lpvsq06gksY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= @@ -56,65 +46,17 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= -github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= -github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= -github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= -github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= -github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= -github.com/swaggo/swag v1.16.5 h1:nMf2fEV1TetMTJb4XzD0Lz7jFfKJmJKGTygEey8NSxM= -github.com/swaggo/swag v1.16.5/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= -github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= -github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= diff --git a/home-page.png b/home-page.png new file mode 100644 index 0000000..1efc6b4 Binary files /dev/null and b/home-page.png differ diff --git a/main.go b/main.go index 1bcb4d0..dc7fcbc 100644 --- a/main.go +++ b/main.go @@ -1,27 +1,39 @@ package main import ( + "embed" + "html/template" + "io" + + "github.com/jkaninda/logger" "github.com/jkaninda/okapi" + "github.com/jkaninda/okapi-example/config" "github.com/jkaninda/okapi-example/routes" ) +//go:embed views/* +var Views embed.FS + +type Template struct { + templates *template.Template +} + +func (t *Template) Render(w io.Writer, name string, data interface{}, c *okapi.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} +func NewTemplate() *Template { + tmpl := template.Must(template.ParseFS(Views, "views/*.html")) + return &Template{templates: tmpl} +} func main() { - // Create a new Okapi instance with default config app := okapi.New() - route := routes.NewRoute(app) - - // ************ Registering Routes ************ - // Register home route - app.Register(route.Home()) - app.Register(route.WhoAmI()) - // Auth - app.Register(route.AuthRoute()) - // Register book routes - app.Register(route.BookRoutes()...) - app.Register(route.APIBookRoutes()...) - app.Register(route.CommonRoutes()...) - // Admin routes - app.Register(route.AdminRoutes()...) + conf := config.New() + if err := conf.Initialize(app); err != nil { + logger.Fatal("Failed to initialize config", "error", err) + } + app.WithRenderer(NewTemplate()) + route := routes.New(app, conf) + route.RegisterRoutes() // Start the server if err := app.Start(); err != nil { diff --git a/middlewares/middleware.go b/middlewares/middleware.go index 2a23c6d..25feb17 100644 --- a/middlewares/middleware.go +++ b/middlewares/middleware.go @@ -2,20 +2,21 @@ package middlewares import ( "fmt" - "github.com/jkaninda/logger" - "github.com/jkaninda/okapi-example/utils" "log/slog" "net/http" "strings" "time" + goutils "github.com/jkaninda/go-utils" + "github.com/jkaninda/logger" + "github.com/golang-jwt/jwt/v5" "github.com/jkaninda/okapi" "github.com/jkaninda/okapi-example/models" ) var ( - signingSecret = utils.GetSingingSecret() + signingSecret = goutils.Env("JWT_SECRET", "default-secret-key") JWTAuth = &okapi.JWTAuth{ SigningSecret: []byte(signingSecret), Audience: "okapi.jkaninda.dev", @@ -40,12 +41,10 @@ var ( "name": "user.name", }, // CustomClaims claims validation function - ValidateClaims: func(context okapi.Context, claims jwt.Claims) error { + ValidateClaims: func(context *okapi.Context, claims jwt.Claims) error { slog.Info("Validating JWT claims for role using custom function") // Simulate a custom claims validation - if _, ok := claims.(jwt.Claims); ok { - } return nil }, } @@ -65,17 +64,14 @@ var ( adminPermissions = []string{"read", "create", "delete", "update"} ) -func Login(authRequest *models.AuthRequest) (models.AuthResponse, error) { +func Login(authRequest *models.AuthRequest) (models.Auth, error) { // This is where you would typically validate the user credentials against a database logger.Info("Login attempt", "username", authRequest.Username) // Simulate a login function that returns a JWT token if authRequest.Username != "admin" && authRequest.Password != "password" || authRequest.Username != "user" && authRequest.Password != "password" { - return models.AuthResponse{ - Success: false, - Message: "Invalid username or password", - }, fmt.Errorf("username or password is wrong") + return models.Auth{}, fmt.Errorf("username or password is wrong") } if _, ok := jwtClaims["user"].(map[string]string); ok { @@ -97,21 +93,21 @@ func Login(authRequest *models.AuthRequest) (models.AuthResponse, error) { token, err := okapi.GenerateJwtToken(JWTAuth.SigningSecret, jwtClaims, expireAt) if err != nil { - return models.AuthResponse{ - Success: false, - Message: "Invalid username or password", - }, fmt.Errorf("failed to generate JWT token: %w", err) + return models.Auth{}, fmt.Errorf("failed to generate JWT token: %w", err) } - return models.AuthResponse{ - Success: true, - Message: "Welcome back " + authRequest.Username, + return models.Auth{ Token: token, ExpiresAt: time.Now().Add(expireAt).Unix(), + USer: models.UserInfo{ + Name: strings.ToTitle(authRequest.Username), + Role: authRequest.Username, + Email: fmt.Sprintf("%s@example.com", authRequest.Username), + }, }, nil } -func CustomMiddleware(next okapi.HandleFunc) okapi.HandleFunc { - return func(c okapi.Context) error { +func CustomMiddleware(next okapi.HandlerFunc) okapi.HandlerFunc { + return func(c *okapi.Context) error { slog.Info("Custom middleware executed", "path", c.Request().URL.Path, "method", c.Request().Method) // You can add any custom logic here, such as logging, authentication, etc. // For example, let's log the request method and URL diff --git a/models/model.go b/models/model.go index 95ec9df..90104fd 100644 --- a/models/model.go +++ b/models/model.go @@ -27,12 +27,46 @@ package models import "time" // **************** Models ************************ - -type Response struct { +type ResponseDto struct { Success bool `json:"success"` Message string `json:"message"` - Data Book `json:"data"` + Details any `json:"details,omitempty"` +} + +type Response[T any] struct { + ResponseDto + Data T `json:"data,omitempty"` +} + +func SuccessResponse[T any](message string, data T) Response[T] { + return Response[T]{ + ResponseDto: ResponseDto{ + Success: true, + Message: message, + }, + Data: data, + } +} +func ErrorResponse(message string, err error) Response[any] { + return Response[any]{ + ResponseDto: ResponseDto{ + Success: false, + Message: message, + Details: err.Error(), + }, + } +} +func ErrorResponseData(message string, err error, data any) Response[any] { + return Response[any]{ + ResponseDto: ResponseDto{ + Success: false, + Message: message, + Details: err, + }, + Data: data, + } } + type Book struct { Id int `json:"id"` Title string `json:"title" form:"title" max:"50" required:"true" description:"Book name"` @@ -47,21 +81,15 @@ type Book struct { CreatedAt time.Time `json:"createdAt" form:"createdAt" query:"createdAt" yaml:"createdAt" required:"false" description:"Book creation date"` UpdatedAt time.Time `json:"updatedAt" form:"updatedAt" query:"updatedAt" yaml:"updatedAt" required:"false" description:"Book last update date"` } -type ErrorResponse struct { - Success bool `json:"success"` - Status int `json:"status"` - Details any `json:"details"` -} type AuthRequest struct { - Username string `json:"username" required:"true" description:"Username for authentication"` + Username string `json:"username" required:"true" description:"Username for authentication" minLength:"3" pattern:"^[a-z]+$"` Password string `json:"password" required:"true" description:"Password for authentication"` } -type AuthResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Token string `json:"token,omitempty"` - ExpiresAt int64 `json:"expires,omitempty"` +type Auth struct { + Token string `json:"token,omitempty"` + ExpiresAt int64 `json:"expires,omitempty"` + USer UserInfo `json:"user"` } type UserInfo struct { Name string `json:"name"` @@ -74,3 +102,23 @@ type WhoAmIResponse struct { RealIp string `json:"realIp"` CurrentUser UserInfo `json:"currentUser"` } +type WhoAmIRequest struct { + Email string `header:"current_user_email" format:"email"` + Name string `header:"current_user_name"` + Role string `header:"current_user_role" enum:"ADMIN,admin,USER,user" doc:"One of ADMIN,admin,USER or user"` +} +type BookResponse = Response[Book] +type UserResponse = Response[UserInfo] +type BooksResponse = Response[[]Book] +type AuthResponse = Response[Auth] + +type RequestDto struct { + Platform string `json:"platform" enum:"web,mobile"` + AppId string `json:"appId" enum:"com.mobile,com.web"` + Details any `json:"details,omitempty"` +} + +type Request[T any] struct { + RequestDto + Fields T `json:"fields,omitempty"` +} diff --git a/routes/route.go b/routes/route.go index 5bce563..3613251 100644 --- a/routes/route.go +++ b/routes/route.go @@ -25,18 +25,20 @@ package routes import ( - "github.com/jkaninda/okapi-example/controllers" - "github.com/jkaninda/okapi-example/models" "net/http" + "github.com/jkaninda/okapi-example/config" + "github.com/jkaninda/okapi-example/models" + "github.com/jkaninda/okapi-example/services" + "github.com/jkaninda/okapi" "github.com/jkaninda/okapi-example/middlewares" ) var ( - bookController = &controllers.BookController{} - homeController = &controllers.HomeController{} - authController = &controllers.AuthController{} + bookService = &services.BookService{} + commonService = &services.CommonService{} + authService = &services.AuthService{} bearerAuthSecurity = []map[string][]string{ { "bearerAuth": {}, @@ -47,171 +49,167 @@ var ( // You can also use this example type Route struct { - app *okapi.Okapi + app *okapi.Okapi + group *okapi.Group + cfg *config.Config } // NewRoute creates a new Route instance with the provided Okapi app -// NewRoute creates a new Route instance with the Okapi application -func NewRoute(app *okapi.Okapi) *Route { - // Update OpenAPI documentation with the application title and version - app.WithOpenAPIDocs(okapi.OpenAPI{ - Title: "Okapi Web Framework Example", - Version: "1.0.0", - License: okapi.License{ - Name: "MIT", - }, - SecuritySchemes: okapi.SecuritySchemes{ - { - Name: "bearerAuth", - Type: "http", - Scheme: "bearer", - BearerFormat: "JWT", - }, - }, - }) +func New(app *okapi.Okapi, conf *config.Config) *Route { + commonService.SessionManager = conf.SessionManager return &Route{ - app: app, + app: app, + group: &okapi.Group{Prefix: "api/v1"}, + cfg: conf, } } -// ****************** Route Definitions ****************** +func (r *Route) RegisterRoutes() { + r.registerAll() + r.app.Register(r.whoAmI()) + r.app.Register(r.authRoute()) + r.app.Register(r.coreRoutes()...) + r.app.Register(r.bookRoutes()...) + r.app.Register(r.v1BookRoutes()...) + // Admin routes + r.app.Register(r.AdminRoutes()...) +} + +// Home return Render +func (r *Route) registerAll() { + r.app.Get("/", func(c *okapi.Context) error { + return commonService.Home(c) + }) + r.app.Get("/sse/sessions", func(c *okapi.Context) error { + return commonService.Session(c) + }, + okapi.Summary("Get current server time"), + ) + r.app.Get("/ws", commonService.WebSocketHandle, + okapi.Summary("Start Websocket"), + okapi.DocQueryParam("token", "string", "Websocket auth token", false), + ) -// Home returns the route definition for the HomeController -func (r *Route) Home() okapi.RouteDefinition { - return okapi.RouteDefinition{ - Path: "/", - Method: http.MethodGet, - Handler: homeController.Home, - Group: &okapi.Group{Prefix: "/", Tags: []string{"HomeController"}}, - } } +// ****************** Route Definitions ****************** + // WhoAmI returns the route definition for the HomeController -func (r *Route) WhoAmI() okapi.RouteDefinition { +func (r *Route) whoAmI() okapi.RouteDefinition { return okapi.RouteDefinition{ - Path: "/whoami", - Method: http.MethodGet, - Handler: homeController.WhoAmI, - Group: &okapi.Group{Prefix: "/", Tags: []string{"HomeController"}}, - Options: []okapi.RouteOption{ - okapi.DocSummary("Whoami"), - okapi.DocDescription("Get the current user's information, no auth required"), - okapi.DocHeader("current_user_email", "string", "current user", false), - okapi.DocHeader("current_user_name", "string", "current name", false), - okapi.DocHeader("current_user_role", "string", "current role", false), - okapi.DocResponse(models.WhoAmIResponse{}), - }, + Path: "/whoami", + Method: http.MethodGet, + Handler: commonService.WhoAmI, + Group: r.group, + Summary: "Whoami", + Description: "Get the current user's information, no auth requir", + Request: &models.WhoAmIRequest{}, + Response: &models.WhoAmIResponse{}, } } // ************* Book Routes ************* -// APIBookRoutes returns the route definitions for the BookController -func (r *Route) APIBookRoutes() []okapi.RouteDefinition { - apiGroup := &okapi.Group{Prefix: "/api", Tags: []string{"BookController"}} +// bookRoutes returns the route definitions for the BookService +func (r *Route) bookRoutes() []okapi.RouteDefinition { + apiGroup := &okapi.Group{Prefix: "/api", Tags: []string{"BookService"}} apiGroup.Use(middlewares.CustomMiddleware) apiGroup.Deprecated() return []okapi.RouteDefinition{ { Method: http.MethodGet, Path: "/books", - Handler: bookController.GetBooks, + Handler: bookService.List, Group: apiGroup, Middlewares: []okapi.Middleware{}, Options: []okapi.RouteOption{ okapi.DocSummary("Get Books"), okapi.DocDescription("Retrieve a list of books"), okapi.DocResponse([]models.Book{}), - okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}), + okapi.DocResponse(&models.BookResponse{}), }, }, { Method: http.MethodGet, Path: "/books/:id", - Handler: bookController.GetBook, + Handler: bookService.Get, Group: apiGroup, Options: []okapi.RouteOption{ okapi.DocSummary("Get Book by ID"), okapi.DocDescription("Retrieve a book by its ID"), okapi.DocPathParam("id", "int", "The ID of the book"), okapi.DocResponse(models.Book{}), - okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}), - okapi.DocResponse(http.StatusNotFound, models.ErrorResponse{}), + okapi.DocResponse(http.StatusBadRequest, &models.Response[models.Book]{}), }, }, } } -func (r *Route) BookRoutes() []okapi.RouteDefinition { +func (r *Route) v1BookRoutes() []okapi.RouteDefinition { + apiGroup := r.group.Group("/books").WithTags([]string{"V1BookService"}) + return []okapi.RouteDefinition{ { Method: http.MethodGet, - Path: "/books", - Handler: bookController.GetBooks, + Path: "", + Handler: bookService.List, Middlewares: []okapi.Middleware{middlewares.CustomMiddleware}, - Options: []okapi.RouteOption{ - okapi.DocSummary("Get Books"), - okapi.DocDescription("Retrieve a list of books"), - okapi.DocResponse([]models.Book{}), - okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}), - }, + Group: apiGroup, + Summary: "Get Books", + Description: "Retrieve a list of books", + Response: &models.BooksResponse{}, }, { Method: http.MethodGet, - Path: "/books/:id", - Handler: bookController.GetBook, + Path: "/:id", + Handler: bookService.Get, Middlewares: []okapi.Middleware{middlewares.CustomMiddleware}, + Group: apiGroup, + Summary: "Get Book by ID", + Description: "Retrieve a book by its ID", Options: []okapi.RouteOption{ - okapi.DocSummary("Get Book by ID"), - okapi.DocDescription("Retrieve a book by its ID"), okapi.DocPathParam("id", "int", "The ID of the book"), - okapi.DocResponse(models.Book{}), - okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}), - okapi.DocResponse(http.StatusNotFound, models.ErrorResponse{}), - }, + okapi.DocResponse(&models.Response[models.Book]{}), + okapi.DocResponse(http.StatusBadRequest, &models.Response[any]{})}, }, } } // *************** Auth Routes **************** -func (r *Route) AuthRoute() okapi.RouteDefinition { - apiGroup := &okapi.Group{Prefix: "/auth", Tags: []string{"AuthController"}} +func (r *Route) authRoute() okapi.RouteDefinition { + apiGroup := r.group.Group("/auth").WithTags([]string{"AuthService"}) apiGroup.Use(middlewares.CustomMiddleware) return okapi.RouteDefinition{ - Method: http.MethodPost, - Path: "/login", - Handler: authController.Login, - Group: apiGroup, - Options: []okapi.RouteOption{ - okapi.DocSummary("Login"), - okapi.DocDescription("User login to get a JWT token"), - okapi.DocRequestBody(models.AuthRequest{}), - okapi.DocResponse(models.AuthResponse{}), - okapi.DocResponse(http.StatusUnauthorized, models.AuthResponse{}), - }, + Method: http.MethodPost, + Path: "/login", + Handler: authService.Login, + Group: apiGroup, + Summary: "Login", + Description: "User login to get a JWT token", + Request: &models.AuthRequest{}, + Response: &models.AuthResponse{}, } } // ************** Authenticated Routes ************** -func (r *Route) CommonRoutes() []okapi.RouteDefinition { - coreGroup := &okapi.Group{Prefix: "/core", Tags: []string{"SecurityController"}} +func (r *Route) coreRoutes() []okapi.RouteDefinition { + coreGroup := r.group.Group("/core").WithTags([]string{"CoreService"}) // Apply JWT authentication middleware to the admin group coreGroup.Use(middlewares.JWTAuth.Middleware) coreGroup.Use(middlewares.CustomMiddleware) - coreGroup.WithSecurity(bearerAuthSecurity) //Enable Bearer token for OpenAPI documentation + // Enable Bearer token for OpenAPI documentation + coreGroup.WithSecurity(bearerAuthSecurity) return []okapi.RouteDefinition{ { - Method: http.MethodPost, - Path: "/whoami", - Handler: authController.WhoAmI, - Group: coreGroup, - Options: []okapi.RouteOption{ - okapi.DocSummary("Whoami"), - okapi.DocDescription("Get the current user's information"), - okapi.DocResponse(models.UserInfo{}), - }, + Method: http.MethodPost, + Path: "/whoami", + Handler: authService.WhoAmI, + Group: coreGroup, + Summary: "Whoami", + Description: "Get the current user's information", + Response: &models.Response[models.UserInfo]{}, }, } } @@ -219,24 +217,23 @@ func (r *Route) CommonRoutes() []okapi.RouteDefinition { // ***************** Admin Routes ***************** func (r *Route) AdminRoutes() []okapi.RouteDefinition { - apiGroup := &okapi.Group{Prefix: "/admin", Tags: []string{"AdminController"}} + apiGroup := r.group.Group("/admin").WithTags([]string{"AdminService"}) // Apply JWT authentication middleware to the admin group apiGroup.Use(middlewares.AdminJWTAuth.Middleware) apiGroup.Use(middlewares.CustomMiddleware) - apiGroup.WithBearerAuth() //Enable Bearer token for OpenAPI documentation + apiGroup.WithBearerAuth() // Enable Bearer token for OpenAPI documentation return []okapi.RouteDefinition{ { Method: http.MethodPost, Path: "/books", - Handler: bookController.CreateBook, + Handler: bookService.Create, Group: apiGroup, Options: []okapi.RouteOption{ okapi.DocSummary("Create Book"), okapi.DocDescription("Create a new book"), okapi.DocRequestBody(models.Book{}), - okapi.DocResponse(models.Response{}), }, Security: bearerAuthSecurity, }, @@ -244,7 +241,7 @@ func (r *Route) AdminRoutes() []okapi.RouteDefinition { { Method: http.MethodGet, Path: "/books", - Handler: bookController.GetBooks, + Handler: bookService.List, Group: apiGroup, Options: []okapi.RouteOption{ okapi.DocSummary("Get Books"), diff --git a/services/auth_service.go b/services/auth_service.go new file mode 100644 index 0000000..52c14a4 --- /dev/null +++ b/services/auth_service.go @@ -0,0 +1,43 @@ +package services + +import ( + "fmt" + + "github.com/jkaninda/okapi" + "github.com/jkaninda/okapi-example/middlewares" + "github.com/jkaninda/okapi-example/models" +) + +type AuthService struct{} + +// ******************** AuthService ***************** + +func (bc *AuthService) Login(c *okapi.Context) error { + authRequest := &models.AuthRequest{} + err := c.Bind(authRequest) + if err != nil { + return c.ErrorBadRequest(models.ErrorResponse("Bad Request", err)) + } + // Validate the authRequest and generate a JWT token + authResponse, err := middlewares.Login(authRequest) + if err != nil { + return c.ErrorUnauthorized(models.ErrorResponse("Invalid username or password", err)) + } + return c.OK(models.SuccessResponse("Welcome back", authResponse)) +} +func (bc *AuthService) WhoAmI(c *okapi.Context) error { + // Get User Information from the context, shared by the JWT middleware using forwardClaims + email := c.GetString("email") + if email == "" { + return c.AbortUnauthorized("Unauthorized", fmt.Errorf("user not authenticated")) + } + + c.Response().Header().Set("X-Okapi-User", email) + c.Response().Header().Set("X-Okapi-User-Name", c.GetString("name")) + c.Response().Header().Set("X-Okapi-Role", c.GetString("role")) + // Respond with the current user information + return c.OK(models.SuccessResponse("Ok", models.UserInfo{ + Email: email, + Role: c.GetString("role"), + Name: c.GetString("name")})) +} diff --git a/services/book_service.go b/services/book_service.go new file mode 100644 index 0000000..da725c0 --- /dev/null +++ b/services/book_service.go @@ -0,0 +1,78 @@ +package services + +import ( + "strconv" + "strings" + + "github.com/jkaninda/okapi" + "github.com/jkaninda/okapi-example/data" + "github.com/jkaninda/okapi-example/models" +) + +type BookService struct{} + +var ( + books = []*models.Book{} +) + +func (bc *BookService) List(c *okapi.Context) error { + q := c.Query("q") + books = bc.booksData() + if len(q) > 0 { + return c.OK(searchBooks(books, q)) + } + return c.OK(books) +} + +func (bc *BookService) Create(c *okapi.Context) error { + // Simulate creating a book in a database + book := &models.Book{} + err := c.Bind(book) + if err != nil { + return c.ErrorBadRequest(models.ErrorResponse) + } + book.Id = len(books) + 1 + books = append(books, book) + + return c.OK(models.SuccessResponse("Book created successfully", book)) +} +func (bc *BookService) Get(c *okapi.Context) error { + id := c.Param("id") + i, err := strconv.Atoi(id) + if err != nil { + return c.ErrorBadRequest(models.ErrorResponse("Bad Request", err)) + } + // Simulate a fetching book from a database + + books = bc.booksData() + + for _, book := range books { + if book.Id == i { + return c.OK(book) + } + } + return c.AbortNotFound("Book not found") +} +func (bc *BookService) booksData() []*models.Book { + books, _ = data.Books() + return books +} +func searchBooks(books []*models.Book, query string) []*models.Book { + if query == "" { + return books + } + + query = strings.ToLower(strings.TrimSpace(query)) + var results []*models.Book + + for _, book := range books { + titleMatch := strings.Contains(strings.ToLower(book.Title), query) + authorMatch := strings.Contains(strings.ToLower(book.Author), query) + + if titleMatch || authorMatch { + results = append(results, book) + } + } + + return results +} diff --git a/services/book_service_test.go b/services/book_service_test.go new file mode 100644 index 0000000..621b3e8 --- /dev/null +++ b/services/book_service_test.go @@ -0,0 +1,26 @@ +package services + +import ( + "testing" + + "github.com/jkaninda/okapi" + "github.com/jkaninda/okapi/okapitest" +) + +var bookService = &BookService{} + +func TestBooksAPI(t *testing.T) { + // Setup test server + server := okapi.NewTestServer(t) + + server.Get("/v1/books", bookService.List) + server.Get("/v1/books/{id:int}", bookService.Get) + + // Create reusable client + client := okapitest.NewClient(t, server.BaseURL) + + client.GET("/v1/books").ExpectStatusOK().ExpectBodyContains("The Kubernetes Bible") + client.GET("/v1/books?q=Vol2").ExpectStatusOK().ExpectBodyContains("System Design Interview Vol2") + client.GET("/v1/books/4").ExpectStatusOK().ExpectBodyContains("System Design Interview Vol2") + +} diff --git a/services/common_service.go b/services/common_service.go new file mode 100644 index 0000000..668060f --- /dev/null +++ b/services/common_service.go @@ -0,0 +1,152 @@ +/* + * MIT License + * + * Copyright (c) 2025 Jonas Kaninda + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package services + +import ( + "fmt" + "log" + "net/http" + "time" + + goutils "github.com/jkaninda/go-utils" + "github.com/jkaninda/logger" + "github.com/jkaninda/okapi" + "github.com/jkaninda/okapi-example/models" + "github.com/jkaninda/okapi-example/session" + okapiws "github.com/jkaninda/okapi-ws" +) + +type CommonService struct { + SessionManager *session.SessionManager +} + +// WebSocket upgrades the HTTP connection to WebSocket. +// Config is optional; pass nil to use default settings. +func WebSocket(config *okapiws.WSConfig, c *okapi.Context) (*okapiws.WSConnection, error) { + upgrader := okapiws.NewWSUpgrader(config) + return upgrader.Upgrade(c.Response(), c.Request(), nil) +} + +// WebSocketWithHeaders upgrades with additional response headers. +func WebSocketWithHeaders(config *okapiws.WSConfig, headers http.Header, c *okapi.Context) (*okapiws.WSConnection, error) { + upgrader := okapiws.NewWSUpgrader(config) + return upgrader.Upgrade(c.Response(), c.Request(), headers) +} + +// ****************** CommonService ***************** + +func (cs *CommonService) Home(c *okapi.Context) error { + return c.Render(http.StatusOK, "home", okapi.M{ + "title": "Go Okapi basic implementation example ", + "message": "Hello from Okapi!", + "appName": "Go Okapi Bookstore", + "headline": "Discover your next great read", + "books": books, + }) +} +func (cs *CommonService) Session(c *okapi.Context) error { + // Generate unique session ID + sessionID := fmt.Sprintf("session:%s-%s-%s", c.Request().RemoteAddr, goutils.Slug(c.Request().UserAgent()), time.Now().Format("20060102150405")) + + // Add session + cs.SessionManager.AddSession(sessionID) + defer cs.SessionManager.RemoveSession(sessionID) + + connectedAt := time.Now() + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + ctx := c.Request().Context() + + for { + select { + case <-ctx.Done(): + return nil + case t := <-ticker.C: + data := okapi.M{ + "time": t.Format("15:04:05"), + "connectedSince": goutils.FormatDuration(time.Since(connectedAt), 1), + "activeConnections": cs.SessionManager.GetCount(), + "totalSessions": cs.SessionManager.GetTotalCount(), + "timestamp": t.Unix(), + } + + if err := c.SSEvent("message", data); err != nil { + return err + } + } + } +} + +func (cs *CommonService) WhoAmI(c *okapi.Context) error { + wr := &models.WhoAmIRequest{} + if err := c.Bind(wr); err != nil { + return c.AbortBadRequest("Bad request", err) + } + if wr.Email == "" { + logger.Warn("no email found") + } + return c.OK(models.WhoAmIResponse{ + Host: c.Request().Host, + RealIp: c.RealIP(), + CurrentUser: models.UserInfo{ + Name: wr.Name, + Email: wr.Email, + Role: wr.Role, + }, + }) +} + +func (cs *CommonService) WebSocketHandle(c *okapi.Context) error { + token := c.Query("token") + if len(token) == 0 { + return c.AbortBadRequest("Bad Request", fmt.Errorf("missing token")) + } + ws, err := WebSocket(nil, c) + if err != nil { + return err + } + defer func() { + if err := ws.Close(); err != nil { + log.Printf("error closing WebSocket: %v", err) + } + }() + + ws.OnMessage(func(msg *okapiws.WSMessage) { + log.Printf("[%d] %s", msg.Type, msg.Data) + // Echo the message back + _ = ws.Send(msg.Data) + }) + + ws.OnError(func(err error) { + log.Printf("WebSocket error: %v", err) + }) + + ws.Start() + + // Block until the connection is closed + <-ws.Context().Done() + return nil +} diff --git a/services/common_service_test.go b/services/common_service_test.go new file mode 100644 index 0000000..70ef605 --- /dev/null +++ b/services/common_service_test.go @@ -0,0 +1,59 @@ +/* + * MIT License + * + * Copyright (c) 2025 Jonas Kaninda + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package services + +import ( + "html/template" + "io" + "testing" + + "github.com/jkaninda/okapi" + "github.com/jkaninda/okapi-example/session" + "github.com/jkaninda/okapi/okapitest" +) + +type Template struct { + templates *template.Template +} + +func (t *Template) Render(w io.Writer, name string, data interface{}, c *okapi.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} + +var commonService = &CommonService{SessionManager: session.New()} + +func TestCommonAPI(t *testing.T) { + // Setup test server + server := okapi.NewTestServer(t) + server.With().WithRenderer(&Template{templates: template.Must(template.ParseGlob("../views/*.html"))}) + + server.Get("/", commonService.Home) + + // Create reusable client + client := okapitest.NewClient(t, server.BaseURL) + + client.GET("/").ExpectStatusOK().ExpectBodyContains("Go Okapi Bookstore") + +} diff --git a/session/sessiom_manager.go b/session/sessiom_manager.go new file mode 100644 index 0000000..3351c1b --- /dev/null +++ b/session/sessiom_manager.go @@ -0,0 +1,51 @@ +package session + +import ( + "sync" + "time" +) + +// SessionManager handles SSE session tracking +type SessionManager struct { + mu sync.RWMutex + sessions map[string]time.Time + count int +} + +func New() *SessionManager { + return &SessionManager{ + sessions: make(map[string]time.Time), + } +} + +// AddSession adds a new session and returns the current count +func (sm *SessionManager) AddSession(id string) int { + sm.mu.Lock() + defer sm.mu.Unlock() + + sm.sessions[id] = time.Now() + sm.count++ + return sm.count +} + +// RemoveSession removes a session and returns the remaining count +func (sm *SessionManager) RemoveSession(id string) int { + sm.mu.Lock() + defer sm.mu.Unlock() + delete(sm.sessions, id) + return len(sm.sessions) +} + +// GetCount returns the current active session count +func (sm *SessionManager) GetCount() int { + sm.mu.RLock() + defer sm.mu.RUnlock() + return len(sm.sessions) +} + +// GetTotalCount returns the total number of sessions since server start +func (sm *SessionManager) GetTotalCount() int { + sm.mu.RLock() + defer sm.mu.RUnlock() + return sm.count +} diff --git a/utils/util.go b/utils/util.go index 7d25860..45e36aa 100644 --- a/utils/util.go +++ b/utils/util.go @@ -1,11 +1,5 @@ package utils -import "os" +const AppName = "Okapi Web Framework Example" -func GetSingingSecret() string { - value := os.Getenv("JWT_SIGNING_SECRET") - if value == "" { - return "supersecret" - } - return value -} +var AppVersion = "1.0" diff --git a/views/home.html b/views/home.html new file mode 100644 index 0000000..0a7519c --- /dev/null +++ b/views/home.html @@ -0,0 +1,666 @@ +{{define "home"}} + + + + + + {{.title}} + + + + +
+
+
+

📚 {{.appName}}

+

{{.headline}}

+
+ +
+
+
+ + +
+
🟢 Active
+
+
+
+ +
+
📊 Total
+
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+
Total Books
+
+
+
+
Average Price
+
+
+
+ +
+
Server Time
+
+
+
+ + +
+
Connected Since
+
+
+ +
+ Loading books +
+ +
+ +
+ +
+ + + +

No Books Found

+

Try adjusting your search or browse all books

+
+
+ + + + +{{end}} \ No newline at end of file