web/src/lib/api.ts (view raw)
1const BASE = '/api';
2
3export interface User {
4 id: number;
5 username: string;
6 is_admin: boolean;
7 created_at: string;
8 updated_at: string;
9}
10
11export interface Image {
12 id: number;
13 filename: string;
14 storage_path: string;
15 thumbnail_path: string | null;
16 uploaded_by: number;
17 uploader_username: string;
18 avatar_url: string;
19 file_size: number;
20 width: number;
21 height: number;
22 mime_type: string;
23 innate_tags?: string[];
24 duration?: number;
25 gem_count?: number;
26 gemmed?: boolean;
27 created_at: string;
28 updated_at: string;
29 tags: string[];
30}
31
32export interface ImageList {
33 images: Image[];
34 total: number;
35}
36
37export interface Tag {
38 tag: string;
39 usage_count: number;
40}
41
42export interface TagDetail {
43 tag: string;
44 usage_count: number;
45}
46
47export interface ImageDetail extends Image {
48 tags_detail: TagDetail[];
49 is_owner: boolean;
50}
51
52export interface CurrentUser {
53 id: number;
54 username: string;
55 is_admin: boolean;
56 avatar_url: string;
57 created_at: string;
58 updated_at: string;
59}
60
61export interface ApiError {
62 error: string;
63}
64
65async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
66 const res = await fetch(BASE + path, {
67 credentials: 'include',
68 headers: opts.body instanceof FormData ? {} : { 'Content-Type': 'application/json' },
69 ...opts,
70 });
71 if (!res.ok) {
72 const body = await res.json().catch(() => ({ error: res.statusText }));
73 throw new Error((body as ApiError).error || `HTTP ${res.status}`);
74 }
75 return res.json();
76}
77
78export async function login(username: string, password: string): Promise<User> {
79 return request<User>('/auth/login', {
80 method: 'POST',
81 body: JSON.stringify({ username, password }),
82 });
83}
84
85export async function register(username: string, password: string): Promise<User> {
86 return request<User>('/auth/register', {
87 method: 'POST',
88 body: JSON.stringify({ username, password }),
89 });
90}
91
92export async function logout(): Promise<void> {
93 await request('/auth/logout', { method: 'POST' });
94}
95
96export async function listImages(tags: string[] = [], uploader = '', page = 1, perPage = 50, sortGems = false, filterGems = false): Promise<ImageList> {
97 const params = new URLSearchParams();
98 params.set('page', String(page));
99 params.set('per_page', String(perPage));
100 for (const t of tags) {
101 params.append('tags', t);
102 }
103 if (uploader) {
104 params.set('uploader', uploader);
105 }
106 if (sortGems) {
107 params.set('sort', 'gems');
108 }
109 if (filterGems) {
110 params.set('gems', 'true');
111 }
112 return request<ImageList>(`/images?${params}`);
113}
114
115export async function getImage(id: number): Promise<ImageDetail> {
116 return request<ImageDetail>(`/images/${id}`);
117}
118
119export function imageDownloadUrl(id: number): string {
120 return `${BASE}/images/${id}/download`;
121}
122
123export function thumbnailUrl(id: number): string {
124 return `${BASE}/images/${id}/thumbnail`;
125}
126
127export async function uploadImage(file: File, tags: string[]): Promise<Image> {
128 const fd = new FormData();
129 fd.append('file', file);
130 for (const t of tags) {
131 fd.append('tags', t);
132 }
133 return request<Image>('/upload', { method: 'POST', body: fd });
134}
135
136export async function suggestTags(query: string): Promise<Tag[]> {
137 return request<Tag[]>(`/tags/suggest?q=${encodeURIComponent(query)}`);
138}
139
140export async function suggestUploaders(query: string): Promise<Tag[]> {
141 return request<Tag[]>(`/users/suggest?q=${encodeURIComponent(query)}`);
142}
143
144export async function uploadAvatar(file: File): Promise<{ avatar_url: string }> {
145 const fd = new FormData();
146 fd.append('avatar', file);
147 return request<{ avatar_url: string }>('/users/me/avatar', { method: 'POST', body: fd });
148}
149
150export async function getMe(): Promise<CurrentUser> {
151 return request<CurrentUser>('/users/me');
152}
153
154export interface SiteConfig {
155 site_title: string;
156 favicon_url?: string;
157 logo_url?: string;
158}
159
160export async function getConfig(): Promise<SiteConfig> {
161 return request<SiteConfig>('/config');
162}
163
164export async function addTags(imageId: number, tags: string[]): Promise<{ tags_detail: TagDetail[] }> {
165 return request<{ tags_detail: TagDetail[] }>(`/images/${imageId}/tags`, {
166 method: 'POST',
167 body: JSON.stringify({ tags }),
168 });
169}
170
171export async function removeTags(imageId: number, tags: string[]): Promise<{ tags_detail: TagDetail[] }> {
172 return request<{ tags_detail: TagDetail[] }>(`/images/${imageId}/tags`, {
173 method: 'DELETE',
174 body: JSON.stringify({ tags }),
175 });
176}
177
178export async function toggleGem(imageId: number): Promise<{ gemmed: boolean; gem_count: number }> {
179 return request<{ gemmed: boolean; gem_count: number }>(`/images/${imageId}/gem`, {
180 method: 'POST',
181 });
182}