Compare commits
36 Commits
f1194d3da8
...
feat/pipel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7580f47a7d | ||
|
|
e251e7b2a3 | ||
|
|
20742bb613 | ||
|
|
54a46458a3 | ||
|
|
29972f54d1 | ||
|
|
f35c563dac | ||
| 2ee7b3d2a2 | |||
| b7c0b1c8d4 | |||
| b8ebe03921 | |||
| 26635ace84 | |||
| 031be8ad5d | |||
| f316fabcb0 | |||
| b1dc8e0b5d | |||
| 641a479b6a | |||
| d80e15b55e | |||
|
|
1fcaf6e174 | ||
|
|
ef97bf0a75 | ||
|
|
4c8a8e43ed | ||
|
|
8db6992296 | ||
|
|
7925fc6caf | ||
|
|
b1f4ff2641 | ||
|
|
a852beff21 | ||
|
|
e17afe909f | ||
|
|
9ed37806dd | ||
|
|
5a21589029 | ||
|
|
189bce040b | ||
| e8c5b8a80c | |||
|
|
d47e3ed7f0 | ||
|
|
92ec16ce93 | ||
|
|
c2db65d9b1 | ||
|
|
1f008f1237 | ||
|
|
898cc8971b | ||
|
|
fd3c172e35 | ||
|
|
581863d69b | ||
|
|
9238036042 | ||
|
|
431e11e052 |
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"isRoot": true,
|
|
||||||
"tools": {
|
|
||||||
"csharpier": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"commands": [
|
|
||||||
"csharpier"
|
|
||||||
],
|
|
||||||
"rollForward": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
archive/** linguist-vendored
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,6 +7,7 @@ assignees: []
|
|||||||
---
|
---
|
||||||
|
|
||||||
## User Story
|
## User Story
|
||||||
|
|
||||||
**As a** (who wants to accomplish something)
|
**As a** (who wants to accomplish something)
|
||||||
**I want to** (what they want to accomplish)
|
**I want to** (what they want to accomplish)
|
||||||
**So that** (why they want to accomplish that thing)
|
**So that** (why they want to accomplish that thing)
|
||||||
@@ -15,29 +16,18 @@ assignees: []
|
|||||||
|
|
||||||
### Scenario 1
|
### Scenario 1
|
||||||
|
|
||||||
|
Given ... When ... Then ...
|
||||||
Given ...
|
|
||||||
When ...
|
|
||||||
Then ...
|
|
||||||
|
|
||||||
|
|
||||||
### Scenario 2
|
### Scenario 2
|
||||||
|
|
||||||
|
Given ... When ... Then ...
|
||||||
Given ...
|
|
||||||
When ...
|
|
||||||
Then ...
|
|
||||||
|
|
||||||
|
|
||||||
### Scenario 3
|
### Scenario 3
|
||||||
|
|
||||||
|
Given ... When ... Then ...
|
||||||
Given ...
|
|
||||||
When ...
|
|
||||||
Then ...
|
|
||||||
|
|
||||||
|
|
||||||
## Subtasks
|
## Subtasks
|
||||||
|
|
||||||
- [ ] Task 1
|
- [ ] Task 1
|
||||||
- [ ] Task 2
|
- [ ] Task 2
|
||||||
- [ ] Task 3
|
- [ ] Task 3
|
||||||
14
.gitignore
vendored
@@ -15,6 +15,14 @@
|
|||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
# project-specific build artifacts
|
||||||
|
/src/Website/build/
|
||||||
|
/src/Website/storybook-static/
|
||||||
|
/src/Website/.react-router/
|
||||||
|
/src/Website/playwright-report/
|
||||||
|
/src/Website/test-results/
|
||||||
|
/test-results/
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
@@ -42,6 +50,9 @@ next-env.d.ts
|
|||||||
|
|
||||||
# vscode
|
# vscode
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
/cloudinary-images
|
/cloudinary-images
|
||||||
|
|
||||||
@@ -487,3 +498,6 @@ FodyWeavers.xsd
|
|||||||
.env.dev
|
.env.dev
|
||||||
.env.test
|
.env.test
|
||||||
.env.prod
|
.env.prod
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
storybook-static
|
||||||
|
|||||||
903
LICENSE.md
318
README.md
@@ -1,261 +1,145 @@
|
|||||||
# The Biergarten App
|
# The Biergarten App
|
||||||
|
|
||||||
A social platform for craft beer enthusiasts to discover breweries, share reviews, and
|
The Biergarten App is a full-stack directory and discovery platform for
|
||||||
connect with fellow beer lovers.
|
breweries. It features a robust user authentication system, a searchable
|
||||||
|
database of brewery locations, and a custom offline data-generation pipeline
|
||||||
|
that uses LLMs (Llama.cpp) and Wikipedia to synthesize realistic seed data.
|
||||||
|
|
||||||
**Documentation**
|
It features:
|
||||||
|
|
||||||
- [Getting Started](docs/getting-started.md) - Setup and installation
|
- A .NET backend (Web API + database migrations/seed) under `web/backend/`
|
||||||
- [Architecture](docs/architecture.md) - System design and patterns
|
- A server-rendered React website (React Router + Vite) under `web/frontend/`
|
||||||
- [Database](docs/database.md) - Schema and stored procedures
|
- A C++20 “pipeline” CLI for generating seed data under `tooling/pipeline/`
|
||||||
- [Docker Guide](docs/docker.md) - Container deployment
|
|
||||||
- [Testing](docs/testing.md) - Test strategy and commands
|
|
||||||
- [Environment Variables](docs/environment-variables.md) - Configuration reference
|
|
||||||
|
|
||||||
**Diagrams**
|
Specialized documentation (setup, architecture, docker, testing, diagrams, and
|
||||||
|
pipeline notes) lives under `docs/`.
|
||||||
|
|
||||||
- [Architecture](docs/diagrams/pdf/architecture.pdf) - Layered architecture
|
## Documentation (Start Here)
|
||||||
- [Deployment](docs/diagrams/pdf/deployment.pdf) - Docker topology
|
|
||||||
- [Authentication Flow](docs/diagrams/pdf/authentication-flow.pdf) - Auth sequence
|
|
||||||
- [Database Schema](docs/diagrams/pdf/database-schema.pdf) - Entity relationships
|
|
||||||
|
|
||||||
## Project Status
|
Website + backend (active stack):
|
||||||
|
|
||||||
**Active Development** - Transitioning from full-stack Next.js to multi-project monorepo
|
- [Getting Started](docs/website/getting-started.md)
|
||||||
|
- [Architecture](docs/architecture.md)
|
||||||
|
- [Docker Guide](docs/website/docker.md)
|
||||||
|
- [Testing](docs/website/testing.md)
|
||||||
|
- [Environment Variables](docs/website/environment-variables.md)
|
||||||
|
- [Token Validation](docs/website/token-validation.md)
|
||||||
|
|
||||||
- Core authentication and user management APIs
|
Data generation pipeline (C++):
|
||||||
- Database schema with migrations and seeding
|
|
||||||
- Layered architecture (Domain, Service, Infrastructure, Repository, API)
|
|
||||||
- Comprehensive test suite (unit + integration)
|
|
||||||
- Frontend integration with .NET API (in progress)
|
|
||||||
- Migration from Next.js serverless functions
|
|
||||||
|
|
||||||
---
|
- [Pipeline README](docs/pipeline/README.md)
|
||||||
|
- [Ethics & Known Issues](docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md)
|
||||||
|
|
||||||
|
## Diagrams
|
||||||
|
|
||||||
|
- [Architecture](docs/website/diagrams-out/architecture.svg)
|
||||||
|
- [Deployment](docs/website/diagrams-out/deployment.svg)
|
||||||
|
- [Authentication Flow](docs/website/diagrams-out/authentication-flow.svg)
|
||||||
|
- [Database Schema](docs/website/diagrams-out/database-schema.svg)
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
Active areas in the repository:
|
||||||
|
|
||||||
|
- .NET 10 backend (layered architecture) + SQL Server
|
||||||
|
- React 19 website (React Router 7 + Vite)
|
||||||
|
- Shared Biergarten theme system + Storybook coverage
|
||||||
|
- Auth flows and account/email integration (local Mailpit in dev compose)
|
||||||
|
- Data generation pipeline with C++ and Llama.cpp
|
||||||
|
|
||||||
|
Archived/reference areas:
|
||||||
|
|
||||||
|
- `archive/next-js-web-app/` contains an older Next.js frontend retained for
|
||||||
|
reference
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
**Backend**: .NET 10, ASP.NET Core, SQL Server 2022, DbUp **Frontend**: Next.js 14+,
|
- **Backend**: .NET 10, ASP.NET Core, SQL Server 2022, DbUp
|
||||||
TypeScript, TailwindCSS **Testing**: xUnit, Reqnroll (BDD), FluentAssertions, Moq
|
- **Frontend**: React 19, React Router 7, Vite 7, Tailwind CSS 4, DaisyUI 5
|
||||||
**Infrastructure**: Docker, Docker Compose **Security**: Argon2id password hashing, JWT
|
- **UI Documentation**: Storybook 10, Vitest browser mode, Playwright
|
||||||
(HS256)
|
- **Testing**: xUnit, Reqnroll (BDD), FluentAssertions, Moq
|
||||||
|
- **Infrastructure**: Docker, Docker Compose
|
||||||
---
|
- **Security**: Argon2id password hashing, JWT access/refresh/confirmation
|
||||||
|
tokens
|
||||||
|
- **Data Pipeline**: C++20, CMake, Boost, libcurl, SQLite, llama.cpp
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
For full setup details, use [Getting Started](docs/website/getting-started.md).
|
||||||
|
This section is the shortest path to a working dev environment.
|
||||||
|
|
||||||
- [.NET SDK 10+](https://dotnet.microsoft.com/download)
|
### Backend (Docker)
|
||||||
- [Docker Desktop](https://www.docker.com/products/docker-desktop)
|
|
||||||
- [Node.js 18+](https://nodejs.org/) (for frontend)
|
|
||||||
|
|
||||||
### Start Development Environment
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone repository
|
|
||||||
git clone https://github.com/aaronpo97/the-biergarten-app
|
git clone https://github.com/aaronpo97/the-biergarten-app
|
||||||
cd the-biergarten-app
|
cd the-biergarten-app
|
||||||
|
|
||||||
# Configure environment
|
cp web/.env.example web/.env.dev
|
||||||
cp .env.example .env.dev
|
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up --build -d
|
||||||
|
|
||||||
# Start all services
|
|
||||||
docker compose -f docker-compose.dev.yaml up -d
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker compose -f docker-compose.dev.yaml logs -f
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Access**:
|
Backend access:
|
||||||
|
|
||||||
- API: http://localhost:8080/swagger
|
- API Swagger: http://localhost:8080/swagger
|
||||||
- Health: http://localhost:8080/health
|
- Health Check: http://localhost:8080/health
|
||||||
|
- Mailpit UI (dev SMTP): http://localhost:8025
|
||||||
|
|
||||||
### Run Tests
|
### Frontend (Node)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.test.yaml up --abort-on-container-exit
|
cd web/frontend
|
||||||
|
npm install
|
||||||
|
API_BASE_URL=http://localhost:8080 SESSION_SECRET=dev-secret-change-me npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Results are in `./test-results/`
|
Optional frontend tools:
|
||||||
|
|
||||||
---
|
```bash
|
||||||
|
cd web/frontend
|
||||||
|
npm run storybook
|
||||||
|
npm run test:storybook
|
||||||
|
npm run test:storybook:playwright
|
||||||
|
```
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
web/
|
||||||
|
backend/ .NET API + domain/service/infrastructure + DB projects
|
||||||
|
frontend/ React Router website + Storybook + Playwright/Vitest
|
||||||
|
|
||||||
|
tooling/
|
||||||
|
pipeline/ C++20 seed-data generation CLI (CMake)
|
||||||
|
|
||||||
|
docs/
|
||||||
|
architecture.md High-level architecture overview
|
||||||
|
website/ Backend/frontend setup, docker, testing, diagrams
|
||||||
|
pipeline/ Pipeline docs, ethics notes, PlantUML diagrams
|
||||||
|
|
||||||
|
archive/
|
||||||
|
next-js-web-app/ Older Next.js frontend (reference only)
|
||||||
```
|
```
|
||||||
src/Core/ # Backend (.NET)
|
|
||||||
├── API/
|
|
||||||
│ ├── API.Core/ # ASP.NET Core Web API
|
|
||||||
│ └── API.Specs/ # Integration tests (Reqnroll)
|
|
||||||
├── Database/
|
|
||||||
│ ├── Database.Migrations/ # DbUp migrations
|
|
||||||
│ └── Database.Seed/ # Data seeding
|
|
||||||
├── Domain.Entities/ # Domain models
|
|
||||||
├── Infrastructure/ # Cross-cutting concerns
|
|
||||||
│ ├── Infrastructure.Jwt/
|
|
||||||
│ ├── Infrastructure.PasswordHashing/
|
|
||||||
│ ├── Infrastructure.Email/
|
|
||||||
│ ├── Infrastructure.Repository/
|
|
||||||
│ └── Infrastructure.Repository.Tests/
|
|
||||||
└── Service/ # Business logic
|
|
||||||
├── Service.Auth/
|
|
||||||
├── Service.Auth.Tests/
|
|
||||||
└── Service.UserManagement/
|
|
||||||
|
|
||||||
Website/ # Frontend (Next.js)
|
|
||||||
docs/ # Documentation
|
|
||||||
docs/diagrams/ # PlantUML diagrams
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### Implemented
|
|
||||||
|
|
||||||
- User registration and authentication
|
|
||||||
- JWT token-based auth
|
|
||||||
- Argon2id password hashing
|
|
||||||
- SQL Server with stored procedures
|
|
||||||
- Database migrations (DbUp)
|
|
||||||
- Docker containerization
|
|
||||||
- Comprehensive test suite
|
|
||||||
- Swagger/OpenAPI documentation
|
|
||||||
- Health checks
|
|
||||||
|
|
||||||
### Planned
|
|
||||||
|
|
||||||
- [ ] Brewery discovery and management
|
|
||||||
- [ ] Beer reviews and ratings
|
|
||||||
- [ ] Social following/followers
|
|
||||||
- [ ] Geospatial brewery search
|
|
||||||
- [ ] Image upload (Cloudinary)
|
|
||||||
- [ ] Email notifications
|
|
||||||
- [ ] OAuth integration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Highlights
|
|
||||||
|
|
||||||
### Layered Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
API Layer (Controllers)
|
|
||||||
│
|
|
||||||
Service Layer (Business Logic)
|
|
||||||
│
|
|
||||||
Infrastructure Layer (Repositories, JWT, Email)
|
|
||||||
│
|
|
||||||
Domain Layer (Entities)
|
|
||||||
│
|
|
||||||
Database (SQL Server + Stored Procedures)
|
|
||||||
```
|
|
||||||
|
|
||||||
### SQL-First Approach
|
|
||||||
|
|
||||||
- All queries via stored procedures
|
|
||||||
- No ORM (no Entity Framework)
|
|
||||||
- Version-controlled schema
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- **Password Hashing**: Argon2id (64MB memory, 4 iterations)
|
|
||||||
- **JWT Tokens**: HS256 with configurable expiration
|
|
||||||
- **Credential Rotation**: Built-in password change support
|
|
||||||
|
|
||||||
See [Architecture Guide](docs/architecture.md) for details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
The project includes three test suites:
|
Run the backend test stack with Docker:
|
||||||
|
|
||||||
| Suite | Type | Framework | Purpose |
|
|
||||||
| ---------------------- | ----------- | -------------- | ---------------------- |
|
|
||||||
| **API.Specs** | Integration | Reqnroll (BDD) | End-to-end API testing |
|
|
||||||
| **Repository.Tests** | Unit | xUnit | Data access layer |
|
|
||||||
| **Service.Auth.Tests** | Unit | xUnit + Moq | Business logic |
|
|
||||||
|
|
||||||
**Run All Tests**:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.test.yaml up --abort-on-container-exit
|
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit
|
||||||
```
|
```
|
||||||
|
|
||||||
**Run Individual Test Suite**:
|
See [Testing](docs/website/testing.md) for the full command list.
|
||||||
|
|
||||||
```bash
|
|
||||||
cd src/Core
|
|
||||||
dotnet test API/API.Specs/API.Specs.csproj
|
|
||||||
dotnet test Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj
|
|
||||||
dotnet test Service/Service.Auth.Tests/Service.Auth.Tests.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Testing Guide](docs/testing.md) for more information.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Docker Environments
|
|
||||||
|
|
||||||
The project uses three Docker Compose configurations:
|
|
||||||
|
|
||||||
| File | Purpose | Features |
|
|
||||||
| ---------------------------- | ------------- | ------------------------------------------------- |
|
|
||||||
| **docker-compose.dev.yaml** | Development | Persistent data, hot reload, Swagger UI |
|
|
||||||
| **docker-compose.test.yaml** | CI/CD Testing | Isolated DB, auto-exit, test results export |
|
|
||||||
| **docker-compose.prod.yaml** | Production | Optimized builds, health checks, restart policies |
|
|
||||||
|
|
||||||
**Common Commands**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
docker compose -f docker-compose.dev.yaml up -d
|
|
||||||
docker compose -f docker-compose.dev.yaml logs -f api.core
|
|
||||||
docker compose -f docker-compose.dev.yaml down -v
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
docker compose -f docker-compose.test.yaml up --abort-on-container-exit
|
|
||||||
docker compose -f docker-compose.test.yaml down -v
|
|
||||||
|
|
||||||
# Build
|
|
||||||
docker compose -f docker-compose.dev.yaml build
|
|
||||||
docker compose -f docker-compose.dev.yaml build --no-cache
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Docker Guide](docs/docker.md) for troubleshooting and advanced usage.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Required Environment Variables
|
Common active variables:
|
||||||
|
|
||||||
**Backend** (`.env.dev`):
|
- Backend/Docker: `DB_SERVER`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`,
|
||||||
|
`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`,
|
||||||
|
`WEBSITE_BASE_URL`
|
||||||
|
- Frontend runtime: `API_BASE_URL`, `SESSION_SECRET`, `NODE_ENV`
|
||||||
|
|
||||||
```bash
|
See [Environment Variables](docs/website/environment-variables.md) for details.
|
||||||
DB_SERVER=sqlserver,1433
|
|
||||||
DB_NAME=Biergarten
|
|
||||||
DB_USER=sa
|
|
||||||
DB_PASSWORD=YourStrong!Passw0rd
|
|
||||||
JWT_SECRET=<min-32-chars>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend** (`.env.local`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
BASE_URL=http://localhost:3000
|
|
||||||
NODE_ENV=development
|
|
||||||
CONFIRMATION_TOKEN_SECRET=<generated>
|
|
||||||
RESET_PASSWORD_TOKEN_SECRET=<generated>
|
|
||||||
SESSION_SECRET=<generated>
|
|
||||||
# + External services (Cloudinary, Mapbox, SparkPost)
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Environment Variables Guide](docs/environment-variables.md) for complete reference.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -264,15 +148,3 @@ See [Environment Variables Guide](docs/environment-variables.md) for complete re
|
|||||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
5. Open a Pull Request
|
5. Open a Pull Request
|
||||||
|
|
||||||
### Development Workflow
|
|
||||||
|
|
||||||
1. Start development environment: `docker compose -f docker-compose.dev.yaml up -d`
|
|
||||||
2. Make changes to code
|
|
||||||
3. Run tests: `docker compose -f docker-compose.test.yaml up --abort-on-container-exit`
|
|
||||||
4. Rebuild if needed: `docker compose -f docker-compose.dev.yaml up -d --build api.core`
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
- **Documentation**: [docs/](docs/)
|
|
||||||
- **Architecture**: See [Architecture Guide](docs/architecture.md)
|
|
||||||
|
|||||||
0
src/Website/package-lock.json → archive/next-js-web-app/package-lock.json
generated
vendored
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 515 B After Width: | Height: | Size: 515 B |
|
Before Width: | Height: | Size: 961 B After Width: | Height: | Size: 961 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 256 KiB |