feat: add editable channel images
This commit is contained in:
192
frontend/src/api/schema.d.ts
vendored
192
frontend/src/api/schema.d.ts
vendored
@@ -1156,6 +1156,54 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/channels/{id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put: operations["SocializeApiModulesChannelsHandlersUpdateChannelHandler"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/channels/{id}/portrait": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["SocializeApiModulesChannelsHandlersUploadChannelPortraitHandler"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/channels/{id}/banner": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["SocializeApiModulesChannelsHandlersUploadChannelBannerHandler"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/campaigns": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -2129,6 +2177,8 @@ export interface components {
|
||||
network?: string;
|
||||
handle?: string | null;
|
||||
externalUrl?: string | null;
|
||||
portraitUrl?: string | null;
|
||||
bannerUrl?: string | null;
|
||||
/** Format: date-time */
|
||||
createdAt?: string;
|
||||
};
|
||||
@@ -2139,8 +2189,24 @@ export interface components {
|
||||
network: string;
|
||||
handle?: string | null;
|
||||
externalUrl?: string | null;
|
||||
portraitUrl?: string | null;
|
||||
bannerUrl?: string | null;
|
||||
};
|
||||
SocializeApiModulesChannelsHandlersGetChannelsRequest: Record<string, never>;
|
||||
SocializeApiModulesChannelsHandlersUpdateChannelRequest: {
|
||||
/** Format: guid */
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
network: string;
|
||||
handle?: string | null;
|
||||
externalUrl?: string | null;
|
||||
portraitUrl?: string | null;
|
||||
bannerUrl?: string | null;
|
||||
};
|
||||
SocializeApiModulesChannelsHandlersUploadChannelImageRequest: {
|
||||
/** Format: binary */
|
||||
file: string;
|
||||
};
|
||||
SocializeApiModulesCampaignsHandlersCampaignDto: {
|
||||
/** Format: guid */
|
||||
id?: string;
|
||||
@@ -5269,6 +5335,132 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesChannelsHandlersUpdateChannelHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesChannelsHandlersUpdateChannelRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesChannelsHandlersChannelDto"];
|
||||
};
|
||||
};
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["FastEndpointsErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesChannelsHandlersUploadChannelPortraitHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"multipart/form-data": components["schemas"]["SocializeApiModulesChannelsHandlersUploadChannelImageRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesChannelsHandlersChannelDto"];
|
||||
};
|
||||
};
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["FastEndpointsErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesChannelsHandlersUploadChannelBannerHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"multipart/form-data": components["schemas"]["SocializeApiModulesChannelsHandlersUploadChannelImageRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesChannelsHandlersChannelDto"];
|
||||
};
|
||||
};
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["FastEndpointsErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesCampaignsHandlersGetCampaignsHandler: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export const useChannelsStore = defineStore('channels', () => {
|
||||
const channels = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const isUpdating = ref(false);
|
||||
const error = ref(null);
|
||||
const loadedWorkspaceId = ref(null);
|
||||
const allWorkspacesKey = '__all__';
|
||||
@@ -104,6 +105,71 @@ export const useChannelsStore = defineStore('channels', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateChannel(channelId, payload) {
|
||||
if (!authStore.isAuthenticated) {
|
||||
throw new Error('You must be signed in to update a channel.');
|
||||
}
|
||||
|
||||
if (isUpdating.value) {
|
||||
throw new Error('A channel update request is already in progress.');
|
||||
}
|
||||
|
||||
isUpdating.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.put(`/api/channels/${channelId}`, payload);
|
||||
if (response.data) {
|
||||
channels.value = channels.value
|
||||
.map(channel => channel.id === response.data.id ? response.data : channel)
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (updateError) {
|
||||
console.error('Failed to update channel:', updateError);
|
||||
const message = updateError.response?.data?.errors?.[0]?.reason
|
||||
?? updateError.response?.data?.message
|
||||
?? 'Failed to update channel.';
|
||||
error.value = message;
|
||||
throw new Error(message);
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadChannelImage(channelId, imageKind, file) {
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
isUpdating.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name || `${imageKind}.jpg`);
|
||||
|
||||
const response = await client.post(`/api/channels/${channelId}/${imageKind}`, formData);
|
||||
if (response.data) {
|
||||
channels.value = channels.value
|
||||
.map(channel => channel.id === response.data.id ? response.data : channel)
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (uploadError) {
|
||||
console.error('Failed to upload channel image:', uploadError);
|
||||
const message = uploadError.response?.data?.errors?.[0]?.reason
|
||||
?? uploadError.response?.data?.message
|
||||
?? 'Failed to upload channel image.';
|
||||
error.value = message;
|
||||
throw new Error(message);
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [authStore.isAuthenticated, workspaceStore.workspaceScopeKey],
|
||||
async ([isAuthenticated]) => {
|
||||
@@ -124,8 +190,11 @@ export const useChannelsStore = defineStore('channels', () => {
|
||||
channels,
|
||||
isLoading,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
error,
|
||||
fetchChannels,
|
||||
createChannel,
|
||||
updateChannel,
|
||||
uploadChannelImage,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
import {
|
||||
mdiClose,
|
||||
mdiFacebook,
|
||||
mdiImage,
|
||||
mdiInstagram,
|
||||
mdiLinkedin,
|
||||
mdiMusicNote,
|
||||
mdiOpenInNew,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiReddit,
|
||||
mdiContentSave,
|
||||
mdiWeb,
|
||||
mdiYoutube,
|
||||
} from '@mdi/js';
|
||||
@@ -25,6 +28,7 @@
|
||||
const channelsStore = useChannelsStore();
|
||||
|
||||
const isCreateFormVisible = ref(false);
|
||||
const editingChannelId = ref('');
|
||||
const formError = ref(null);
|
||||
const activeNetwork = ref('Instagram');
|
||||
const form = reactive({
|
||||
@@ -32,6 +36,8 @@
|
||||
network: 'Instagram',
|
||||
handle: '',
|
||||
externalUrl: '',
|
||||
portraitUrl: '',
|
||||
bannerUrl: '',
|
||||
});
|
||||
|
||||
const networkOptions = [
|
||||
@@ -63,11 +69,16 @@
|
||||
const channelsForActiveNetwork = computed(() =>
|
||||
configuredChannels.value.filter(channel => channel.network === activeNetwork.value)
|
||||
);
|
||||
const editingChannel = computed(() =>
|
||||
configuredChannels.value.find(channel => channel.id === editingChannelId.value) ?? null
|
||||
);
|
||||
const previewChannel = computed(() => ({
|
||||
name: form.name.trim() || `${form.network} channel`,
|
||||
network: form.network,
|
||||
handle: form.handle.trim(),
|
||||
externalUrl: form.externalUrl.trim(),
|
||||
portraitUrl: form.portraitUrl.trim(),
|
||||
bannerUrl: form.bannerUrl.trim(),
|
||||
workspaceName: workspaceStore.activeWorkspace?.name ?? t('nav.noWorkspace'),
|
||||
scheduled: 0,
|
||||
readyCount: 0,
|
||||
@@ -95,6 +106,8 @@
|
||||
form.network = activeNetwork.value;
|
||||
form.handle = '';
|
||||
form.externalUrl = '';
|
||||
form.portraitUrl = '';
|
||||
form.bannerUrl = '';
|
||||
formError.value = null;
|
||||
}
|
||||
|
||||
@@ -102,6 +115,20 @@
|
||||
activeNetwork.value = network;
|
||||
resetForm();
|
||||
form.network = network;
|
||||
editingChannelId.value = '';
|
||||
isCreateFormVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditForm(channel) {
|
||||
activeNetwork.value = channel.network;
|
||||
editingChannelId.value = channel.id;
|
||||
form.name = channel.name ?? '';
|
||||
form.network = channel.network ?? activeNetwork.value;
|
||||
form.handle = channel.handle ?? '';
|
||||
form.externalUrl = channel.externalUrl ?? '';
|
||||
form.portraitUrl = channel.portraitUrl ?? '';
|
||||
form.bannerUrl = channel.bannerUrl ?? '';
|
||||
formError.value = null;
|
||||
isCreateFormVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -117,14 +144,28 @@
|
||||
formError.value = null;
|
||||
|
||||
try {
|
||||
await channelsStore.createChannel({
|
||||
const payload = {
|
||||
name: form.name,
|
||||
network: form.network,
|
||||
handle: form.handle,
|
||||
externalUrl: form.externalUrl,
|
||||
});
|
||||
portraitUrl: form.portraitUrl,
|
||||
bannerUrl: form.bannerUrl,
|
||||
};
|
||||
|
||||
if (editingChannelId.value) {
|
||||
await channelsStore.updateChannel(editingChannelId.value, {
|
||||
id: editingChannelId.value,
|
||||
workspaceId: editingChannel.value?.workspaceId ?? workspaceStore.activeWorkspaceId,
|
||||
...payload,
|
||||
});
|
||||
} else {
|
||||
await channelsStore.createChannel(payload);
|
||||
}
|
||||
|
||||
activeNetwork.value = form.network;
|
||||
isCreateFormVisible.value = false;
|
||||
editingChannelId.value = '';
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
formError.value = error.message ?? t('channels.errors.createFailed');
|
||||
@@ -164,6 +205,27 @@
|
||||
return `network-${(network ?? 'other').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
|
||||
}
|
||||
|
||||
async function uploadChannelImage(channel, imageKind, event) {
|
||||
const [file] = Array.from(event.target.files ?? []);
|
||||
event.target.value = '';
|
||||
|
||||
if (!file || !channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
formError.value = null;
|
||||
|
||||
try {
|
||||
const updated = await channelsStore.uploadChannelImage(channel.id, imageKind, file);
|
||||
if (editingChannelId.value === channel.id && updated) {
|
||||
form.portraitUrl = updated.portraitUrl ?? '';
|
||||
form.bannerUrl = updated.bannerUrl ?? '';
|
||||
}
|
||||
} catch (error) {
|
||||
formError.value = error.message ?? t('channels.errors.updateFailed');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query.create,
|
||||
createValue => {
|
||||
@@ -221,11 +283,25 @@
|
||||
:class="networkClass(form.network)"
|
||||
>
|
||||
<div class="channel-banner">
|
||||
<v-icon :icon="networkIcon(form.network)" />
|
||||
<img
|
||||
v-if="previewChannel.bannerUrl"
|
||||
:src="previewChannel.bannerUrl"
|
||||
:alt="`${previewChannel.name} banner`"
|
||||
/>
|
||||
<v-icon
|
||||
v-else
|
||||
class="banner-network-icon"
|
||||
:icon="networkIcon(form.network)"
|
||||
/>
|
||||
</div>
|
||||
<div class="channel-profile-row">
|
||||
<div class="channel-portrait">
|
||||
{{ channelInitials(previewChannel) }}
|
||||
<img
|
||||
v-if="previewChannel.portraitUrl"
|
||||
:src="previewChannel.portraitUrl"
|
||||
:alt="`${previewChannel.name} portrait`"
|
||||
/>
|
||||
<span v-else>{{ channelInitials(previewChannel) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ previewChannel.name }}</strong>
|
||||
@@ -240,7 +316,7 @@
|
||||
@submit.prevent="submitForm"
|
||||
>
|
||||
<div class="panel-header">
|
||||
<strong>{{ t('channels.createTitle') }}</strong>
|
||||
<strong>{{ editingChannelId ? t('channels.editTitle') : t('channels.createTitle') }}</strong>
|
||||
<span>{{ form.network }}</span>
|
||||
</div>
|
||||
|
||||
@@ -277,6 +353,44 @@
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="form.portraitUrl"
|
||||
:label="t('channels.fields.portraitUrl')"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="form.bannerUrl"
|
||||
:label="t('channels.fields.bannerUrl')"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="editingChannelId"
|
||||
class="image-upload-row"
|
||||
>
|
||||
<label class="image-upload-button">
|
||||
<v-icon :icon="mdiImage" />
|
||||
<span>{{ t('channels.actions.changePortrait') }}</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/jpg,image/gif,image/webp"
|
||||
:disabled="channelsStore.isUpdating"
|
||||
@change="uploadChannelImage(editingChannel, 'portrait', $event)"
|
||||
/>
|
||||
</label>
|
||||
<label class="image-upload-button">
|
||||
<v-icon :icon="mdiImage" />
|
||||
<span>{{ t('channels.actions.changeBanner') }}</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/jpg,image/gif,image/webp"
|
||||
:disabled="channelsStore.isUpdating"
|
||||
@change="uploadChannelImage(editingChannel, 'banner', $event)"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="panel-actions">
|
||||
@@ -290,9 +404,12 @@
|
||||
<v-btn variant="text" :ripple="false"
|
||||
class="primary"
|
||||
type="submit"
|
||||
:disabled="channelsStore.isCreating"
|
||||
:disabled="channelsStore.isCreating || channelsStore.isUpdating"
|
||||
>
|
||||
{{ channelsStore.isCreating ? t('common.saving') : t('channels.createTitle') }}
|
||||
<v-icon :icon="editingChannelId ? mdiContentSave : mdiPlus" />
|
||||
<span>
|
||||
{{ channelsStore.isCreating || channelsStore.isUpdating ? t('common.saving') : (editingChannelId ? t('channels.saveChanges') : t('channels.createTitle')) }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
</form>
|
||||
@@ -323,7 +440,24 @@
|
||||
:class="networkClass(channel.network)"
|
||||
>
|
||||
<div class="channel-banner">
|
||||
<v-icon :icon="networkIcon(channel.network)" />
|
||||
<img
|
||||
v-if="channel.bannerUrl"
|
||||
:src="channel.bannerUrl"
|
||||
:alt="`${channel.name} banner`"
|
||||
/>
|
||||
<v-icon
|
||||
v-else
|
||||
class="banner-network-icon"
|
||||
:icon="networkIcon(channel.network)"
|
||||
/>
|
||||
<button
|
||||
class="channel-edit-button"
|
||||
type="button"
|
||||
:aria-label="`${t('channels.editTitle')} ${channel.name}`"
|
||||
@click="openEditForm(channel)"
|
||||
>
|
||||
<v-icon :icon="mdiPencil" />
|
||||
</button>
|
||||
<a
|
||||
v-if="channel.externalUrl"
|
||||
:href="channel.externalUrl"
|
||||
@@ -337,7 +471,12 @@
|
||||
|
||||
<div class="channel-profile-row">
|
||||
<div class="channel-portrait">
|
||||
{{ channelInitials(channel) }}
|
||||
<img
|
||||
v-if="channel.portraitUrl"
|
||||
:src="channel.portraitUrl"
|
||||
:alt="`${channel.name} portrait`"
|
||||
/>
|
||||
<span v-else>{{ channelInitials(channel) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ channel.name }}</strong>
|
||||
@@ -451,6 +590,7 @@
|
||||
}
|
||||
|
||||
.primary {
|
||||
@apply gap-2;
|
||||
background: var(--app-color-on-surface);
|
||||
color: var(--app-color-on-primary);
|
||||
}
|
||||
@@ -480,6 +620,21 @@
|
||||
@apply grid gap-4;
|
||||
}
|
||||
|
||||
.image-upload-row {
|
||||
@apply flex flex-wrap gap-2;
|
||||
}
|
||||
|
||||
.image-upload-button {
|
||||
@apply inline-flex min-h-10 cursor-pointer items-center gap-2 rounded-full border px-4 text-sm font-bold;
|
||||
background: var(--app-control-hover);
|
||||
border-color: var(--app-border-subtle);
|
||||
color: var(--app-color-on-surface);
|
||||
}
|
||||
|
||||
.image-upload-button input {
|
||||
@apply sr-only;
|
||||
}
|
||||
|
||||
.field {
|
||||
@apply flex flex-col gap-2 text-sm font-semibold;
|
||||
}
|
||||
@@ -507,16 +662,29 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.channel-banner :deep(.v-icon:first-child) {
|
||||
.channel-banner img {
|
||||
@apply h-full w-full object-cover;
|
||||
}
|
||||
|
||||
.banner-network-icon {
|
||||
@apply text-5xl opacity-90;
|
||||
}
|
||||
|
||||
.channel-edit-button,
|
||||
.channel-banner a {
|
||||
@apply absolute right-3 top-3 grid h-8 w-8 place-items-center rounded-full;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.channel-banner a {
|
||||
@apply top-12;
|
||||
}
|
||||
|
||||
.channel-edit-button {
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
.channel-profile-row {
|
||||
@apply -mt-9 flex items-end gap-3 px-5;
|
||||
}
|
||||
@@ -530,12 +698,16 @@
|
||||
}
|
||||
|
||||
.channel-portrait {
|
||||
@apply grid h-16 w-16 shrink-0 place-items-center rounded-full border-4 text-lg font-black shadow-sm;
|
||||
@apply grid h-16 w-16 shrink-0 place-items-center overflow-hidden rounded-full border-4 text-lg font-black shadow-sm;
|
||||
background: var(--app-color-on-primary);
|
||||
border-color: var(--app-color-on-primary);
|
||||
color: var(--app-color-on-surface);
|
||||
}
|
||||
|
||||
.channel-portrait img {
|
||||
@apply h-full w-full object-cover;
|
||||
}
|
||||
|
||||
.channel-preview-card > p {
|
||||
@apply px-5 text-sm font-semibold;
|
||||
color: var(--app-text-muted);
|
||||
|
||||
@@ -981,6 +981,8 @@
|
||||
"title": "Channels",
|
||||
"description": "Add channels to the workspace.",
|
||||
"createTitle": "Create channel",
|
||||
"editTitle": "Edit channel",
|
||||
"saveChanges": "Save changes",
|
||||
"empty": "No channels are available for the active workspace yet.",
|
||||
"emptyAction": "Add a channel for {network}",
|
||||
"nextDue": "Next due",
|
||||
@@ -989,7 +991,13 @@
|
||||
"name": "Channel name",
|
||||
"network": "Network",
|
||||
"handle": "Handle",
|
||||
"externalUrl": "External URL"
|
||||
"externalUrl": "External URL",
|
||||
"portraitUrl": "Portrait URL",
|
||||
"bannerUrl": "Banner URL"
|
||||
},
|
||||
"actions": {
|
||||
"changePortrait": "Change portrait",
|
||||
"changeBanner": "Change banner"
|
||||
},
|
||||
"metrics": {
|
||||
"scheduled": "Scheduled",
|
||||
@@ -997,7 +1005,8 @@
|
||||
"blocked": "Blocked"
|
||||
},
|
||||
"errors": {
|
||||
"createFailed": "The channel could not be created."
|
||||
"createFailed": "The channel could not be created.",
|
||||
"updateFailed": "The channel could not be updated."
|
||||
}
|
||||
},
|
||||
"reviewQueue": {
|
||||
|
||||
@@ -981,6 +981,8 @@
|
||||
"title": "Canaux",
|
||||
"description": "Ajoutez des canaux à l'espace.",
|
||||
"createTitle": "Créer un canal",
|
||||
"editTitle": "Modifier le canal",
|
||||
"saveChanges": "Enregistrer",
|
||||
"empty": "Aucun canal n'est disponible pour l'espace actif pour le moment.",
|
||||
"emptyAction": "Ajouter un canal pour {network}",
|
||||
"nextDue": "Prochaine échéance",
|
||||
@@ -989,7 +991,13 @@
|
||||
"name": "Nom du canal",
|
||||
"network": "Réseau",
|
||||
"handle": "Identifiant",
|
||||
"externalUrl": "URL externe"
|
||||
"externalUrl": "URL externe",
|
||||
"portraitUrl": "URL du portrait",
|
||||
"bannerUrl": "URL de la bannière"
|
||||
},
|
||||
"actions": {
|
||||
"changePortrait": "Changer le portrait",
|
||||
"changeBanner": "Changer la bannière"
|
||||
},
|
||||
"metrics": {
|
||||
"scheduled": "Planifié",
|
||||
@@ -997,7 +1005,8 @@
|
||||
"blocked": "Bloqué"
|
||||
},
|
||||
"errors": {
|
||||
"createFailed": "Le canal n'a pas pu être créé."
|
||||
"createFailed": "Le canal n'a pas pu être créé.",
|
||||
"updateFailed": "Le canal n'a pas pu être modifié."
|
||||
}
|
||||
},
|
||||
"reviewQueue": {
|
||||
|
||||
Reference in New Issue
Block a user