[Spec Kit] Implementation progress
Implements all 88 tasks for the Reaction Image Board (specs/001-reaction-image-board): - docker-compose.yml: postgres, minio, minio-init, api, ui services with healthchecks - api/: FastAPI app with SQLAlchemy 2.x async, Alembic migrations, S3/MinIO storage, full integration + unit test suite (pytest + pytest-asyncio) - ui/: Angular 19 standalone app (Library, Upload, Detail, NotFound components) - .env.example: all required environment variables - .gitignore: Python, Node, Docker, IDE, .env patterns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
64
ui/src/app/services/image.service.ts
Normal file
64
ui/src/app/services/image.service.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export interface ImageRecord {
|
||||
id: string;
|
||||
hash: string;
|
||||
filename: string;
|
||||
mime_type: string;
|
||||
size_bytes: number;
|
||||
width: number;
|
||||
height: number;
|
||||
storage_key: string;
|
||||
created_at: string;
|
||||
tags: string[];
|
||||
duplicate?: boolean;
|
||||
}
|
||||
|
||||
export interface ImageListResponse {
|
||||
items: ImageRecord[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ImageService {
|
||||
private readonly base = '/api/v1';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
upload(file: File, tags: string[]): Observable<ImageRecord> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
if (tags.length) {
|
||||
form.append('tags', tags.join(','));
|
||||
}
|
||||
return this.http.post<ImageRecord>(`${this.base}/images`, form);
|
||||
}
|
||||
|
||||
list(tagFilter: string[] = [], limit = 50, offset = 0): Observable<ImageListResponse> {
|
||||
let params = new HttpParams().set('limit', limit).set('offset', offset);
|
||||
if (tagFilter.length) {
|
||||
params = params.set('tags', tagFilter.join(','));
|
||||
}
|
||||
return this.http.get<ImageListResponse>(`${this.base}/images`, { params });
|
||||
}
|
||||
|
||||
get(id: string): Observable<ImageRecord> {
|
||||
return this.http.get<ImageRecord>(`${this.base}/images/${id}`);
|
||||
}
|
||||
|
||||
getFileUrl(id: string): string {
|
||||
return `${this.base}/images/${id}/file`;
|
||||
}
|
||||
|
||||
updateTags(id: string, tags: string[]): Observable<ImageRecord> {
|
||||
return this.http.patch<ImageRecord>(`${this.base}/images/${id}/tags`, { tags });
|
||||
}
|
||||
|
||||
delete(id: string): Observable<void> {
|
||||
return this.http.delete<void>(`${this.base}/images/${id}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user