feat(api): refactored to api client to be per feature
This commit is contained in:
10
src/frontend/.idea/.gitignore
generated
vendored
Normal file
10
src/frontend/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
4
src/frontend/.idea/encodings.xml
generated
Normal file
4
src/frontend/.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
||||||
8
src/frontend/.idea/frontend.iml
generated
Normal file
8
src/frontend/.idea/frontend.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
src/frontend/.idea/modules.xml
generated
Normal file
8
src/frontend/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
src/frontend/.idea/vcs.xml
generated
Normal file
6
src/frontend/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
14
src/frontend/src/api/analytics.js
Normal file
14
src/frontend/src/api/analytics.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const analyticsApi = {
|
||||||
|
getWorkspaceAnalytics(workspaceId, period = '7d', startDate = null, endDate = null) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (startDate && endDate) {
|
||||||
|
params.set('startDate', startDate);
|
||||||
|
params.set('endDate', endDate);
|
||||||
|
} else {
|
||||||
|
params.set('period', period);
|
||||||
|
}
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/analytics?${params.toString()}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
15
src/frontend/src/api/apiKeys.js
Normal file
15
src/frontend/src/api/apiKeys.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const apiKeysApi = {
|
||||||
|
list(workspaceId) {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/api-keys`);
|
||||||
|
},
|
||||||
|
|
||||||
|
create(workspaceId, name, expiresAt = null, scopes = null) {
|
||||||
|
return baseClient.post(`/workspaces/${workspaceId}/api-keys`, { name, expiresAt, scopes });
|
||||||
|
},
|
||||||
|
|
||||||
|
delete(workspaceId, id) {
|
||||||
|
return baseClient.delete(`/workspaces/${workspaceId}/api-keys/${id}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
15
src/frontend/src/api/assets.js
Normal file
15
src/frontend/src/api/assets.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const assetsApi = {
|
||||||
|
list(workspaceId) {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/assets`);
|
||||||
|
},
|
||||||
|
|
||||||
|
upload(workspaceId, file) {
|
||||||
|
return baseClient.upload(`/workspaces/${workspaceId}/assets`, file);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete(workspaceId, id) {
|
||||||
|
return baseClient.delete(`/workspaces/${workspaceId}/assets/${id}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
47
src/frontend/src/api/auth.js
Normal file
47
src/frontend/src/api/auth.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const authApi = {
|
||||||
|
register(email, password) {
|
||||||
|
return baseClient.post('/auth/register', { email, password });
|
||||||
|
},
|
||||||
|
|
||||||
|
login(email, password) {
|
||||||
|
return baseClient.post('/auth/login', { email, password });
|
||||||
|
},
|
||||||
|
|
||||||
|
forgotPassword(email) {
|
||||||
|
return baseClient.post('/auth/forgot', { email });
|
||||||
|
},
|
||||||
|
|
||||||
|
resetPassword(token, newPassword) {
|
||||||
|
return baseClient.post('/auth/reset', { token, newPassword });
|
||||||
|
},
|
||||||
|
|
||||||
|
getProfile() {
|
||||||
|
return baseClient.get('/auth/profile');
|
||||||
|
},
|
||||||
|
|
||||||
|
updateProfile(data) {
|
||||||
|
return baseClient.put('/auth/profile', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
changePassword(currentPassword, newPassword) {
|
||||||
|
return baseClient.post('/auth/change-password', { currentPassword, newPassword });
|
||||||
|
},
|
||||||
|
|
||||||
|
resendVerification() {
|
||||||
|
return baseClient.post('/auth/resend-verification');
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyEmail(token) {
|
||||||
|
return baseClient.post('/auth/verify-email', { token });
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteAccount(password) {
|
||||||
|
return baseClient.delete('/auth/account', { password });
|
||||||
|
},
|
||||||
|
|
||||||
|
setToken(token) {
|
||||||
|
baseClient.setToken(token);
|
||||||
|
},
|
||||||
|
};
|
||||||
91
src/frontend/src/api/base.js
Normal file
91
src/frontend/src/api/base.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
const API_BASE = 'https://localhost:42001';
|
||||||
|
|
||||||
|
class BaseClient {
|
||||||
|
constructor() {
|
||||||
|
this.token = localStorage.getItem('token');
|
||||||
|
}
|
||||||
|
|
||||||
|
setToken(token) {
|
||||||
|
this.token = token;
|
||||||
|
if (token) {
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #request(method, path, body = null) {
|
||||||
|
const headers = {};
|
||||||
|
|
||||||
|
if (this.token) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = { method, headers };
|
||||||
|
if (body) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
options.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}${path}`, options);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
this.setToken(null);
|
||||||
|
window.location.href = '/login';
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
const data = text ? JSON.parse(text) : null;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data?.message || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(path) {
|
||||||
|
return this.#request('GET', path);
|
||||||
|
}
|
||||||
|
|
||||||
|
post(path, body = null) {
|
||||||
|
return this.#request('POST', path, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
put(path, body) {
|
||||||
|
return this.#request('PUT', path, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(path, body = null) {
|
||||||
|
return this.#request('DELETE', path, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload(path, file) {
|
||||||
|
const headers = {};
|
||||||
|
if (this.token) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}${path}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data?.message || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBaseUrl() {
|
||||||
|
return API_BASE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const baseClient = new BaseClient();
|
||||||
20
src/frontend/src/api/billing.js
Normal file
20
src/frontend/src/api/billing.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const billingApi = {
|
||||||
|
createCheckoutSession(workspaceId, plan, successUrl, cancelUrl) {
|
||||||
|
return baseClient.post('/billing/checkout', {
|
||||||
|
workspaceId,
|
||||||
|
plan,
|
||||||
|
successUrl,
|
||||||
|
cancelUrl,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createPortalSession(returnUrl) {
|
||||||
|
return baseClient.post('/billing/portal', { returnUrl });
|
||||||
|
},
|
||||||
|
|
||||||
|
getSubscription(workspaceId) {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/subscription`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
const API_BASE = 'https://localhost:42001';
|
|
||||||
|
|
||||||
class ApiClient {
|
|
||||||
constructor() {
|
|
||||||
this.token = localStorage.getItem('token');
|
|
||||||
}
|
|
||||||
|
|
||||||
setToken(token) {
|
|
||||||
this.token = token;
|
|
||||||
if (token) {
|
|
||||||
localStorage.setItem('token', token);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async request(method, path, body = null) {
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.token) {
|
|
||||||
headers['Authorization'] = `Bearer ${this.token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = { method, headers };
|
|
||||||
if (body) {
|
|
||||||
options.body = JSON.stringify(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}${path}`, options);
|
|
||||||
|
|
||||||
if (response.status === 401) {
|
|
||||||
this.setToken(null);
|
|
||||||
window.location.href = '/login';
|
|
||||||
throw new Error('Unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = await response.text();
|
|
||||||
const data = text ? JSON.parse(text) : null;
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data?.message || `HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async upload(path, file) {
|
|
||||||
const headers = {};
|
|
||||||
if (this.token) {
|
|
||||||
headers['Authorization'] = `Bearer ${this.token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}${path}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data?.message || `HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
register(email, password) {
|
|
||||||
return this.request('POST', '/auth/register', { email, password });
|
|
||||||
}
|
|
||||||
|
|
||||||
login(email, password) {
|
|
||||||
return this.request('POST', '/auth/login', { email, password });
|
|
||||||
}
|
|
||||||
|
|
||||||
forgotPassword(email) {
|
|
||||||
return this.request('POST', '/auth/forgot', { email });
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPassword(token, newPassword) {
|
|
||||||
return this.request('POST', '/auth/reset', { token, newPassword });
|
|
||||||
}
|
|
||||||
|
|
||||||
getProfile() {
|
|
||||||
return this.request('GET', '/auth/profile');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProfile(data) {
|
|
||||||
return this.request('PUT', '/auth/profile', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
changePassword(currentPassword, newPassword) {
|
|
||||||
return this.request('POST', '/auth/change-password', { currentPassword, newPassword });
|
|
||||||
}
|
|
||||||
|
|
||||||
resendVerification() {
|
|
||||||
return this.request('POST', '/auth/resend-verification');
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyEmail(token) {
|
|
||||||
return this.request('POST', '/auth/verify-email', { token });
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteAccount(password) {
|
|
||||||
return this.request('DELETE', '/auth/account', { password });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workspaces
|
|
||||||
listWorkspaces() {
|
|
||||||
return this.request('GET', '/workspaces');
|
|
||||||
}
|
|
||||||
|
|
||||||
createWorkspace(name) {
|
|
||||||
return this.request('POST', '/workspaces', { name });
|
|
||||||
}
|
|
||||||
|
|
||||||
getWorkspace(id) {
|
|
||||||
return this.request('GET', `/workspaces/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateWorkspace(id, name) {
|
|
||||||
return this.request('PUT', `/workspaces/${id}`, { name });
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteWorkspace(id) {
|
|
||||||
return this.request('DELETE', `/workspaces/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Projects
|
|
||||||
listProjects(workspaceId) {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/projects`);
|
|
||||||
}
|
|
||||||
|
|
||||||
createProject(workspaceId, name, description = '') {
|
|
||||||
return this.request('POST', `/workspaces/${workspaceId}/projects`, { name, description });
|
|
||||||
}
|
|
||||||
|
|
||||||
getProject(workspaceId, id) {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/projects/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProject(workspaceId, id, data) {
|
|
||||||
return this.request('PUT', `/workspaces/${workspaceId}/projects/${id}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteProject(workspaceId, id) {
|
|
||||||
return this.request('DELETE', `/workspaces/${workspaceId}/projects/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Links
|
|
||||||
listLinks(workspaceId, params = {}) {
|
|
||||||
const query = new URLSearchParams(params).toString();
|
|
||||||
const path = `/workspaces/${workspaceId}/links${query ? `?${query}` : ''}`;
|
|
||||||
return this.request('GET', path);
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreLink(workspaceId, id) {
|
|
||||||
return this.request('POST', `/workspaces/${workspaceId}/links/${id}/restore`);
|
|
||||||
}
|
|
||||||
|
|
||||||
createLink(workspaceId, data) {
|
|
||||||
return this.request('POST', `/workspaces/${workspaceId}/links`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
getLink(workspaceId, id) {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/links/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLink(workspaceId, id, data) {
|
|
||||||
return this.request('PUT', `/workspaces/${workspaceId}/links/${id}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteLink(workspaceId, id) {
|
|
||||||
return this.request('DELETE', `/workspaces/${workspaceId}/links/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
bulkCreateLinks(workspaceId, links) {
|
|
||||||
return this.request('POST', `/workspaces/${workspaceId}/links/bulk`, { links });
|
|
||||||
}
|
|
||||||
|
|
||||||
getLinkAnalytics(workspaceId, linkId, period = '7d', startDate = null, endDate = null) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (startDate && endDate) {
|
|
||||||
params.set('startDate', startDate);
|
|
||||||
params.set('endDate', endDate);
|
|
||||||
} else {
|
|
||||||
params.set('period', period);
|
|
||||||
}
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/links/${linkId}/analytics?${params.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// QR Codes
|
|
||||||
listQRCodes(workspaceId) {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/qrcodes`);
|
|
||||||
}
|
|
||||||
|
|
||||||
createQRCode(workspaceId, data) {
|
|
||||||
return this.request('POST', `/workspaces/${workspaceId}/qrcodes`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
getQRCode(workspaceId, id) {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/qrcodes/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateQRCode(workspaceId, id, data) {
|
|
||||||
return this.request('PUT', `/workspaces/${workspaceId}/qrcodes/${id}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteQRCode(workspaceId, id) {
|
|
||||||
return this.request('DELETE', `/workspaces/${workspaceId}/qrcodes/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getQRCodePreview(workspaceId, id) {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/qrcodes/${id}/preview`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getQRCodeExportUrl(workspaceId, id, format = 'png', size = 512) {
|
|
||||||
return `${API_BASE}/workspaces/${workspaceId}/qrcodes/${id}/export?format=${format}&size=${size}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getQRCodeAnalytics(workspaceId, qrCodeId, period = '7d') {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/qrcodes/${qrCodeId}/analytics?period=${period}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Analytics
|
|
||||||
getWorkspaceAnalytics(workspaceId, period = '7d', startDate = null, endDate = null) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (startDate && endDate) {
|
|
||||||
params.set('startDate', startDate);
|
|
||||||
params.set('endDate', endDate);
|
|
||||||
} else {
|
|
||||||
params.set('period', period);
|
|
||||||
}
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/analytics?${params.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Domains
|
|
||||||
listDomains(workspaceId) {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/domains`);
|
|
||||||
}
|
|
||||||
|
|
||||||
addDomain(workspaceId, hostname) {
|
|
||||||
return this.request('POST', `/workspaces/${workspaceId}/domains`, { hostname });
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteDomain(workspaceId, id) {
|
|
||||||
return this.request('DELETE', `/workspaces/${workspaceId}/domains/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyDomain(workspaceId, id) {
|
|
||||||
return this.request('POST', `/workspaces/${workspaceId}/domains/${id}/verify`, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assets
|
|
||||||
listAssets(workspaceId) {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/assets`);
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadAsset(workspaceId, file) {
|
|
||||||
return this.upload(`/workspaces/${workspaceId}/assets`, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteAsset(workspaceId, id) {
|
|
||||||
return this.request('DELETE', `/workspaces/${workspaceId}/assets/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Billing
|
|
||||||
createCheckoutSession(workspaceId, plan, successUrl, cancelUrl) {
|
|
||||||
return this.request('POST', '/billing/checkout', {
|
|
||||||
workspaceId,
|
|
||||||
plan,
|
|
||||||
successUrl,
|
|
||||||
cancelUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createPortalSession(returnUrl) {
|
|
||||||
return this.request('POST', '/billing/portal', { returnUrl });
|
|
||||||
}
|
|
||||||
|
|
||||||
getSubscription(workspaceId) {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/subscription`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
getUsage(workspaceId = null) {
|
|
||||||
const path = workspaceId ? `/usage?workspaceId=${workspaceId}` : '/usage';
|
|
||||||
return this.request('GET', path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Keys
|
|
||||||
listApiKeys(workspaceId) {
|
|
||||||
return this.request('GET', `/workspaces/${workspaceId}/api-keys`);
|
|
||||||
}
|
|
||||||
|
|
||||||
createApiKey(workspaceId, name, expiresAt = null, scopes = null) {
|
|
||||||
return this.request('POST', `/workspaces/${workspaceId}/api-keys`, { name, expiresAt, scopes });
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteApiKey(workspaceId, id) {
|
|
||||||
return this.request('DELETE', `/workspaces/${workspaceId}/api-keys/${id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const api = new ApiClient();
|
|
||||||
19
src/frontend/src/api/domains.js
Normal file
19
src/frontend/src/api/domains.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const domainsApi = {
|
||||||
|
list(workspaceId) {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/domains`);
|
||||||
|
},
|
||||||
|
|
||||||
|
add(workspaceId, hostname) {
|
||||||
|
return baseClient.post(`/workspaces/${workspaceId}/domains`, { hostname });
|
||||||
|
},
|
||||||
|
|
||||||
|
delete(workspaceId, id) {
|
||||||
|
return baseClient.delete(`/workspaces/${workspaceId}/domains/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
verify(workspaceId, id) {
|
||||||
|
return baseClient.post(`/workspaces/${workspaceId}/domains/${id}/verify`);
|
||||||
|
},
|
||||||
|
};
|
||||||
12
src/frontend/src/api/index.js
Normal file
12
src/frontend/src/api/index.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export { baseClient } from './base.js';
|
||||||
|
export { authApi } from './auth.js';
|
||||||
|
export { workspacesApi } from './workspaces.js';
|
||||||
|
export { projectsApi } from './projects.js';
|
||||||
|
export { linksApi } from './links.js';
|
||||||
|
export { qrcodesApi } from './qrcodes.js';
|
||||||
|
export { domainsApi } from './domains.js';
|
||||||
|
export { assetsApi } from './assets.js';
|
||||||
|
export { billingApi } from './billing.js';
|
||||||
|
export { analyticsApi } from './analytics.js';
|
||||||
|
export { apiKeysApi } from './apiKeys.js';
|
||||||
|
export { usageApi } from './usage.js';
|
||||||
44
src/frontend/src/api/links.js
Normal file
44
src/frontend/src/api/links.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const linksApi = {
|
||||||
|
list(workspaceId, params = {}) {
|
||||||
|
const query = new URLSearchParams(params).toString();
|
||||||
|
const path = `/workspaces/${workspaceId}/links${query ? `?${query}` : ''}`;
|
||||||
|
return baseClient.get(path);
|
||||||
|
},
|
||||||
|
|
||||||
|
create(workspaceId, data) {
|
||||||
|
return baseClient.post(`/workspaces/${workspaceId}/links`, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
get(workspaceId, id) {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/links/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
update(workspaceId, id, data) {
|
||||||
|
return baseClient.put(`/workspaces/${workspaceId}/links/${id}`, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete(workspaceId, id) {
|
||||||
|
return baseClient.delete(`/workspaces/${workspaceId}/links/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
restore(workspaceId, id) {
|
||||||
|
return baseClient.post(`/workspaces/${workspaceId}/links/${id}/restore`);
|
||||||
|
},
|
||||||
|
|
||||||
|
bulkCreate(workspaceId, links) {
|
||||||
|
return baseClient.post(`/workspaces/${workspaceId}/links/bulk`, { links });
|
||||||
|
},
|
||||||
|
|
||||||
|
getAnalytics(workspaceId, linkId, period = '7d', startDate = null, endDate = null) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (startDate && endDate) {
|
||||||
|
params.set('startDate', startDate);
|
||||||
|
params.set('endDate', endDate);
|
||||||
|
} else {
|
||||||
|
params.set('period', period);
|
||||||
|
}
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/links/${linkId}/analytics?${params.toString()}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
23
src/frontend/src/api/projects.js
Normal file
23
src/frontend/src/api/projects.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const projectsApi = {
|
||||||
|
list(workspaceId) {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/projects`);
|
||||||
|
},
|
||||||
|
|
||||||
|
create(workspaceId, name, description = '') {
|
||||||
|
return baseClient.post(`/workspaces/${workspaceId}/projects`, { name, description });
|
||||||
|
},
|
||||||
|
|
||||||
|
get(workspaceId, id) {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/projects/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
update(workspaceId, id, data) {
|
||||||
|
return baseClient.put(`/workspaces/${workspaceId}/projects/${id}`, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete(workspaceId, id) {
|
||||||
|
return baseClient.delete(`/workspaces/${workspaceId}/projects/${id}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
35
src/frontend/src/api/qrcodes.js
Normal file
35
src/frontend/src/api/qrcodes.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const qrcodesApi = {
|
||||||
|
list(workspaceId) {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/qrcodes`);
|
||||||
|
},
|
||||||
|
|
||||||
|
create(workspaceId, data) {
|
||||||
|
return baseClient.post(`/workspaces/${workspaceId}/qrcodes`, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
get(workspaceId, id) {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/qrcodes/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
update(workspaceId, id, data) {
|
||||||
|
return baseClient.put(`/workspaces/${workspaceId}/qrcodes/${id}`, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete(workspaceId, id) {
|
||||||
|
return baseClient.delete(`/workspaces/${workspaceId}/qrcodes/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
getPreview(workspaceId, id) {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/qrcodes/${id}/preview`);
|
||||||
|
},
|
||||||
|
|
||||||
|
getExportUrl(workspaceId, id, format = 'png', size = 512) {
|
||||||
|
return `${baseClient.getBaseUrl()}/workspaces/${workspaceId}/qrcodes/${id}/export?format=${format}&size=${size}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getAnalytics(workspaceId, qrCodeId, period = '7d') {
|
||||||
|
return baseClient.get(`/workspaces/${workspaceId}/qrcodes/${qrCodeId}/analytics?period=${period}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
8
src/frontend/src/api/usage.js
Normal file
8
src/frontend/src/api/usage.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const usageApi = {
|
||||||
|
get(workspaceId = null) {
|
||||||
|
const path = workspaceId ? `/usage?workspaceId=${workspaceId}` : '/usage';
|
||||||
|
return baseClient.get(path);
|
||||||
|
},
|
||||||
|
};
|
||||||
23
src/frontend/src/api/workspaces.js
Normal file
23
src/frontend/src/api/workspaces.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { baseClient } from './base.js';
|
||||||
|
|
||||||
|
export const workspacesApi = {
|
||||||
|
list() {
|
||||||
|
return baseClient.get('/workspaces');
|
||||||
|
},
|
||||||
|
|
||||||
|
create(name) {
|
||||||
|
return baseClient.post('/workspaces', { name });
|
||||||
|
},
|
||||||
|
|
||||||
|
get(id) {
|
||||||
|
return baseClient.get(`/workspaces/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
update(id, name) {
|
||||||
|
return baseClient.put(`/workspaces/${id}`, { name });
|
||||||
|
},
|
||||||
|
|
||||||
|
delete(id) {
|
||||||
|
return baseClient.delete(`/workspaces/${id}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
36
src/frontend/src/stores/analytics.js
Normal file
36
src/frontend/src/stores/analytics.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { analyticsApi } from '../api/analytics.js';
|
||||||
|
import { useWorkspaceStore } from './workspace.js';
|
||||||
|
|
||||||
|
export const useAnalyticsStore = defineStore('analytics', {
|
||||||
|
state: () => ({
|
||||||
|
analytics: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchAnalytics(period = '7d', startDate = null, endDate = null) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
this.analytics = await analyticsApi.getWorkspaceAnalytics(
|
||||||
|
workspaceStore.currentWorkspaceId,
|
||||||
|
period,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll() {
|
||||||
|
this.analytics = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
59
src/frontend/src/stores/assets.js
Normal file
59
src/frontend/src/stores/assets.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { assetsApi } from '../api/assets.js';
|
||||||
|
import { useWorkspaceStore } from './workspace.js';
|
||||||
|
|
||||||
|
export const useAssetsStore = defineStore('assets', {
|
||||||
|
state: () => ({
|
||||||
|
assets: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchAssets() {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await assetsApi.list(workspaceStore.currentWorkspaceId);
|
||||||
|
this.assets = response.assets || [];
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async uploadAsset(file) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const asset = await assetsApi.upload(workspaceStore.currentWorkspaceId, file);
|
||||||
|
this.assets.unshift(asset);
|
||||||
|
return asset;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteAsset(id) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assetsApi.delete(workspaceStore.currentWorkspaceId, id);
|
||||||
|
this.assets = this.assets.filter(a => a.id !== id);
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll() {
|
||||||
|
this.assets = [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { api } from '../api/client';
|
import { authApi } from '../api/auth.js';
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -19,12 +19,11 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
|
||||||
if (this.token) {
|
if (this.token) {
|
||||||
api.setToken(this.token);
|
authApi.setToken(this.token);
|
||||||
try {
|
try {
|
||||||
const profile = await api.getProfile();
|
const profile = await authApi.getProfile();
|
||||||
this.user = profile;
|
this.user = profile;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Token is invalid, clear it
|
|
||||||
this.logout();
|
this.logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,11 +34,11 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
try {
|
try {
|
||||||
const response = await api.register(email, password);
|
const response = await authApi.register(email, password);
|
||||||
this.token = response.token;
|
this.token = response.token;
|
||||||
this.user = { email: response.email, isVerified: false };
|
this.user = { email: response.email, isVerified: false };
|
||||||
localStorage.setItem('token', response.token);
|
localStorage.setItem('token', response.token);
|
||||||
api.setToken(response.token);
|
authApi.setToken(response.token);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = err.message;
|
this.error = err.message;
|
||||||
@@ -53,11 +52,11 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
try {
|
try {
|
||||||
const response = await api.login(email, password);
|
const response = await authApi.login(email, password);
|
||||||
this.token = response.token;
|
this.token = response.token;
|
||||||
this.user = { email: response.email };
|
this.user = { email: response.email };
|
||||||
localStorage.setItem('token', response.token);
|
localStorage.setItem('token', response.token);
|
||||||
api.setToken(response.token);
|
authApi.setToken(response.token);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = err.message;
|
this.error = err.message;
|
||||||
@@ -72,12 +71,12 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
this.user = null;
|
this.user = null;
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
localStorage.removeItem('currentWorkspaceId');
|
localStorage.removeItem('currentWorkspaceId');
|
||||||
api.setToken(null);
|
authApi.setToken(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchProfile() {
|
async fetchProfile() {
|
||||||
try {
|
try {
|
||||||
this.user = await api.getProfile();
|
this.user = await authApi.getProfile();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = err.message;
|
this.error = err.message;
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/frontend/src/stores/domains.js
Normal file
73
src/frontend/src/stores/domains.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { domainsApi } from '../api/domains.js';
|
||||||
|
import { useWorkspaceStore } from './workspace.js';
|
||||||
|
|
||||||
|
export const useDomainsStore = defineStore('domains', {
|
||||||
|
state: () => ({
|
||||||
|
domains: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchDomains() {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await domainsApi.list(workspaceStore.currentWorkspaceId);
|
||||||
|
this.domains = response.domains || [];
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async addDomain(hostname) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const domain = await domainsApi.add(workspaceStore.currentWorkspaceId, hostname);
|
||||||
|
this.domains.unshift(domain);
|
||||||
|
return domain;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async verifyDomain(id) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await domainsApi.verify(workspaceStore.currentWorkspaceId, id);
|
||||||
|
await this.fetchDomains();
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteDomain(id) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await domainsApi.delete(workspaceStore.currentWorkspaceId, id);
|
||||||
|
this.domains = this.domains.filter(d => d.id !== id);
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll() {
|
||||||
|
this.domains = [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
8
src/frontend/src/stores/index.js
Normal file
8
src/frontend/src/stores/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export { useAuthStore } from './auth.js';
|
||||||
|
export { useWorkspaceStore } from './workspace.js';
|
||||||
|
export { useProjectsStore } from './projects.js';
|
||||||
|
export { useLinksStore } from './links.js';
|
||||||
|
export { useQRCodesStore } from './qrcodes.js';
|
||||||
|
export { useDomainsStore } from './domains.js';
|
||||||
|
export { useAssetsStore } from './assets.js';
|
||||||
|
export { useAnalyticsStore } from './analytics.js';
|
||||||
119
src/frontend/src/stores/links.js
Normal file
119
src/frontend/src/stores/links.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { linksApi } from '../api/links.js';
|
||||||
|
import { useWorkspaceStore } from './workspace.js';
|
||||||
|
|
||||||
|
export const useLinksStore = defineStore('links', {
|
||||||
|
state: () => ({
|
||||||
|
links: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchLinks(params = {}) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await linksApi.list(workspaceStore.currentWorkspaceId, params);
|
||||||
|
this.links = response.links || [];
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createLink(data) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const link = await linksApi.create(workspaceStore.currentWorkspaceId, data);
|
||||||
|
this.links.unshift(link);
|
||||||
|
return link;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateLink(id, data) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const link = await linksApi.update(workspaceStore.currentWorkspaceId, id, data);
|
||||||
|
const index = this.links.findIndex(l => l.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.links[index] = link;
|
||||||
|
}
|
||||||
|
return link;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteLink(id) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await linksApi.delete(workspaceStore.currentWorkspaceId, id);
|
||||||
|
this.links = this.links.filter(l => l.id !== id);
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async restoreLink(id) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const link = await linksApi.restore(workspaceStore.currentWorkspaceId, id);
|
||||||
|
const index = this.links.findIndex(l => l.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.links[index] = link;
|
||||||
|
}
|
||||||
|
return link;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async bulkCreateLinks(links) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await linksApi.bulkCreate(workspaceStore.currentWorkspaceId, links);
|
||||||
|
await this.fetchLinks();
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getLinkAnalytics(linkId, period = '7d', startDate = null, endDate = null) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await linksApi.getAnalytics(workspaceStore.currentWorkspaceId, linkId, period, startDate, endDate);
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll() {
|
||||||
|
this.links = [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
76
src/frontend/src/stores/projects.js
Normal file
76
src/frontend/src/stores/projects.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { projectsApi } from '../api/projects.js';
|
||||||
|
import { useWorkspaceStore } from './workspace.js';
|
||||||
|
|
||||||
|
export const useProjectsStore = defineStore('projects', {
|
||||||
|
state: () => ({
|
||||||
|
projects: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchProjects() {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await projectsApi.list(workspaceStore.currentWorkspaceId);
|
||||||
|
this.projects = response.projects || [];
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createProject(name, description = '') {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const project = await projectsApi.create(workspaceStore.currentWorkspaceId, name, description);
|
||||||
|
this.projects.unshift(project);
|
||||||
|
return project;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateProject(id, data) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const project = await projectsApi.update(workspaceStore.currentWorkspaceId, id, data);
|
||||||
|
const index = this.projects.findIndex(p => p.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.projects[index] = project;
|
||||||
|
}
|
||||||
|
return project;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteProject(id) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await projectsApi.delete(workspaceStore.currentWorkspaceId, id);
|
||||||
|
this.projects = this.projects.filter(p => p.id !== id);
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll() {
|
||||||
|
this.projects = [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
107
src/frontend/src/stores/qrcodes.js
Normal file
107
src/frontend/src/stores/qrcodes.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { qrcodesApi } from '../api/qrcodes.js';
|
||||||
|
import { useWorkspaceStore } from './workspace.js';
|
||||||
|
|
||||||
|
export const useQRCodesStore = defineStore('qrcodes', {
|
||||||
|
state: () => ({
|
||||||
|
qrcodes: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchQRCodes() {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await qrcodesApi.list(workspaceStore.currentWorkspaceId);
|
||||||
|
this.qrcodes = response.qrCodes || [];
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createQRCode(data) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const qrcode = await qrcodesApi.create(workspaceStore.currentWorkspaceId, data);
|
||||||
|
this.qrcodes.unshift(qrcode);
|
||||||
|
return qrcode;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateQRCode(id, data) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const qrcode = await qrcodesApi.update(workspaceStore.currentWorkspaceId, id, data);
|
||||||
|
const index = this.qrcodes.findIndex(q => q.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.qrcodes[index] = qrcode;
|
||||||
|
}
|
||||||
|
return qrcode;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteQRCode(id) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await qrcodesApi.delete(workspaceStore.currentWorkspaceId, id);
|
||||||
|
this.qrcodes = this.qrcodes.filter(q => q.id !== id);
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getQRCodePreview(id) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await qrcodesApi.getPreview(workspaceStore.currentWorkspaceId, id);
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getQRCodeExportUrl(id, format = 'png', size = 512) {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return null;
|
||||||
|
|
||||||
|
return qrcodesApi.getExportUrl(workspaceStore.currentWorkspaceId, id, format, size);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getQRCodeAnalytics(qrCodeId, period = '7d') {
|
||||||
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
if (!workspaceStore.currentWorkspaceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await qrcodesApi.getAnalytics(workspaceStore.currentWorkspaceId, qrCodeId, period);
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll() {
|
||||||
|
this.qrcodes = [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { api } from '../api/client';
|
import { workspacesApi } from '../api/workspaces.js';
|
||||||
|
|
||||||
export const useWorkspaceStore = defineStore('workspace', {
|
export const useWorkspaceStore = defineStore('workspace', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
workspaces: [],
|
workspaces: [],
|
||||||
currentWorkspace: null,
|
currentWorkspace: null,
|
||||||
projects: [],
|
|
||||||
links: [],
|
|
||||||
qrcodes: [],
|
|
||||||
domains: [],
|
|
||||||
assets: [],
|
|
||||||
analytics: null,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -35,10 +29,9 @@ export const useWorkspaceStore = defineStore('workspace', {
|
|||||||
async fetchWorkspaces() {
|
async fetchWorkspaces() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await api.listWorkspaces();
|
const response = await workspacesApi.list();
|
||||||
this.workspaces = response.workspaces || [];
|
this.workspaces = response.workspaces || [];
|
||||||
|
|
||||||
// Restore saved workspace or use first one
|
|
||||||
const savedId = localStorage.getItem('currentWorkspaceId');
|
const savedId = localStorage.getItem('currentWorkspaceId');
|
||||||
const saved = savedId ? this.workspaces.find(w => w.id === savedId) : null;
|
const saved = savedId ? this.workspaces.find(w => w.id === savedId) : null;
|
||||||
|
|
||||||
@@ -61,18 +54,11 @@ export const useWorkspaceStore = defineStore('workspace', {
|
|||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('currentWorkspaceId');
|
localStorage.removeItem('currentWorkspaceId');
|
||||||
}
|
}
|
||||||
// Clear workspace-specific data
|
|
||||||
this.projects = [];
|
|
||||||
this.links = [];
|
|
||||||
this.qrcodes = [];
|
|
||||||
this.domains = [];
|
|
||||||
this.assets = [];
|
|
||||||
this.analytics = null;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async createWorkspace(name) {
|
async createWorkspace(name) {
|
||||||
try {
|
try {
|
||||||
const workspace = await api.createWorkspace(name);
|
const workspace = await workspacesApi.create(name);
|
||||||
this.workspaces.push(workspace);
|
this.workspaces.push(workspace);
|
||||||
return workspace;
|
return workspace;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -83,7 +69,7 @@ export const useWorkspaceStore = defineStore('workspace', {
|
|||||||
|
|
||||||
async updateWorkspace(id, name) {
|
async updateWorkspace(id, name) {
|
||||||
try {
|
try {
|
||||||
const updated = await api.updateWorkspace(id, name);
|
const updated = await workspacesApi.update(id, name);
|
||||||
const index = this.workspaces.findIndex(w => w.id === id);
|
const index = this.workspaces.findIndex(w => w.id === id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.workspaces[index] = updated;
|
this.workspaces[index] = updated;
|
||||||
@@ -100,7 +86,7 @@ export const useWorkspaceStore = defineStore('workspace', {
|
|||||||
|
|
||||||
async deleteWorkspace(id) {
|
async deleteWorkspace(id) {
|
||||||
try {
|
try {
|
||||||
await api.deleteWorkspace(id);
|
await workspacesApi.delete(id);
|
||||||
this.workspaces = this.workspaces.filter(w => w.id !== id);
|
this.workspaces = this.workspaces.filter(w => w.id !== id);
|
||||||
if (this.currentWorkspace?.id === id) {
|
if (this.currentWorkspace?.id === id) {
|
||||||
this.setCurrentWorkspace(this.workspaces[0] || null);
|
this.setCurrentWorkspace(this.workspaces[0] || null);
|
||||||
@@ -111,254 +97,9 @@ export const useWorkspaceStore = defineStore('workspace', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Projects
|
|
||||||
async fetchProjects() {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const response = await api.listProjects(this.currentWorkspaceId);
|
|
||||||
this.projects = response.projects || [];
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async createProject(name, description = '') {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const project = await api.createProject(this.currentWorkspaceId, name, description);
|
|
||||||
this.projects.unshift(project);
|
|
||||||
return project;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateProject(id, data) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const project = await api.updateProject(this.currentWorkspaceId, id, data);
|
|
||||||
const index = this.projects.findIndex(p => p.id === id);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.projects[index] = project;
|
|
||||||
}
|
|
||||||
return project;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteProject(id) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
await api.deleteProject(this.currentWorkspaceId, id);
|
|
||||||
this.projects = this.projects.filter(p => p.id !== id);
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Links
|
|
||||||
async fetchLinks(params = {}) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const response = await api.listLinks(this.currentWorkspaceId, params);
|
|
||||||
this.links = response.links || [];
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async createLink(data) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const link = await api.createLink(this.currentWorkspaceId, data);
|
|
||||||
this.links.unshift(link);
|
|
||||||
return link;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateLink(id, data) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const link = await api.updateLink(this.currentWorkspaceId, id, data);
|
|
||||||
const index = this.links.findIndex(l => l.id === id);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.links[index] = link;
|
|
||||||
}
|
|
||||||
return link;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteLink(id) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
await api.deleteLink(this.currentWorkspaceId, id);
|
|
||||||
this.links = this.links.filter(l => l.id !== id);
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// QR Codes
|
|
||||||
async fetchQRCodes() {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const response = await api.listQRCodes(this.currentWorkspaceId);
|
|
||||||
this.qrcodes = response.qrCodes || [];
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async createQRCode(data) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const qrcode = await api.createQRCode(this.currentWorkspaceId, data);
|
|
||||||
this.qrcodes.unshift(qrcode);
|
|
||||||
return qrcode;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateQRCode(id, data) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const qrcode = await api.updateQRCode(this.currentWorkspaceId, id, data);
|
|
||||||
const index = this.qrcodes.findIndex(q => q.id === id);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.qrcodes[index] = qrcode;
|
|
||||||
}
|
|
||||||
return qrcode;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteQRCode(id) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
await api.deleteQRCode(this.currentWorkspaceId, id);
|
|
||||||
this.qrcodes = this.qrcodes.filter(q => q.id !== id);
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Domains
|
|
||||||
async fetchDomains() {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const response = await api.listDomains(this.currentWorkspaceId);
|
|
||||||
this.domains = response.domains || [];
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async addDomain(hostname) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const domain = await api.addDomain(this.currentWorkspaceId, hostname);
|
|
||||||
this.domains.unshift(domain);
|
|
||||||
return domain;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async verifyDomain(id) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const result = await api.verifyDomain(this.currentWorkspaceId, id);
|
|
||||||
// Refresh domains to get updated status
|
|
||||||
await this.fetchDomains();
|
|
||||||
return result;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteDomain(id) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
await api.deleteDomain(this.currentWorkspaceId, id);
|
|
||||||
this.domains = this.domains.filter(d => d.id !== id);
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Assets
|
|
||||||
async fetchAssets() {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const response = await api.listAssets(this.currentWorkspaceId);
|
|
||||||
this.assets = response.assets || [];
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async uploadAsset(file) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
const asset = await api.uploadAsset(this.currentWorkspaceId, file);
|
|
||||||
this.assets.unshift(asset);
|
|
||||||
return asset;
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteAsset(id) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
await api.deleteAsset(this.currentWorkspaceId, id);
|
|
||||||
this.assets = this.assets.filter(a => a.id !== id);
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Analytics
|
|
||||||
async fetchAnalytics(period = '7d', startDate = null, endDate = null) {
|
|
||||||
if (!this.currentWorkspaceId) return;
|
|
||||||
try {
|
|
||||||
this.analytics = await api.getWorkspaceAnalytics(this.currentWorkspaceId, period, startDate, endDate);
|
|
||||||
} catch (err) {
|
|
||||||
this.error = err.message;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Clear all data (for logout)
|
|
||||||
clearAll() {
|
clearAll() {
|
||||||
this.workspaces = [];
|
this.workspaces = [];
|
||||||
this.currentWorkspace = null;
|
this.currentWorkspace = null;
|
||||||
this.projects = [];
|
|
||||||
this.links = [];
|
|
||||||
this.qrcodes = [];
|
|
||||||
this.domains = [];
|
|
||||||
this.assets = [];
|
|
||||||
this.analytics = null;
|
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
localStorage.removeItem('currentWorkspaceId');
|
localStorage.removeItem('currentWorkspaceId');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -200,9 +200,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
|
import { useAnalyticsStore } from '../../stores/analytics.js';
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore();
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
const analyticsStore = useAnalyticsStore();
|
||||||
|
|
||||||
const periods = [
|
const periods = [
|
||||||
{ label: '24h', value: '24h' },
|
{ label: '24h', value: '24h' },
|
||||||
@@ -214,12 +216,12 @@ const period = ref('7d');
|
|||||||
const isCustomRange = ref(false);
|
const isCustomRange = ref(false);
|
||||||
const startDate = ref('');
|
const startDate = ref('');
|
||||||
const endDate = ref('');
|
const endDate = ref('');
|
||||||
const analytics = computed(() => workspaceStore.analytics);
|
const analytics = computed(() => analyticsStore.analytics);
|
||||||
|
|
||||||
const setPeriod = async (p) => {
|
const setPeriod = async (p) => {
|
||||||
isCustomRange.value = false;
|
isCustomRange.value = false;
|
||||||
period.value = p;
|
period.value = p;
|
||||||
await workspaceStore.fetchAnalytics(p);
|
await analyticsStore.fetchAnalytics(p);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleCustomRange = () => {
|
const toggleCustomRange = () => {
|
||||||
@@ -235,7 +237,7 @@ const toggleCustomRange = () => {
|
|||||||
|
|
||||||
const applyCustomRange = async () => {
|
const applyCustomRange = async () => {
|
||||||
if (startDate.value && endDate.value) {
|
if (startDate.value && endDate.value) {
|
||||||
await workspaceStore.fetchAnalytics(null, startDate.value, endDate.value);
|
await analyticsStore.fetchAnalytics(null, startDate.value, endDate.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -267,12 +269,12 @@ const getReferrerPercentage = (value) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await workspaceStore.fetchAnalytics(period.value);
|
await analyticsStore.fetchAnalytics(period.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => workspaceStore.currentWorkspaceId, async () => {
|
watch(() => workspaceStore.currentWorkspaceId, async () => {
|
||||||
if (workspaceStore.currentWorkspaceId) {
|
if (workspaceStore.currentWorkspaceId) {
|
||||||
await workspaceStore.fetchAnalytics(period.value);
|
await analyticsStore.fetchAnalytics(period.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { api } from '../../api/client';
|
import { authApi } from '../../api/auth.js';
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -65,7 +65,7 @@ const handleSubmit = async () => {
|
|||||||
error.value = '';
|
error.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.forgotPassword(email.value);
|
await authApi.forgotPassword(email.value);
|
||||||
submitted.value = true;
|
submitted.value = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message;
|
error.value = err.message;
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { api } from '../../api/client';
|
import { authApi } from '../../api/auth.js';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ const handleSubmit = async () => {
|
|||||||
error.value = '';
|
error.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.resetPassword(token.value, password.value);
|
await authApi.resetPassword(token.value, password.value);
|
||||||
success.value = true;
|
success.value = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message;
|
error.value = err.message;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { api } from '../../api/client';
|
import { authApi } from '../../api/auth.js';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ onMounted(async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.verifyEmail(token.value);
|
await authApi.verifyEmail(token.value);
|
||||||
success.value = true;
|
success.value = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message || 'Unable to verify your email. The link may be invalid or expired.';
|
error.value = err.message || 'Unable to verify your email. The link may be invalid or expired.';
|
||||||
|
|||||||
@@ -205,8 +205,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { api } from '../../api/client';
|
import { billingApi } from '../../api/billing.js';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { usageApi } from '../../api/usage.js';
|
||||||
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -239,8 +240,8 @@ async function loadData() {
|
|||||||
const workspaceId = workspaceStore.currentWorkspace?.id;
|
const workspaceId = workspaceStore.currentWorkspace?.id;
|
||||||
|
|
||||||
const [usageData, subData] = await Promise.all([
|
const [usageData, subData] = await Promise.all([
|
||||||
api.getUsage(workspaceId),
|
usageApi.get(workspaceId),
|
||||||
workspaceId ? api.getSubscription(workspaceId) : Promise.resolve(null),
|
workspaceId ? billingApi.getSubscription(workspaceId) : Promise.resolve(null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
usage.value = usageData;
|
usage.value = usageData;
|
||||||
@@ -265,7 +266,7 @@ async function upgrade(plan) {
|
|||||||
const successUrl = window.location.origin + '/billing';
|
const successUrl = window.location.origin + '/billing';
|
||||||
const cancelUrl = window.location.origin + '/billing';
|
const cancelUrl = window.location.origin + '/billing';
|
||||||
|
|
||||||
const { url } = await api.createCheckoutSession(
|
const { url } = await billingApi.createCheckoutSession(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
plan,
|
plan,
|
||||||
successUrl,
|
successUrl,
|
||||||
@@ -285,7 +286,7 @@ async function openPortal() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const returnUrl = window.location.origin + '/billing';
|
const returnUrl = window.location.origin + '/billing';
|
||||||
const { url } = await api.createPortalSession(returnUrl);
|
const { url } = await billingApi.createPortalSession(returnUrl);
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message;
|
error.value = err.message;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<p class="stat-value">{{ workspaceStore.links.length }}</p>
|
<p class="stat-value">{{ linksStore.links.length }}</p>
|
||||||
<p class="stat-label">Active Links</p>
|
<p class="stat-label">Active Links</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,9 +173,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
|
import { useLinksStore } from '../../stores/links.js';
|
||||||
|
import { useAnalyticsStore } from '../../stores/analytics.js';
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore();
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
const linksStore = useLinksStore();
|
||||||
|
const analyticsStore = useAnalyticsStore();
|
||||||
|
|
||||||
const periods = [
|
const periods = [
|
||||||
{ label: '24h', value: '24h' },
|
{ label: '24h', value: '24h' },
|
||||||
@@ -184,11 +188,11 @@ const periods = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const period = ref('7d');
|
const period = ref('7d');
|
||||||
const analytics = computed(() => workspaceStore.analytics);
|
const analytics = computed(() => analyticsStore.analytics);
|
||||||
|
|
||||||
const setPeriod = async (p) => {
|
const setPeriod = async (p) => {
|
||||||
period.value = p;
|
period.value = p;
|
||||||
await workspaceStore.fetchAnalytics(p);
|
await analyticsStore.fetchAnalytics(p);
|
||||||
};
|
};
|
||||||
|
|
||||||
const maxEvents = computed(() => {
|
const maxEvents = computed(() => {
|
||||||
@@ -256,14 +260,14 @@ const getCountryName = (countryCode) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await workspaceStore.fetchLinks();
|
await linksStore.fetchLinks();
|
||||||
await workspaceStore.fetchAnalytics(period.value);
|
await analyticsStore.fetchAnalytics(period.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => workspaceStore.currentWorkspaceId, async () => {
|
watch(() => workspaceStore.currentWorkspaceId, async () => {
|
||||||
if (workspaceStore.currentWorkspaceId) {
|
if (workspaceStore.currentWorkspaceId) {
|
||||||
await workspaceStore.fetchLinks();
|
await linksStore.fetchLinks();
|
||||||
await workspaceStore.fetchAnalytics(period.value);
|
await analyticsStore.fetchAnalytics(period.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -206,12 +206,14 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
|
import { useDomainsStore } from '../../stores/domains.js';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore();
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
const domainsStore = useDomainsStore();
|
||||||
|
|
||||||
const domains = computed(() => workspaceStore.domains);
|
const domains = computed(() => domainsStore.domains);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const adding = ref(false);
|
const adding = ref(false);
|
||||||
const verifying = ref(false);
|
const verifying = ref(false);
|
||||||
@@ -242,7 +244,7 @@ watch(() => workspaceStore.currentWorkspaceId, async () => {
|
|||||||
async function loadDomains() {
|
async function loadDomains() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await workspaceStore.fetchDomains();
|
await domainsStore.fetchDomains();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message;
|
error.value = err.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -254,7 +256,7 @@ async function addDomain() {
|
|||||||
adding.value = true;
|
adding.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
const domain = await workspaceStore.addDomain(newDomain.value);
|
const domain = await domainsStore.addDomain(newDomain.value);
|
||||||
showAddModal.value = false;
|
showAddModal.value = false;
|
||||||
newDomain.value = '';
|
newDomain.value = '';
|
||||||
// Show verification instructions for the new domain
|
// Show verification instructions for the new domain
|
||||||
@@ -277,7 +279,7 @@ async function verifyDomain() {
|
|||||||
verifying.value = true;
|
verifying.value = true;
|
||||||
verifyError.value = '';
|
verifyError.value = '';
|
||||||
try {
|
try {
|
||||||
await workspaceStore.verifyDomain(verifyingDomain.value.id);
|
await domainsStore.verifyDomain(verifyingDomain.value.id);
|
||||||
showVerifyModal.value = false;
|
showVerifyModal.value = false;
|
||||||
verifyingDomain.value = null;
|
verifyingDomain.value = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -295,7 +297,7 @@ function confirmDelete(domain) {
|
|||||||
async function deleteDomain() {
|
async function deleteDomain() {
|
||||||
deleting.value = true;
|
deleting.value = true;
|
||||||
try {
|
try {
|
||||||
await workspaceStore.deleteDomain(domainToDelete.value.id);
|
await domainsStore.deleteDomain(domainToDelete.value.id);
|
||||||
showDeleteModal.value = false;
|
showDeleteModal.value = false;
|
||||||
domainToDelete.value = null;
|
domainToDelete.value = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -147,8 +147,8 @@
|
|||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
import { api } from '../../api/client';
|
import { linksApi } from '../../api/links.js';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const workspaceStore = useWorkspaceStore();
|
const workspaceStore = useWorkspaceStore();
|
||||||
@@ -170,7 +170,7 @@ const fetchData = async () => {
|
|||||||
if (!workspaceId) return;
|
if (!workspaceId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
link.value = await api.getLink(workspaceId, linkId);
|
link.value = await linksApi.get(workspaceId, linkId);
|
||||||
await fetchAnalytics();
|
await fetchAnalytics();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch link:', err);
|
console.error('Failed to fetch link:', err);
|
||||||
@@ -184,7 +184,7 @@ const fetchAnalytics = async () => {
|
|||||||
if (!workspaceId) return;
|
if (!workspaceId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
analytics.value = await api.getLinkAnalytics(workspaceId, linkId, period.value);
|
analytics.value = await linksApi.getAnalytics(workspaceId, linkId, period.value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch analytics:', err);
|
console.error('Failed to fetch analytics:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="links-list" v-if="workspaceStore.links.length">
|
<div class="links-list" v-if="linksStore.links.length">
|
||||||
<div
|
<div
|
||||||
v-for="link in workspaceStore.links"
|
v-for="link in linksStore.links"
|
||||||
:key="link.id"
|
:key="link.id"
|
||||||
class="link-card"
|
class="link-card"
|
||||||
>
|
>
|
||||||
@@ -311,10 +311,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
import { api } from '../../api/client';
|
import { useLinksStore } from '../../stores/links.js';
|
||||||
|
import { linksApi } from '../../api/links.js';
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore();
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
const linksStore = useLinksStore();
|
||||||
|
|
||||||
const showCreateModal = ref(false);
|
const showCreateModal = ref(false);
|
||||||
const showDeleteModal = ref(false);
|
const showDeleteModal = ref(false);
|
||||||
@@ -431,12 +433,12 @@ const saveLink = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (editingLink.value) {
|
if (editingLink.value) {
|
||||||
await workspaceStore.updateLink(editingLink.value.id, data);
|
await linksStore.updateLink(editingLink.value.id, data);
|
||||||
} else {
|
} else {
|
||||||
if (formData.value.slug) {
|
if (formData.value.slug) {
|
||||||
data.slug = formData.value.slug;
|
data.slug = formData.value.slug;
|
||||||
}
|
}
|
||||||
await workspaceStore.createLink(data);
|
await linksStore.createLink(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
@@ -454,7 +456,7 @@ const confirmDelete = (link) => {
|
|||||||
|
|
||||||
const deleteLink = async () => {
|
const deleteLink = async () => {
|
||||||
if (deletingLink.value) {
|
if (deletingLink.value) {
|
||||||
await workspaceStore.deleteLink(deletingLink.value.id);
|
await linksStore.deleteLink(deletingLink.value.id);
|
||||||
showDeleteModal.value = false;
|
showDeleteModal.value = false;
|
||||||
deletingLink.value = null;
|
deletingLink.value = null;
|
||||||
}
|
}
|
||||||
@@ -517,12 +519,8 @@ const importBulkLinks = async () => {
|
|||||||
bulkError.value = '';
|
bulkError.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workspaceId = workspaceStore.currentWorkspaceId;
|
const result = await linksStore.bulkCreateLinks(parsedBulkLinks.value);
|
||||||
const result = await api.bulkCreateLinks(workspaceId, parsedBulkLinks.value);
|
|
||||||
bulkResults.value = result;
|
bulkResults.value = result;
|
||||||
|
|
||||||
// Refresh links list
|
|
||||||
await workspaceStore.fetchLinks();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
bulkError.value = err.message;
|
bulkError.value = err.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -532,30 +530,30 @@ const importBulkLinks = async () => {
|
|||||||
|
|
||||||
const toggleDeleted = async () => {
|
const toggleDeleted = async () => {
|
||||||
showDeleted.value = true;
|
showDeleted.value = true;
|
||||||
await workspaceStore.fetchLinks({ includeDeleted: true });
|
await linksStore.fetchLinks({ includeDeleted: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreLink = async (link) => {
|
const restoreLink = async (link) => {
|
||||||
try {
|
try {
|
||||||
await api.restoreLink(workspaceStore.currentWorkspaceId, link.id);
|
await linksStore.restoreLink(link.id);
|
||||||
await workspaceStore.fetchLinks({ includeDeleted: showDeleted.value });
|
await linksStore.fetchLinks({ includeDeleted: showDeleted.value });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to restore link:', err);
|
console.error('Failed to restore link:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await workspaceStore.fetchLinks();
|
await linksStore.fetchLinks();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => workspaceStore.currentWorkspaceId, async () => {
|
watch(() => workspaceStore.currentWorkspaceId, async () => {
|
||||||
if (workspaceStore.currentWorkspaceId) {
|
if (workspaceStore.currentWorkspaceId) {
|
||||||
await workspaceStore.fetchLinks({ includeDeleted: showDeleted.value });
|
await linksStore.fetchLinks({ includeDeleted: showDeleted.value });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(showDeleted, async (value) => {
|
watch(showDeleted, async (value) => {
|
||||||
await workspaceStore.fetchLinks({ includeDeleted: value });
|
await linksStore.fetchLinks({ includeDeleted: value });
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -142,12 +142,14 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
|
import { useProjectsStore } from '../../stores/projects.js';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore();
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
const projectsStore = useProjectsStore();
|
||||||
|
|
||||||
const projects = computed(() => workspaceStore.projects);
|
const projects = computed(() => projectsStore.projects);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const deleting = ref(false);
|
const deleting = ref(false);
|
||||||
@@ -174,7 +176,7 @@ watch(() => workspaceStore.currentWorkspaceId, async () => {
|
|||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await workspaceStore.fetchProjects();
|
await projectsStore.fetchProjects();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message;
|
error.value = err.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -186,7 +188,7 @@ async function createProject() {
|
|||||||
saving.value = true;
|
saving.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
await workspaceStore.createProject(form.value.name, form.value.description);
|
await projectsStore.createProject(form.value.name, form.value.description);
|
||||||
closeModals();
|
closeModals();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message;
|
error.value = err.message;
|
||||||
@@ -205,7 +207,7 @@ async function updateProject() {
|
|||||||
saving.value = true;
|
saving.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
await workspaceStore.updateProject(editingProject.value.id, {
|
await projectsStore.updateProject(editingProject.value.id, {
|
||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
description: form.value.description
|
description: form.value.description
|
||||||
});
|
});
|
||||||
@@ -225,7 +227,7 @@ function confirmDelete(project) {
|
|||||||
async function deleteProject() {
|
async function deleteProject() {
|
||||||
deleting.value = true;
|
deleting.value = true;
|
||||||
try {
|
try {
|
||||||
await workspaceStore.deleteProject(projectToDelete.value.id);
|
await projectsStore.deleteProject(projectToDelete.value.id);
|
||||||
showDeleteModal.value = false;
|
showDeleteModal.value = false;
|
||||||
projectToDelete.value = null;
|
projectToDelete.value = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
<label for="linkId">Link *</label>
|
<label for="linkId">Link *</label>
|
||||||
<select id="linkId" v-model="formData.linkId" required>
|
<select id="linkId" v-model="formData.linkId" required>
|
||||||
<option value="">Select a link</option>
|
<option value="">Select a link</option>
|
||||||
<option v-for="link in workspaceStore.links" :key="link.id" :value="link.id">
|
<option v-for="link in linksStore.links" :key="link.id" :value="link.id">
|
||||||
{{ link.title || link.slug }} ({{ link.slug }})
|
{{ link.title || link.slug }} ({{ link.slug }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -254,12 +254,18 @@
|
|||||||
import { ref, watch, onMounted, computed } from 'vue';
|
import { ref, watch, onMounted, computed } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
import { api } from '../../api/client';
|
import { useLinksStore } from '../../stores/links.js';
|
||||||
|
import { useAssetsStore } from '../../stores/assets.js';
|
||||||
|
import { useQRCodesStore } from '../../stores/qrcodes.js';
|
||||||
|
import { qrcodesApi } from '../../api/qrcodes.js';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workspaceStore = useWorkspaceStore();
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
const linksStore = useLinksStore();
|
||||||
|
const assetsStore = useAssetsStore();
|
||||||
|
const qrcodesStore = useQRCodesStore();
|
||||||
|
|
||||||
const isEditing = computed(() => !!route.params.id);
|
const isEditing = computed(() => !!route.params.id);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@@ -267,7 +273,7 @@ const error = ref('');
|
|||||||
const previewUrl = ref('');
|
const previewUrl = ref('');
|
||||||
const previewTimeout = ref(null);
|
const previewTimeout = ref(null);
|
||||||
const uploadingLogo = ref(false);
|
const uploadingLogo = ref(false);
|
||||||
const assets = computed(() => workspaceStore.assets.filter(a => a.type === 'Logo'));
|
const assets = computed(() => assetsStore.assets.filter(a => a.type === 'Logo'));
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -296,7 +302,7 @@ const eyeShapes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const getLogoUrl = (assetId) => {
|
const getLogoUrl = (assetId) => {
|
||||||
const asset = workspaceStore.assets.find(a => a.id === assetId);
|
const asset = assetsStore.assets.find(a => a.id === assetId);
|
||||||
return asset?.url || '';
|
return asset?.url || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -306,7 +312,7 @@ const handleLogoUpload = async (event) => {
|
|||||||
|
|
||||||
uploadingLogo.value = true;
|
uploadingLogo.value = true;
|
||||||
try {
|
try {
|
||||||
const asset = await workspaceStore.uploadAsset(file);
|
const asset = await assetsStore.uploadAsset(file);
|
||||||
formData.value.logoAssetId = asset.id;
|
formData.value.logoAssetId = asset.id;
|
||||||
// If adding a logo, suggest high error correction
|
// If adding a logo, suggest high error correction
|
||||||
if (formData.value.style.errorCorrectionLevel !== 'H') {
|
if (formData.value.style.errorCorrectionLevel !== 'H') {
|
||||||
@@ -373,7 +379,7 @@ const fetchPreview = async () => {
|
|||||||
// For now, we'll just show a placeholder until saved
|
// For now, we'll just show a placeholder until saved
|
||||||
if (isEditing.value) {
|
if (isEditing.value) {
|
||||||
try {
|
try {
|
||||||
const preview = await api.getQRCodePreview(workspaceStore.currentWorkspaceId, route.params.id);
|
const preview = await qrcodesStore.getQRCodePreview(route.params.id);
|
||||||
previewUrl.value = preview.dataUrl;
|
previewUrl.value = preview.dataUrl;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Preview error:', err);
|
console.error('Preview error:', err);
|
||||||
@@ -400,9 +406,9 @@ const save = async () => {
|
|||||||
if (!formData.value.logoAssetId) {
|
if (!formData.value.logoAssetId) {
|
||||||
data.removeLogo = true;
|
data.removeLogo = true;
|
||||||
}
|
}
|
||||||
await workspaceStore.updateQRCode(route.params.id, data);
|
await qrcodesStore.updateQRCode(route.params.id, data);
|
||||||
} else {
|
} else {
|
||||||
await workspaceStore.createQRCode(data);
|
await qrcodesStore.createQRCode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push('/qrcodes');
|
router.push('/qrcodes');
|
||||||
@@ -416,7 +422,7 @@ const save = async () => {
|
|||||||
const downloadQR = async (format) => {
|
const downloadQR = async (format) => {
|
||||||
if (!isEditing.value) return;
|
if (!isEditing.value) return;
|
||||||
|
|
||||||
const url = api.getQRCodeExportUrl(workspaceStore.currentWorkspaceId, route.params.id, format, 512);
|
const url = qrcodesStore.getQRCodeExportUrl(route.params.id, format, 512);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `${formData.value.name}.${format}`;
|
link.download = `${formData.value.name}.${format}`;
|
||||||
@@ -427,7 +433,7 @@ const loadExisting = async () => {
|
|||||||
if (!isEditing.value) return;
|
if (!isEditing.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const qr = await api.getQRCode(workspaceStore.currentWorkspaceId, route.params.id);
|
const qr = await qrcodesApi.get(workspaceStore.currentWorkspaceId, route.params.id);
|
||||||
const defaultStyle = {
|
const defaultStyle = {
|
||||||
foregroundColor: '#000000',
|
foregroundColor: '#000000',
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
@@ -460,8 +466,8 @@ watch(() => formData.value.logoAssetId, () => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
workspaceStore.fetchLinks(),
|
linksStore.fetchLinks(),
|
||||||
workspaceStore.fetchAssets(),
|
assetsStore.fetchAssets(),
|
||||||
]);
|
]);
|
||||||
if (isEditing.value) {
|
if (isEditing.value) {
|
||||||
await loadExisting();
|
await loadExisting();
|
||||||
|
|||||||
@@ -168,8 +168,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { api } from '../../api/client';
|
import { qrcodesApi } from '../../api/qrcodes.js';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -208,8 +208,8 @@ async function loadData() {
|
|||||||
if (!workspaceId) return;
|
if (!workspaceId) return;
|
||||||
|
|
||||||
const [qrData, analyticsData] = await Promise.all([
|
const [qrData, analyticsData] = await Promise.all([
|
||||||
api.getQRCode(workspaceId, id.value),
|
qrcodesApi.get(workspaceId, id.value),
|
||||||
api.getQRCodeAnalytics(workspaceId, id.value, period.value)
|
qrcodesApi.getAnalytics(workspaceId, id.value, period.value)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
qrCode.value = qrData;
|
qrCode.value = qrData;
|
||||||
@@ -226,7 +226,7 @@ async function setPeriod(newPeriod) {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const workspaceId = workspaceStore.currentWorkspace?.id;
|
const workspaceId = workspaceStore.currentWorkspace?.id;
|
||||||
analytics.value = await api.getQRCodeAnalytics(workspaceId, id.value, newPeriod);
|
analytics.value = await qrcodesApi.getAnalytics(workspaceId, id.value, newPeriod);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message;
|
error.value = err.message;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="qrcodes-grid" v-if="workspaceStore.qrcodes.length">
|
<div class="qrcodes-grid" v-if="qrcodesStore.qrcodes.length">
|
||||||
<div
|
<div
|
||||||
v-for="qr in workspaceStore.qrcodes"
|
v-for="qr in qrcodesStore.qrcodes"
|
||||||
:key="qr.id"
|
:key="qr.id"
|
||||||
class="qr-card"
|
class="qr-card"
|
||||||
>
|
>
|
||||||
@@ -98,19 +98,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue';
|
import { ref, onMounted, watch } from 'vue';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
import { api } from '../../api/client';
|
import { useQRCodesStore } from '../../stores/qrcodes.js';
|
||||||
|
import { useLinksStore } from '../../stores/links.js';
|
||||||
|
import { qrcodesApi } from '../../api/qrcodes.js';
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore();
|
const workspaceStore = useWorkspaceStore();
|
||||||
|
const qrcodesStore = useQRCodesStore();
|
||||||
|
const linksStore = useLinksStore();
|
||||||
|
|
||||||
const previews = ref({});
|
const previews = ref({});
|
||||||
const showDeleteModal = ref(false);
|
const showDeleteModal = ref(false);
|
||||||
const deletingQR = ref(null);
|
const deletingQR = ref(null);
|
||||||
|
|
||||||
const fetchPreviews = async () => {
|
const fetchPreviews = async () => {
|
||||||
for (const qr of workspaceStore.qrcodes) {
|
for (const qr of qrcodesStore.qrcodes) {
|
||||||
try {
|
try {
|
||||||
const preview = await api.getQRCodePreview(workspaceStore.currentWorkspaceId, qr.id);
|
const preview = await qrcodesStore.getQRCodePreview(qr.id);
|
||||||
previews.value[qr.id] = preview.dataUrl;
|
previews.value[qr.id] = preview.dataUrl;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch preview:', err);
|
console.error('Failed to fetch preview:', err);
|
||||||
@@ -119,12 +123,12 @@ const fetchPreviews = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getLinkSlug = (linkId) => {
|
const getLinkSlug = (linkId) => {
|
||||||
const link = workspaceStore.links.find(l => l.id === linkId);
|
const link = linksStore.links.find(l => l.id === linkId);
|
||||||
return link ? `/${link.slug}` : 'No link';
|
return link ? `/${link.slug}` : 'No link';
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadQR = async (qr, format) => {
|
const downloadQR = async (qr, format) => {
|
||||||
const url = api.getQRCodeExportUrl(workspaceStore.currentWorkspaceId, qr.id, format, 512);
|
const url = qrcodesStore.getQRCodeExportUrl(qr.id, format, 512);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `${qr.name}.${format}`;
|
link.download = `${qr.name}.${format}`;
|
||||||
@@ -138,7 +142,7 @@ const confirmDelete = (qr) => {
|
|||||||
|
|
||||||
const deleteQR = async () => {
|
const deleteQR = async () => {
|
||||||
if (deletingQR.value) {
|
if (deletingQR.value) {
|
||||||
await workspaceStore.deleteQRCode(deletingQR.value.id);
|
await qrcodesStore.deleteQRCode(deletingQR.value.id);
|
||||||
delete previews.value[deletingQR.value.id];
|
delete previews.value[deletingQR.value.id];
|
||||||
showDeleteModal.value = false;
|
showDeleteModal.value = false;
|
||||||
deletingQR.value = null;
|
deletingQR.value = null;
|
||||||
@@ -146,16 +150,16 @@ const deleteQR = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await workspaceStore.fetchQRCodes();
|
await qrcodesStore.fetchQRCodes();
|
||||||
await workspaceStore.fetchLinks();
|
await linksStore.fetchLinks();
|
||||||
await fetchPreviews();
|
await fetchPreviews();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => workspaceStore.currentWorkspaceId, async () => {
|
watch(() => workspaceStore.currentWorkspaceId, async () => {
|
||||||
if (workspaceStore.currentWorkspaceId) {
|
if (workspaceStore.currentWorkspaceId) {
|
||||||
previews.value = {};
|
previews.value = {};
|
||||||
await workspaceStore.fetchQRCodes();
|
await qrcodesStore.fetchQRCodes();
|
||||||
await workspaceStore.fetchLinks();
|
await linksStore.fetchLinks();
|
||||||
await fetchPreviews();
|
await fetchPreviews();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -277,9 +277,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue';
|
import { ref, onMounted, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { api } from '../../api/client';
|
import { authApi } from '../../api/auth.js';
|
||||||
import { useAuthStore } from '../../stores/auth';
|
import { apiKeysApi } from '../../api/apiKeys.js';
|
||||||
import { useWorkspaceStore } from '../../stores/workspace';
|
import { useAuthStore } from '../../stores/auth.js';
|
||||||
|
import { useWorkspaceStore } from '../../stores/workspace.js';
|
||||||
import AppLayout from '../../components/layout/AppLayout.vue';
|
import AppLayout from '../../components/layout/AppLayout.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -323,7 +324,7 @@ const deleteKeyError = ref('');
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await api.getProfile();
|
const data = await authApi.getProfile();
|
||||||
profile.value = data;
|
profile.value = data;
|
||||||
await loadApiKeys();
|
await loadApiKeys();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -345,7 +346,7 @@ async function loadApiKeys() {
|
|||||||
|
|
||||||
loadingKeys.value = true;
|
loadingKeys.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await api.listApiKeys(workspaceId);
|
const result = await apiKeysApi.list(workspaceId);
|
||||||
apiKeys.value = result.apiKeys;
|
apiKeys.value = result.apiKeys;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load API keys:', err);
|
console.error('Failed to load API keys:', err);
|
||||||
@@ -360,7 +361,7 @@ async function updateProfile() {
|
|||||||
profileSuccess.value = '';
|
profileSuccess.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.updateProfile({ email: profile.value.email });
|
const data = await authApi.updateProfile({ email: profile.value.email });
|
||||||
profile.value = data;
|
profile.value = data;
|
||||||
profileSuccess.value = 'Profile updated successfully';
|
profileSuccess.value = 'Profile updated successfully';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -381,7 +382,7 @@ async function changePassword() {
|
|||||||
passwordSuccess.value = '';
|
passwordSuccess.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.changePassword(
|
await authApi.changePassword(
|
||||||
passwordForm.value.currentPassword,
|
passwordForm.value.currentPassword,
|
||||||
passwordForm.value.newPassword
|
passwordForm.value.newPassword
|
||||||
);
|
);
|
||||||
@@ -400,7 +401,7 @@ async function resendVerification() {
|
|||||||
resendingVerification.value = true;
|
resendingVerification.value = true;
|
||||||
profileError.value = '';
|
profileError.value = '';
|
||||||
try {
|
try {
|
||||||
await api.resendVerification();
|
await authApi.resendVerification();
|
||||||
profileSuccess.value = 'Verification email sent! Check your inbox.';
|
profileSuccess.value = 'Verification email sent! Check your inbox.';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
profileError.value = err.message;
|
profileError.value = err.message;
|
||||||
@@ -414,7 +415,7 @@ async function deleteAccount() {
|
|||||||
deleteError.value = '';
|
deleteError.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.deleteAccount(deletePassword.value);
|
await authApi.deleteAccount(deletePassword.value);
|
||||||
authStore.logout();
|
authStore.logout();
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -440,7 +441,7 @@ async function createApiKey() {
|
|||||||
expiresAt = date.toISOString();
|
expiresAt = date.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await api.createApiKey(workspaceId, newKeyName.value, expiresAt);
|
const result = await apiKeysApi.create(workspaceId, newKeyName.value, expiresAt);
|
||||||
newlyCreatedKey.value = result.key;
|
newlyCreatedKey.value = result.key;
|
||||||
apiKeys.value.unshift({
|
apiKeys.value.unshift({
|
||||||
id: result.id,
|
id: result.id,
|
||||||
@@ -487,7 +488,7 @@ async function deleteApiKey() {
|
|||||||
deleteKeyError.value = '';
|
deleteKeyError.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.deleteApiKey(workspaceId, keyToDelete.value.id);
|
await apiKeysApi.delete(workspaceId, keyToDelete.value.id);
|
||||||
apiKeys.value = apiKeys.value.filter(k => k.id !== keyToDelete.value.id);
|
apiKeys.value = apiKeys.value.filter(k => k.id !== keyToDelete.value.id);
|
||||||
keyToDelete.value = null;
|
keyToDelete.value = null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user