feat: improvements on ui
This commit is contained in:
parent
38774aaaa6
commit
b8071e775b
31 changed files with 554 additions and 205 deletions
|
|
@ -130,10 +130,6 @@ PINTEREST_CLIENT_ID=
|
|||
PINTEREST_CLIENT_SECRET=
|
||||
PINTEREST_CLIENT_REDIRECT="${APP_URL}/accounts/pinterest/callback"
|
||||
|
||||
# Unsplash (https://unsplash.com/developers)
|
||||
UNSPLASH_ACCESS_KEY=
|
||||
UNSPLASH_SECRET_KEY=
|
||||
|
||||
# Vite
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
||||
|
|
|
|||
|
|
@ -34,7 +34,4 @@ GOOGLE_CLIENT_ID=test-google-client-id
|
|||
GOOGLE_CLIENT_SECRET=test-google-client-secret
|
||||
|
||||
PINTEREST_CLIENT_ID=test-pinterest-client-id
|
||||
PINTEREST_CLIENT_SECRET=test-pinterest-client-secret
|
||||
|
||||
UNSPLASH_ACCESS_KEY=test-unsplash-access-key
|
||||
UNSPLASH_SECRET_KEY=test-unsplash-secret-key
|
||||
PINTEREST_CLIENT_SECRET=test-pinterest-client-secret
|
||||
|
|
@ -12,7 +12,7 @@ class LanguageSeeder extends Seeder
|
|||
*/
|
||||
public function run(): void
|
||||
{
|
||||
Language::create(['name' => 'English (US)', 'code' => 'en-US']);
|
||||
Language::create(['name' => 'Português (BR)', 'code' => 'pt-BR']);
|
||||
Language::create(['name' => 'English', 'code' => 'en']);
|
||||
Language::create(['name' => 'Português', 'code' => 'pt-br']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
59
lang/en/accounts.php
Normal file
59
lang/en/accounts.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Connections',
|
||||
'page_title' => 'Connected Accounts',
|
||||
'description' => 'Connect your social networks to schedule and publish posts',
|
||||
|
||||
'not_connected' => 'Not connected',
|
||||
'connect' => 'Connect',
|
||||
'connection_lost' => 'Connection lost',
|
||||
'reconnect_account' => 'Reconnect account',
|
||||
'view_profile' => 'View profile',
|
||||
'disconnect' => 'Disconnect',
|
||||
|
||||
'disconnect_modal' => [
|
||||
'title' => 'Disconnect Account',
|
||||
'description' => 'Are you sure you want to disconnect this account? You can reconnect it at any time.',
|
||||
'confirm' => 'Disconnect',
|
||||
'cancel' => 'Cancel',
|
||||
],
|
||||
|
||||
'bluesky' => [
|
||||
'title' => 'Connect Bluesky',
|
||||
'description' => 'Enter your credentials to connect',
|
||||
'email' => 'Email',
|
||||
'email_placeholder' => 'yourhandle.bsky.social',
|
||||
'app_password' => 'App Password',
|
||||
'app_password_placeholder' => 'xxxx-xxxx-xxxx-xxxx',
|
||||
'app_password_hint' => 'Use an <strong>App Password</strong> for security. Create one at <a href="https://bsky.app/settings/app-passwords" target="_blank" class="underline">bsky.app/settings</a>.',
|
||||
'submit' => 'Connect Bluesky',
|
||||
'submitting' => 'Connecting...',
|
||||
],
|
||||
|
||||
'mastodon' => [
|
||||
'title' => 'Connect Mastodon',
|
||||
'description' => 'Enter your Mastodon instance',
|
||||
'instance_url' => 'Instance URL',
|
||||
'instance_placeholder' => 'https://mastodon.social',
|
||||
'instance_hint' => 'Enter your Mastodon instance URL (e.g., mastodon.social, techhub.social)',
|
||||
'submit' => 'Continue with Mastodon',
|
||||
'submitting' => 'Connecting...',
|
||||
],
|
||||
|
||||
'facebook' => [
|
||||
'title' => 'Select Facebook Page',
|
||||
'description' => 'Choose which page you want to connect',
|
||||
'no_pages' => 'No pages found',
|
||||
'no_pages_description' => 'You are not an admin of any Facebook page.',
|
||||
'page_label' => 'Facebook Page',
|
||||
],
|
||||
|
||||
'linkedin' => [
|
||||
'title' => 'Select LinkedIn Page',
|
||||
'description' => 'Choose which page you want to connect',
|
||||
'no_pages' => 'No pages found',
|
||||
'no_pages_description' => 'You are not an administrator of any LinkedIn page.',
|
||||
'page_label' => 'LinkedIn Page',
|
||||
],
|
||||
];
|
||||
11
lang/en/calendar.php
Normal file
11
lang/en/calendar.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Calendar',
|
||||
'today' => 'Today',
|
||||
'week' => 'Week',
|
||||
'month' => 'Month',
|
||||
'new_post' => 'New Post',
|
||||
'no_content' => 'No content',
|
||||
'more' => '+:count more',
|
||||
];
|
||||
104
lang/en/settings.php
Normal file
104
lang/en/settings.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Settings',
|
||||
'description' => 'Manage your profile and account settings',
|
||||
|
||||
'nav' => [
|
||||
'profile' => 'Profile',
|
||||
'password' => 'Password',
|
||||
'workspace' => 'Workspace',
|
||||
'members' => 'Members',
|
||||
'billing' => 'Billing',
|
||||
],
|
||||
|
||||
'profile' => [
|
||||
'title' => 'Profile settings',
|
||||
'heading' => 'Profile information',
|
||||
'description' => 'Update your name and email address',
|
||||
'avatar' => 'Avatar',
|
||||
'name' => 'Name',
|
||||
'name_placeholder' => 'Full name',
|
||||
'email' => 'Email address',
|
||||
'email_placeholder' => 'Email address',
|
||||
'email_unverified' => 'Your email address is unverified.',
|
||||
'resend_verification' => 'Click here to resend the verification email.',
|
||||
'verification_sent' => 'A new verification link has been sent to your email address.',
|
||||
'save' => 'Save',
|
||||
],
|
||||
|
||||
'password' => [
|
||||
'title' => 'Password settings',
|
||||
'heading' => 'Update password',
|
||||
'description' => 'Ensure your account is using a long, random password to stay secure',
|
||||
'current_password' => 'Current password',
|
||||
'current_password_placeholder' => 'Current password',
|
||||
'new_password' => 'New password',
|
||||
'new_password_placeholder' => 'New password',
|
||||
'confirm_password' => 'Confirm password',
|
||||
'confirm_password_placeholder' => 'Confirm password',
|
||||
'save' => 'Save password',
|
||||
],
|
||||
|
||||
'delete_account' => [
|
||||
'heading' => 'Delete account',
|
||||
'description' => 'Delete your account and all of its resources',
|
||||
'warning' => 'Warning',
|
||||
'warning_message' => 'Please proceed with caution, this cannot be undone.',
|
||||
'button' => 'Delete account',
|
||||
'modal_title' => 'Are you sure you want to delete your account?',
|
||||
'modal_description' => 'Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.',
|
||||
'password' => 'Password',
|
||||
'password_placeholder' => 'Password',
|
||||
'cancel' => 'Cancel',
|
||||
'confirm' => 'Delete account',
|
||||
],
|
||||
|
||||
'workspace' => [
|
||||
'title' => 'Workspace settings',
|
||||
'heading' => 'Workspace settings',
|
||||
'description' => 'Update your workspace name, logo, and timezone',
|
||||
'logo' => 'Logo',
|
||||
'name' => 'Name',
|
||||
'name_placeholder' => 'My Workspace',
|
||||
'timezone' => 'Timezone',
|
||||
'save' => 'Save',
|
||||
'saved' => 'Saved.',
|
||||
],
|
||||
|
||||
'members' => [
|
||||
'title' => 'Members',
|
||||
'heading' => 'Team members',
|
||||
'description' => 'Manage members and invites for this workspace',
|
||||
|
||||
'invite' => [
|
||||
'title' => 'Invite Member',
|
||||
'description' => 'Send an email invite to add collaborators',
|
||||
'email' => 'Email',
|
||||
'email_placeholder' => 'collaborator@email.com',
|
||||
'role' => 'Role',
|
||||
'role_placeholder' => 'Select a role',
|
||||
'submit' => 'Send Invite',
|
||||
'cancel_confirm' => 'Are you sure you want to cancel this invite?',
|
||||
],
|
||||
|
||||
'pending' => [
|
||||
'title' => 'Pending Invites',
|
||||
'description' => 'Invites awaiting acceptance',
|
||||
'empty' => 'No pending invites',
|
||||
],
|
||||
|
||||
'list' => [
|
||||
'title' => 'Members',
|
||||
'description' => 'People with access to this workspace',
|
||||
'empty' => 'No members besides the owner',
|
||||
'remove_confirm' => 'Are you sure you want to remove this member?',
|
||||
],
|
||||
|
||||
'roles' => [
|
||||
'owner' => 'Owner',
|
||||
'admin' => 'Admin',
|
||||
'member' => 'Member',
|
||||
],
|
||||
],
|
||||
];
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
59
lang/pt-BR/accounts.php
Normal file
59
lang/pt-BR/accounts.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Conexões',
|
||||
'page_title' => 'Contas Conectadas',
|
||||
'description' => 'Conecte suas redes sociais para agendar e publicar posts',
|
||||
|
||||
'not_connected' => 'Não conectado',
|
||||
'connect' => 'Conectar',
|
||||
'connection_lost' => 'Conexão perdida',
|
||||
'reconnect_account' => 'Reconectar conta',
|
||||
'view_profile' => 'Ver perfil',
|
||||
'disconnect' => 'Desconectar',
|
||||
|
||||
'disconnect_modal' => [
|
||||
'title' => 'Desconectar Conta',
|
||||
'description' => 'Tem certeza que deseja desconectar esta conta? Você pode reconectá-la a qualquer momento.',
|
||||
'confirm' => 'Desconectar',
|
||||
'cancel' => 'Cancelar',
|
||||
],
|
||||
|
||||
'bluesky' => [
|
||||
'title' => 'Conectar Bluesky',
|
||||
'description' => 'Digite suas credenciais para conectar',
|
||||
'email' => 'E-mail',
|
||||
'email_placeholder' => 'seuhandle.bsky.social',
|
||||
'app_password' => 'Senha do App',
|
||||
'app_password_placeholder' => 'xxxx-xxxx-xxxx-xxxx',
|
||||
'app_password_hint' => 'Use uma <strong>Senha do App</strong> por segurança. Crie uma em <a href="https://bsky.app/settings/app-passwords" target="_blank" class="underline">bsky.app/settings</a>.',
|
||||
'submit' => 'Conectar Bluesky',
|
||||
'submitting' => 'Conectando...',
|
||||
],
|
||||
|
||||
'mastodon' => [
|
||||
'title' => 'Conectar Mastodon',
|
||||
'description' => 'Digite a instância do seu Mastodon',
|
||||
'instance_url' => 'URL da Instância',
|
||||
'instance_placeholder' => 'https://mastodon.social',
|
||||
'instance_hint' => 'Digite a URL da sua instância Mastodon (ex: mastodon.social, techhub.social)',
|
||||
'submit' => 'Continuar com Mastodon',
|
||||
'submitting' => 'Conectando...',
|
||||
],
|
||||
|
||||
'facebook' => [
|
||||
'title' => 'Selecionar Página do Facebook',
|
||||
'description' => 'Escolha qual página você deseja conectar',
|
||||
'no_pages' => 'Nenhuma página encontrada',
|
||||
'no_pages_description' => 'Você não é administrador de nenhuma página do Facebook.',
|
||||
'page_label' => 'Página do Facebook',
|
||||
],
|
||||
|
||||
'linkedin' => [
|
||||
'title' => 'Selecionar Página do LinkedIn',
|
||||
'description' => 'Escolha qual página você deseja conectar',
|
||||
'no_pages' => 'Nenhuma página encontrada',
|
||||
'no_pages_description' => 'Você não é administrador de nenhuma página do LinkedIn.',
|
||||
'page_label' => 'Página do LinkedIn',
|
||||
],
|
||||
];
|
||||
11
lang/pt-BR/calendar.php
Normal file
11
lang/pt-BR/calendar.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Calendário',
|
||||
'today' => 'Hoje',
|
||||
'week' => 'Semana',
|
||||
'month' => 'Mês',
|
||||
'new_post' => 'Novo Post',
|
||||
'no_content' => 'Sem conteúdo',
|
||||
'more' => '+:count mais',
|
||||
];
|
||||
104
lang/pt-BR/settings.php
Normal file
104
lang/pt-BR/settings.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Configurações',
|
||||
'description' => 'Gerencie seu perfil e configurações da conta',
|
||||
|
||||
'nav' => [
|
||||
'profile' => 'Perfil',
|
||||
'password' => 'Senha',
|
||||
'workspace' => 'Workspace',
|
||||
'members' => 'Membros',
|
||||
'billing' => 'Faturamento',
|
||||
],
|
||||
|
||||
'profile' => [
|
||||
'title' => 'Configurações do perfil',
|
||||
'heading' => 'Informações do perfil',
|
||||
'description' => 'Atualize seu nome e endereço de e-mail',
|
||||
'avatar' => 'Avatar',
|
||||
'name' => 'Nome',
|
||||
'name_placeholder' => 'Nome completo',
|
||||
'email' => 'Endereço de e-mail',
|
||||
'email_placeholder' => 'Endereço de e-mail',
|
||||
'email_unverified' => 'Seu endereço de e-mail não foi verificado.',
|
||||
'resend_verification' => 'Clique aqui para reenviar o e-mail de verificação.',
|
||||
'verification_sent' => 'Um novo link de verificação foi enviado para seu endereço de e-mail.',
|
||||
'save' => 'Salvar',
|
||||
],
|
||||
|
||||
'password' => [
|
||||
'title' => 'Configurações de senha',
|
||||
'heading' => 'Atualizar senha',
|
||||
'description' => 'Certifique-se de que sua conta esteja usando uma senha longa e aleatória para se manter seguro',
|
||||
'current_password' => 'Senha atual',
|
||||
'current_password_placeholder' => 'Senha atual',
|
||||
'new_password' => 'Nova senha',
|
||||
'new_password_placeholder' => 'Nova senha',
|
||||
'confirm_password' => 'Confirmar senha',
|
||||
'confirm_password_placeholder' => 'Confirmar senha',
|
||||
'save' => 'Salvar senha',
|
||||
],
|
||||
|
||||
'delete_account' => [
|
||||
'heading' => 'Excluir conta',
|
||||
'description' => 'Exclua sua conta e todos os seus recursos',
|
||||
'warning' => 'Atenção',
|
||||
'warning_message' => 'Por favor, prossiga com cuidado, isso não pode ser desfeito.',
|
||||
'button' => 'Excluir conta',
|
||||
'modal_title' => 'Tem certeza que deseja excluir sua conta?',
|
||||
'modal_description' => 'Uma vez que sua conta for excluída, todos os seus recursos e dados também serão permanentemente excluídos. Por favor, digite sua senha para confirmar que deseja excluir permanentemente sua conta.',
|
||||
'password' => 'Senha',
|
||||
'password_placeholder' => 'Senha',
|
||||
'cancel' => 'Cancelar',
|
||||
'confirm' => 'Excluir conta',
|
||||
],
|
||||
|
||||
'workspace' => [
|
||||
'title' => 'Configurações do workspace',
|
||||
'heading' => 'Configurações do workspace',
|
||||
'description' => 'Atualize o nome, logo e fuso horário do workspace',
|
||||
'logo' => 'Logo',
|
||||
'name' => 'Nome',
|
||||
'name_placeholder' => 'Meu Workspace',
|
||||
'timezone' => 'Fuso horário',
|
||||
'save' => 'Salvar',
|
||||
'saved' => 'Salvo.',
|
||||
],
|
||||
|
||||
'members' => [
|
||||
'title' => 'Membros',
|
||||
'heading' => 'Membros da equipe',
|
||||
'description' => 'Gerencie membros e convites deste workspace',
|
||||
|
||||
'invite' => [
|
||||
'title' => 'Convidar Membro',
|
||||
'description' => 'Envie um convite por e-mail para adicionar colaboradores',
|
||||
'email' => 'E-mail',
|
||||
'email_placeholder' => 'colaborador@email.com',
|
||||
'role' => 'Função',
|
||||
'role_placeholder' => 'Selecione uma função',
|
||||
'submit' => 'Enviar Convite',
|
||||
'cancel_confirm' => 'Tem certeza que deseja cancelar este convite?',
|
||||
],
|
||||
|
||||
'pending' => [
|
||||
'title' => 'Convites Pendentes',
|
||||
'description' => 'Convites aguardando aceitação',
|
||||
'empty' => 'Nenhum convite pendente',
|
||||
],
|
||||
|
||||
'list' => [
|
||||
'title' => 'Membros',
|
||||
'description' => 'Pessoas com acesso a este workspace',
|
||||
'empty' => 'Nenhum membro além do proprietário',
|
||||
'remove_confirm' => 'Tem certeza que deseja remover este membro?',
|
||||
],
|
||||
|
||||
'roles' => [
|
||||
'owner' => 'Proprietário',
|
||||
'admin' => 'Administrador',
|
||||
'member' => 'Membro',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
@ -8,6 +8,7 @@ import type { DefineComponent } from 'vue';
|
|||
import { createApp, h } from 'vue';
|
||||
|
||||
import { initializeTheme } from './composables/useAppearance';
|
||||
import dayjs from './dayjs';
|
||||
|
||||
configureEcho({
|
||||
broadcaster: 'reverb',
|
||||
|
|
@ -23,6 +24,10 @@ createInertiaApp({
|
|||
import.meta.glob<DefineComponent>('./pages/**/*.vue'),
|
||||
),
|
||||
setup({ el, App, props, plugin }) {
|
||||
// Set dayjs locale based on user's language
|
||||
const userLanguage = (props.initialPage.props as { auth?: { user?: { language?: { code?: string } } } })?.auth?.user?.language?.code;
|
||||
dayjs.locale(userLanguage?.toLowerCase() || 'en');
|
||||
|
||||
createApp({ render: () => h(App, props) })
|
||||
.use(i18nVue, {
|
||||
resolve: async (lang: string) => {
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ function handleLogout() {
|
|||
<div v-if="currentWorkspace" class="px-2 py-2">
|
||||
<Link :href="createPost.url()">
|
||||
<Button :size="sidebarState === 'collapsed' ? 'icon' : 'default'" class="w-full">
|
||||
<IconPlus class="size-4" />
|
||||
<IconPlus v-if="sidebarState === 'collapsed'" class="size-4" />
|
||||
<span v-if="sidebarState === 'expanded'">{{ $t('sidebar.create_post') }}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { Form } from '@inertiajs/vue3';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||
|
|
@ -25,23 +26,23 @@ const passwordInput = useTemplateRef('passwordInput');
|
|||
<template>
|
||||
<div class="space-y-6">
|
||||
<HeadingSmall
|
||||
title="Delete account"
|
||||
description="Delete your account and all of its resources"
|
||||
:title="$t('settings.delete_account.heading')"
|
||||
:description="$t('settings.delete_account.description')"
|
||||
/>
|
||||
<div
|
||||
class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10"
|
||||
>
|
||||
<div class="relative space-y-0.5 text-red-600 dark:text-red-100">
|
||||
<p class="font-medium">Warning</p>
|
||||
<p class="font-medium">{{ $t('settings.delete_account.warning') }}</p>
|
||||
<p class="text-sm">
|
||||
Please proceed with caution, this cannot be undone.
|
||||
{{ $t('settings.delete_account.warning_message') }}
|
||||
</p>
|
||||
</div>
|
||||
<Dialog>
|
||||
<DialogTrigger as-child>
|
||||
<Button variant="destructive" data-test="delete-user-button"
|
||||
>Delete account</Button
|
||||
>
|
||||
<Button variant="destructive" data-test="delete-user-button">
|
||||
{{ $t('settings.delete_account.button') }}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<Form
|
||||
|
|
@ -55,29 +56,22 @@ const passwordInput = useTemplateRef('passwordInput');
|
|||
v-slot="{ errors, processing, reset, clearErrors }"
|
||||
>
|
||||
<DialogHeader class="space-y-3">
|
||||
<DialogTitle
|
||||
>Are you sure you want to delete your
|
||||
account?</DialogTitle
|
||||
>
|
||||
<DialogTitle>{{ $t('settings.delete_account.modal_title') }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Once your account is deleted, all of its
|
||||
resources and data will also be permanently
|
||||
deleted. Please enter your password to confirm
|
||||
you would like to permanently delete your
|
||||
account.
|
||||
{{ $t('settings.delete_account.modal_description') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password" class="sr-only"
|
||||
>Password</Label
|
||||
>
|
||||
<Label for="password" class="sr-only">
|
||||
{{ $t('settings.delete_account.password') }}
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
ref="passwordInput"
|
||||
placeholder="Password"
|
||||
:placeholder="trans('settings.delete_account.password_placeholder')"
|
||||
/>
|
||||
<InputError :message="errors.password" />
|
||||
</div>
|
||||
|
|
@ -93,7 +87,7 @@ const passwordInput = useTemplateRef('passwordInput');
|
|||
}
|
||||
"
|
||||
>
|
||||
Cancel
|
||||
{{ $t('settings.delete_account.cancel') }}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
|
|
@ -103,7 +97,7 @@ const passwordInput = useTemplateRef('passwordInput');
|
|||
:disabled="processing"
|
||||
data-test="confirm-delete-user-button"
|
||||
>
|
||||
Delete account
|
||||
{{ $t('settings.delete_account.confirm') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { router } from '@inertiajs/vue3';
|
||||
import { IconAlertCircle, IconCheck, IconExternalLink, IconRefresh, IconTrash } from '@tabler/icons-vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
|
|
@ -171,7 +172,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
|
|||
@{{ platform.account.username || platform.account.display_name }}
|
||||
</p>
|
||||
<p v-else class="text-sm text-muted-foreground">
|
||||
Not connected
|
||||
{{ trans('accounts.not_connected') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -182,7 +183,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
|
|||
<div v-if="isDisconnected(platform.account)" class="mb-3 flex items-start gap-2 rounded-lg bg-red-100 p-2 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
||||
<IconAlertCircle class="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium">Connection lost</p>
|
||||
<p class="font-medium">{{ trans('accounts.connection_lost') }}</p>
|
||||
<p v-if="platform.account.error_message" class="text-xs truncate opacity-80">
|
||||
{{ platform.account.error_message }}
|
||||
</p>
|
||||
|
|
@ -214,7 +215,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
|
|||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Reconnect account</p>
|
||||
<p>{{ trans('accounts.reconnect_account') }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
|
@ -230,7 +231,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
|
|||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>View profile</p>
|
||||
<p>{{ trans('accounts.view_profile') }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
|
@ -245,7 +246,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
|
|||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Disconnect</p>
|
||||
<p>{{ trans('accounts.disconnect') }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
|
@ -256,7 +257,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
|
|||
<!-- Not Connected State -->
|
||||
<div v-else class="border-t px-4 py-3">
|
||||
<Button variant="outline" class="w-full" size="sm" @click="openOAuthPopup(platform.value)">
|
||||
Connect
|
||||
{{ trans('accounts.connect') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import weekday from 'dayjs/plugin/weekday';
|
|||
|
||||
// Import locales
|
||||
import 'dayjs/locale/en';
|
||||
import "dayjs/locale/pt-br"
|
||||
|
||||
// Extend dayjs with plugins
|
||||
dayjs.extend(utc);
|
||||
|
|
@ -25,11 +26,13 @@ dayjs.extend(advancedFormat);
|
|||
dayjs.extend(weekday);
|
||||
dayjs.extend(isBetween);
|
||||
|
||||
dayjs.locale('en');
|
||||
|
||||
// Set Monday as first day of week (to match Carbon/Laravel)
|
||||
dayjs.updateLocale('en', {
|
||||
weekStart: 1,
|
||||
});
|
||||
|
||||
dayjs.updateLocale('pt-br', {
|
||||
weekStart: 1,
|
||||
});
|
||||
|
||||
export default dayjs;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import Heading from '@/components/Heading.vue';
|
||||
|
|
@ -19,11 +20,11 @@ const canManageWorkspace = computed(() => auth.value.role !== 'member');
|
|||
const navItems = computed<NavItem[]>(() => {
|
||||
const items: NavItem[] = [
|
||||
{
|
||||
title: 'Profile',
|
||||
title: trans('settings.nav.profile'),
|
||||
href: editProfile(),
|
||||
},
|
||||
{
|
||||
title: 'Password',
|
||||
title: trans('settings.nav.password'),
|
||||
href: editPassword(),
|
||||
},
|
||||
];
|
||||
|
|
@ -31,18 +32,18 @@ const navItems = computed<NavItem[]>(() => {
|
|||
if (canManageWorkspace.value) {
|
||||
items.push(
|
||||
{
|
||||
title: 'Workspace',
|
||||
title: trans('settings.nav.workspace'),
|
||||
href: workspaceSettings(),
|
||||
},
|
||||
{
|
||||
title: 'Members',
|
||||
title: trans('settings.nav.members'),
|
||||
href: members(),
|
||||
},
|
||||
);
|
||||
|
||||
if (!page.props.selfHosted) {
|
||||
items.push({
|
||||
title: 'Billing',
|
||||
title: trans('settings.nav.billing'),
|
||||
href: billing(),
|
||||
});
|
||||
}
|
||||
|
|
@ -58,8 +59,8 @@ const { urlIsActive } = useActiveUrl();
|
|||
<div class="px-4 py-6">
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<Heading
|
||||
title="Settings"
|
||||
description="Manage your profile and account settings"
|
||||
:title="$t('settings.title')"
|
||||
:description="$t('settings.description')"
|
||||
class="w-full max-w-2xl"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { IconInfoCircle } from '@tabler/icons-vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
|
@ -29,13 +30,13 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<PopupLayout title="Connect Bluesky">
|
||||
<PopupLayout :title="$t('accounts.bluesky.title')">
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<img src="/images/accounts/bluesky.png" alt="Bluesky" class="h-12 w-12" />
|
||||
<div>
|
||||
<h1 class="text-xl font-bold tracking-tight">Connect Bluesky</h1>
|
||||
<p class="text-sm text-muted-foreground">Enter your credentials to connect</p>
|
||||
<h1 class="text-xl font-bold tracking-tight">{{ $t('accounts.bluesky.title') }}</h1>
|
||||
<p class="text-sm text-muted-foreground">{{ $t('accounts.bluesky.description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -43,9 +44,9 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
<input type="hidden" name="_token" :value="csrfToken" />
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="identifier">Email</Label>
|
||||
<Label for="identifier">{{ $t('accounts.bluesky.email') }}</Label>
|
||||
<Input id="identifier" name="identifier" v-model="identifier" type="text"
|
||||
placeholder="yourhandle.bsky.social" :class="{ 'border-destructive': errors?.identifier }"
|
||||
:placeholder="trans('accounts.bluesky.email_placeholder')" :class="{ 'border-destructive': errors?.identifier }"
|
||||
required />
|
||||
<p v-if="errors?.identifier" class="text-sm text-destructive">
|
||||
{{ errors.identifier }}
|
||||
|
|
@ -53,9 +54,9 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="password">App Password</Label>
|
||||
<Label for="password">{{ $t('accounts.bluesky.app_password') }}</Label>
|
||||
<Input id="password" name="password" v-model="password" type="password"
|
||||
placeholder="xxxx-xxxx-xxxx-xxxx" :class="{ 'border-destructive': errors?.password }"
|
||||
:placeholder="trans('accounts.bluesky.app_password_placeholder')" :class="{ 'border-destructive': errors?.password }"
|
||||
required />
|
||||
<p v-if="errors?.password" class="text-sm text-destructive">
|
||||
{{ errors.password }}
|
||||
|
|
@ -64,15 +65,11 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
|
||||
<Alert>
|
||||
<IconInfoCircle class="h-4 w-4" />
|
||||
<AlertDescription class="inline">
|
||||
Use an <strong>App Password</strong> for security. Create one at <a
|
||||
href="https://bsky.app/settings/app-passwords" target="_blank"
|
||||
class="underline">bsky.app/settings</a>.
|
||||
</AlertDescription>
|
||||
<AlertDescription class="inline" v-html="$t('accounts.bluesky.app_password_hint')" />
|
||||
</Alert>
|
||||
|
||||
<Button type="submit" :disabled="isSubmitting" class="w-full">
|
||||
{{ isSubmitting ? 'Connecting...' : 'Connect Bluesky' }}
|
||||
{{ isSubmitting ? $t('accounts.bluesky.submitting') : $t('accounts.bluesky.submit') }}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { IconBrandFacebook, IconCheck } from '@tabler/icons-vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
|
@ -40,7 +41,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<PopupLayout title="Select Facebook Page">
|
||||
<PopupLayout :title="$t('accounts.facebook.title')">
|
||||
<!-- Hidden form for regular POST submission -->
|
||||
<form ref="formRef" :action="selectFacebookPage.url()" method="POST" class="hidden">
|
||||
<input type="hidden" name="_token" :value="csrfToken" />
|
||||
|
|
@ -51,8 +52,8 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
<div class="flex items-center gap-3">
|
||||
<img src="/images/accounts/facebook.png" alt="Facebook" class="h-10 w-10" />
|
||||
<div>
|
||||
<h1 class="text-xl font-bold tracking-tight">Select Facebook Page</h1>
|
||||
<p class="text-sm text-muted-foreground">Choose which page you want to connect</p>
|
||||
<h1 class="text-xl font-bold tracking-tight">{{ $t('accounts.facebook.title') }}</h1>
|
||||
<p class="text-sm text-muted-foreground">{{ $t('accounts.facebook.description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -64,9 +65,9 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-muted">
|
||||
<IconBrandFacebook class="h-7 w-7 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 class="mt-4 text-lg font-semibold">No pages found</h3>
|
||||
<h3 class="mt-4 text-lg font-semibold">{{ $t('accounts.facebook.no_pages') }}</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
You are not an admin of any Facebook page.
|
||||
{{ $t('accounts.facebook.no_pages_description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -91,7 +92,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
<p v-if="page.username" class="text-sm text-muted-foreground truncate">
|
||||
facebook.com/{{ page.username }}
|
||||
</p>
|
||||
<p v-else class="text-sm text-muted-foreground">Facebook Page</p>
|
||||
<p v-else class="text-sm text-muted-foreground">{{ $t('accounts.facebook.page_label') }}</p>
|
||||
</div>
|
||||
<div class="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
|
||||
import SocialAccountsGrid, { type Platform } from '@/components/SocialAccountsGrid.vue';
|
||||
|
|
@ -23,9 +24,9 @@ defineProps<Props>();
|
|||
|
||||
const deleteModal = ref<InstanceType<typeof ConfirmDeleteModal> | null>(null);
|
||||
|
||||
const breadcrumbs: BreadcrumbItemType[] = [
|
||||
{ title: 'Accounts', href: accounts.url() },
|
||||
];
|
||||
const breadcrumbs = computed<BreadcrumbItemType[]>(() => [
|
||||
{ title: trans('accounts.title'), href: accounts.url() },
|
||||
]);
|
||||
|
||||
const handleDisconnect = (accountId: string) => {
|
||||
deleteModal.value?.open({
|
||||
|
|
@ -35,14 +36,14 @@ const handleDisconnect = (accountId: string) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Connected Accounts" />
|
||||
<Head :title="$t('accounts.page_title')" />
|
||||
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<div class="flex flex-col gap-8 p-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Connected Accounts</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight">{{ $t('accounts.page_title') }}</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Connect your social networks to schedule and publish posts
|
||||
{{ $t('accounts.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -55,9 +56,9 @@ const handleDisconnect = (accountId: string) => {
|
|||
|
||||
<ConfirmDeleteModal
|
||||
ref="deleteModal"
|
||||
title="Disconnect Account"
|
||||
description="Are you sure you want to disconnect this account? You can reconnect it at any time."
|
||||
action="Disconnect"
|
||||
cancel="Cancel"
|
||||
:title="$t('accounts.disconnect_modal.title')"
|
||||
:description="$t('accounts.disconnect_modal.description')"
|
||||
:action="$t('accounts.disconnect_modal.confirm')"
|
||||
:cancel="$t('accounts.disconnect_modal.cancel')"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { IconBuilding, IconCheck } from '@tabler/icons-vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
|
@ -40,7 +41,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<PopupLayout title="Select LinkedIn Page">
|
||||
<PopupLayout :title="$t('accounts.linkedin.title')">
|
||||
<!-- Hidden form for regular POST submission -->
|
||||
<form ref="formRef" :action="selectLinkedInPage.url()" method="POST" class="hidden">
|
||||
<input type="hidden" name="_token" :value="csrfToken" />
|
||||
|
|
@ -54,8 +55,8 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
<div class="flex items-center gap-3">
|
||||
<img src="/images/accounts/linkedin.png" alt="LinkedIn" class="h-10 w-10" />
|
||||
<div>
|
||||
<h1 class="text-xl font-bold tracking-tight">Select LinkedIn Page</h1>
|
||||
<p class="text-sm text-muted-foreground">Choose which page you want to connect</p>
|
||||
<h1 class="text-xl font-bold tracking-tight">{{ $t('accounts.linkedin.title') }}</h1>
|
||||
<p class="text-sm text-muted-foreground">{{ $t('accounts.linkedin.description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -67,9 +68,9 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-muted">
|
||||
<IconBuilding class="h-7 w-7 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 class="mt-4 text-lg font-semibold">No pages found</h3>
|
||||
<h3 class="mt-4 text-lg font-semibold">{{ $t('accounts.linkedin.no_pages') }}</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
You are not an administrator of any LinkedIn page.
|
||||
{{ $t('accounts.linkedin.no_pages_description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -94,7 +95,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
<p v-if="org.vanity_name" class="text-sm text-muted-foreground truncate">
|
||||
linkedin.com/company/{{ org.vanity_name }}
|
||||
</p>
|
||||
<p v-else class="text-sm text-muted-foreground">LinkedIn Page</p>
|
||||
<p v-else class="text-sm text-muted-foreground">{{ $t('accounts.linkedin.page_label') }}</p>
|
||||
</div>
|
||||
<div class="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { IconInfoCircle } from '@tabler/icons-vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
|
@ -28,13 +29,13 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<PopupLayout title="Connect Mastodon">
|
||||
<PopupLayout :title="$t('accounts.mastodon.title')">
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<img src="/images/accounts/mastodon.png" alt="Mastodon" class="h-12 w-12" />
|
||||
<div>
|
||||
<h1 class="text-xl font-bold tracking-tight">Connect Mastodon</h1>
|
||||
<p class="text-sm text-muted-foreground">Enter your Mastodon instance</p>
|
||||
<h1 class="text-xl font-bold tracking-tight">{{ $t('accounts.mastodon.title') }}</h1>
|
||||
<p class="text-sm text-muted-foreground">{{ $t('accounts.mastodon.description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -48,13 +49,13 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
<input type="hidden" name="_token" :value="csrfToken" />
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="instance">Instance URL</Label>
|
||||
<Label for="instance">{{ $t('accounts.mastodon.instance_url') }}</Label>
|
||||
<Input
|
||||
id="instance"
|
||||
name="instance"
|
||||
v-model="instance"
|
||||
type="url"
|
||||
placeholder="https://mastodon.social"
|
||||
:placeholder="trans('accounts.mastodon.instance_placeholder')"
|
||||
:class="{ 'border-destructive': errors?.instance }"
|
||||
required
|
||||
/>
|
||||
|
|
@ -65,11 +66,11 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
|||
|
||||
<Alert>
|
||||
<IconInfoCircle class="h-4 w-4" />
|
||||
<AlertDescription class="inline">Enter your Mastodon instance URL (e.g., mastodon.social, techhub.social)</AlertDescription>
|
||||
<AlertDescription class="inline">{{ $t('accounts.mastodon.instance_hint') }}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button type="submit" :disabled="isSubmitting" class="w-full">
|
||||
{{ isSubmitting ? 'Connecting...' : 'Continue with Mastodon' }}
|
||||
{{ isSubmitting ? $t('accounts.mastodon.submitting') : $t('accounts.mastodon.submit') }}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { Head } from '@inertiajs/vue3';
|
||||
import { IconPlus, IconHash, IconPencil, IconTrash } from '@tabler/icons-vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
|
||||
import CreateDialog from '@/components/hashtags/CreateDialog.vue';
|
||||
|
|
@ -37,9 +37,9 @@ const isCreateDialogOpen = ref(false);
|
|||
const isEditDialogOpen = ref(false);
|
||||
const editingHashtag = ref<Hashtag | null>(null);
|
||||
|
||||
const breadcrumbs: BreadcrumbItemType[] = [
|
||||
const breadcrumbs = computed<BreadcrumbItemType[]>(() => [
|
||||
{ title: trans('hashtags.title'), href: hashtagsIndex.url() },
|
||||
];
|
||||
]);
|
||||
|
||||
const openEditDialog = (hashtag: Hashtag) => {
|
||||
editingHashtag.value = hashtag;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { Head } from '@inertiajs/vue3';
|
||||
import { IconPlus, IconTag, IconPencil, IconTrash } from '@tabler/icons-vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
|
||||
import CreateDialog from '@/components/labels/CreateDialog.vue';
|
||||
|
|
@ -37,9 +37,9 @@ const isCreateDialogOpen = ref(false);
|
|||
const isEditDialogOpen = ref(false);
|
||||
const editingLabel = ref<Label | null>(null);
|
||||
|
||||
const breadcrumbs: BreadcrumbItemType[] = [
|
||||
const breadcrumbs = computed<BreadcrumbItemType[]>(() => [
|
||||
{ title: trans('labels.title'), href: labelsIndex.url() },
|
||||
];
|
||||
]);
|
||||
|
||||
const openEditDialog = (label: Label) => {
|
||||
editingLabel.value = label;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Head, Link, router } from '@inertiajs/vue3';
|
||||
import { IconChevronLeft, IconChevronRight, IconPlus } from '@tabler/icons-vue';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
|
@ -46,9 +47,19 @@ interface Props {
|
|||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const breadcrumbs: BreadcrumbItemType[] = [
|
||||
{ title: 'Calendar', href: calendar.url() },
|
||||
];
|
||||
const breadcrumbs = computed<BreadcrumbItemType[]>(() => [
|
||||
{ title: trans('calendar.title'), href: calendar.url() },
|
||||
]);
|
||||
|
||||
// Generate weekday names based on dayjs locale (respects weekStart config)
|
||||
const weekdayNames = computed(() => {
|
||||
const names = [];
|
||||
const start = dayjs().startOf('week');
|
||||
for (let i = 0; i < 7; i++) {
|
||||
names.push(start.add(i, 'day').format('dddd'));
|
||||
}
|
||||
return names;
|
||||
});
|
||||
|
||||
// Week view computed
|
||||
const weekStart = computed(() => dayjs(props.currentWeekStart));
|
||||
|
|
@ -193,7 +204,7 @@ const formatTime = (scheduledAt: string): string => {
|
|||
|
||||
<template>
|
||||
|
||||
<Head title="Calendar" />
|
||||
<Head :title="$t('calendar.title')" />
|
||||
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<div class="flex flex-col h-full">
|
||||
|
|
@ -209,7 +220,7 @@ const formatTime = (scheduledAt: string): string => {
|
|||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" @click="goToToday">
|
||||
Today
|
||||
{{ $t('calendar.today') }}
|
||||
</Button>
|
||||
<h1 class="text-lg font-semibold">
|
||||
{{ headerTitle }}
|
||||
|
|
@ -218,14 +229,13 @@ const formatTime = (scheduledAt: string): string => {
|
|||
<div class="flex items-center gap-4">
|
||||
<Tabs :default-value="view" @update:model-value="switchView">
|
||||
<TabsList>
|
||||
<TabsTrigger value="week">Week</TabsTrigger>
|
||||
<TabsTrigger value="month">Month</TabsTrigger>
|
||||
<TabsTrigger value="week">{{ $t('calendar.week') }}</TabsTrigger>
|
||||
<TabsTrigger value="month">{{ $t('calendar.month') }}</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<Link :href="createPost.url()">
|
||||
<Button>
|
||||
<IconPlus class="h-4 w-4" />
|
||||
New Post
|
||||
{{ $t('calendar.new_post') }}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -237,15 +247,14 @@ const formatTime = (scheduledAt: string): string => {
|
|||
:class="{ 'bg-primary/5': isToday(day) }">
|
||||
<!-- Day Header -->
|
||||
<div class="flex flex-col items-center py-3 border-b bg-muted/30">
|
||||
<span class="text-xs font-medium text-muted-foreground uppercase">
|
||||
{{ day.format('ddd') }}
|
||||
<span class="text-xs font-medium text-muted-foreground">
|
||||
{{ day.format('dddd') }}
|
||||
</span>
|
||||
<span class="mt-1 flex items-center justify-center w-8 h-8 text-sm font-semibold rounded-full"
|
||||
:class="{
|
||||
'bg-primary text-primary-foreground': isToday(day),
|
||||
'text-foreground': !isToday(day),
|
||||
}">
|
||||
{{ day.format('D') }}
|
||||
<span class="mt-1 text-sm font-semibold" :class="{
|
||||
'text-primary': isToday(day),
|
||||
'text-foreground': !isToday(day),
|
||||
}">
|
||||
{{ day.format('D/MMMM') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -279,7 +288,7 @@ const formatTime = (scheduledAt: string): string => {
|
|||
|
||||
<!-- Content Preview -->
|
||||
<p class="text-xs line-clamp-2 opacity-80">
|
||||
{{ post.post_platforms[0]?.content || 'No content' }}
|
||||
{{ post.post_platforms[0]?.content || $t('calendar.no_content') }}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
|
@ -288,11 +297,11 @@ const formatTime = (scheduledAt: string): string => {
|
|||
</div>
|
||||
|
||||
<!-- Month View -->
|
||||
<div v-else class="flex-1 flex flex-col overflow-hidden border-l">
|
||||
<div v-else class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- Weekday Headers -->
|
||||
<div class="grid grid-cols-7 divide-x border-b bg-muted/30">
|
||||
<div v-for="day in ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']" :key="day"
|
||||
class="py-3 text-center text-xs font-medium text-muted-foreground uppercase">
|
||||
<div v-for="day in weekdayNames" :key="day"
|
||||
class="py-3 text-center text-xs font-medium text-muted-foreground">
|
||||
{{ day }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -343,7 +352,7 @@ const formatTime = (scheduledAt: string): string => {
|
|||
</Link>
|
||||
<div v-if="getPostsForDay(day).length > 3"
|
||||
class="text-xs text-muted-foreground px-2 py-0.5">
|
||||
+{{ getPostsForDay(day).length - 3 }} more
|
||||
{{ $t('calendar.more', { count: getPostsForDay(day).length - 3 }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import dayjs from '@/dayjs';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { calendar } from '@/routes';
|
||||
import { index as postsIndex, create as createPost, edit as editPost, destroy as destroyPost } from '@/actions/App/Http/Controllers/PostController';
|
||||
import { type BreadcrumbItemType } from '@/types';
|
||||
|
||||
|
|
@ -161,15 +160,8 @@ const handleDelete = (post: Post) => {
|
|||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Link :href="calendar.url()">
|
||||
<Button variant="outline">
|
||||
<IconClock class="mr-2 h-4 w-4" />
|
||||
Calendar
|
||||
</Button>
|
||||
</Link>
|
||||
<Link :href="createPost.url()">
|
||||
<Button>
|
||||
<IconPlus class="mr-2 h-4 w-4" />
|
||||
New Post
|
||||
</Button>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { Head, useForm, router } from '@inertiajs/vue3';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { IconUserPlus, IconUsers, IconMail, IconTrash, IconCrown, IconUser, IconShield } from '@tabler/icons-vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import HeadingSmall from '@/components/HeadingSmall.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
|
@ -49,9 +51,9 @@ interface Props {
|
|||
|
||||
defineProps<Props>();
|
||||
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{ title: 'Members', href: membersRoute.url() },
|
||||
];
|
||||
const breadcrumbItems = computed<BreadcrumbItem[]>(() => [
|
||||
{ title: trans('settings.members.title'), href: membersRoute.url() },
|
||||
]);
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
|
|
@ -68,7 +70,7 @@ function submitInvite() {
|
|||
}
|
||||
|
||||
function cancelInvite(inviteId: string) {
|
||||
if (confirm('Are you sure you want to cancel this invite?')) {
|
||||
if (confirm(trans('settings.members.invite.cancel_confirm'))) {
|
||||
router.delete(destroyInvite.url(inviteId), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
|
|
@ -76,7 +78,7 @@ function cancelInvite(inviteId: string) {
|
|||
}
|
||||
|
||||
function handleRemoveMember(memberId: string) {
|
||||
if (confirm('Are you sure you want to remove this member?')) {
|
||||
if (confirm(trans('settings.members.list.remove_confirm'))) {
|
||||
router.delete(removeMember.url(memberId), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
|
|
@ -84,12 +86,7 @@ function handleRemoveMember(memberId: string) {
|
|||
}
|
||||
|
||||
function getRoleLabel(role: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
owner: 'Owner',
|
||||
admin: 'Admin',
|
||||
member: 'Member',
|
||||
};
|
||||
return labels[role] || role;
|
||||
return trans(`settings.members.roles.${role}`);
|
||||
}
|
||||
|
||||
function getRoleIcon(role: string) {
|
||||
|
|
@ -100,36 +97,36 @@ function getRoleIcon(role: string) {
|
|||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbItems">
|
||||
<Head title="Members" />
|
||||
<Head :title="$t('settings.members.title')" />
|
||||
|
||||
<h1 class="sr-only">Members</h1>
|
||||
<h1 class="sr-only">{{ $t('settings.members.title') }}</h1>
|
||||
|
||||
<SettingsLayout>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<HeadingSmall
|
||||
title="Team members"
|
||||
description="Manage members and invites for this workspace"
|
||||
:title="$t('settings.members.heading')"
|
||||
:description="$t('settings.members.description')"
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<IconUserPlus class="h-5 w-5" />
|
||||
Invite Member
|
||||
{{ $t('settings.members.invite.title') }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Send an email invite to add collaborators
|
||||
{{ $t('settings.members.invite.description') }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form @submit.prevent="submitInvite" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Label for="email">{{ $t('settings.members.invite.email') }}</Label>
|
||||
<Input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
placeholder="collaborator@email.com"
|
||||
:placeholder="trans('settings.members.invite.email_placeholder')"
|
||||
:class="{ 'border-red-500': form.errors.email }"
|
||||
/>
|
||||
<p v-if="form.errors.email" class="text-sm text-red-500">
|
||||
|
|
@ -138,10 +135,10 @@ function getRoleIcon(role: string) {
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="role">Role</Label>
|
||||
<Label for="role">{{ $t('settings.members.invite.role') }}</Label>
|
||||
<Select v-model="form.role">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a role" />
|
||||
<SelectValue :placeholder="trans('settings.members.invite.role_placeholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
|
|
@ -157,7 +154,7 @@ function getRoleIcon(role: string) {
|
|||
|
||||
<Button type="submit" :disabled="form.processing" class="w-full">
|
||||
<IconMail class="mr-2 h-4 w-4" />
|
||||
Send Invite
|
||||
{{ $t('settings.members.invite.submit') }}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
|
@ -167,15 +164,15 @@ function getRoleIcon(role: string) {
|
|||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<IconMail class="h-5 w-5" />
|
||||
Pending Invites
|
||||
{{ $t('settings.members.pending.title') }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Invites awaiting acceptance
|
||||
{{ $t('settings.members.pending.description') }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="invites.length === 0" class="text-center py-6 text-muted-foreground">
|
||||
No pending invites
|
||||
{{ $t('settings.members.pending.empty') }}
|
||||
</div>
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
|
|
@ -205,10 +202,10 @@ function getRoleIcon(role: string) {
|
|||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<IconUsers class="h-5 w-5" />
|
||||
Members
|
||||
{{ $t('settings.members.list.title') }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
People with access to this workspace
|
||||
{{ $t('settings.members.list.description') }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
@ -223,7 +220,7 @@ function getRoleIcon(role: string) {
|
|||
<p class="text-sm text-muted-foreground">{{ owner.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge>Owner</Badge>
|
||||
<Badge>{{ $t('settings.members.roles.owner') }}</Badge>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -241,7 +238,7 @@ function getRoleIcon(role: string) {
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge variant="outline" class="capitalize">{{ member.role }}</Badge>
|
||||
<Badge variant="outline">{{ getRoleLabel(member.role) }}</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
|
@ -253,7 +250,7 @@ function getRoleIcon(role: string) {
|
|||
</div>
|
||||
|
||||
<div v-if="members.length === 0" class="text-center py-6 text-muted-foreground">
|
||||
No members besides the owner
|
||||
{{ $t('settings.members.list.empty') }}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Form, Head } from '@inertiajs/vue3';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import PasswordController from '@/actions/App/Http/Controllers/Settings/PasswordController';
|
||||
import HeadingSmall from '@/components/HeadingSmall.vue';
|
||||
|
|
@ -12,25 +14,25 @@ import SettingsLayout from '@/layouts/settings/Layout.vue';
|
|||
import { edit } from '@/routes/user-password';
|
||||
import { type BreadcrumbItem } from '@/types';
|
||||
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
const breadcrumbItems = computed<BreadcrumbItem[]>(() => [
|
||||
{
|
||||
title: 'Password settings',
|
||||
title: trans('settings.password.title'),
|
||||
href: edit().url,
|
||||
},
|
||||
];
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbItems">
|
||||
<Head title="Password settings" />
|
||||
<Head :title="$t('settings.password.title')" />
|
||||
|
||||
<h1 class="sr-only">Password Settings</h1>
|
||||
<h1 class="sr-only">{{ $t('settings.password.title') }}</h1>
|
||||
|
||||
<SettingsLayout>
|
||||
<div class="space-y-6">
|
||||
<HeadingSmall
|
||||
title="Update password"
|
||||
description="Ensure your account is using a long, random password to stay secure"
|
||||
:title="$t('settings.password.heading')"
|
||||
:description="$t('settings.password.description')"
|
||||
/>
|
||||
|
||||
<Form
|
||||
|
|
@ -48,37 +50,37 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
|||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<Label for="current_password">Current password</Label>
|
||||
<Label for="current_password">{{ $t('settings.password.current_password') }}</Label>
|
||||
<Input
|
||||
id="current_password"
|
||||
name="current_password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="Current password"
|
||||
:placeholder="trans('settings.password.current_password_placeholder')"
|
||||
/>
|
||||
<InputError :message="errors.current_password" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">New password</Label>
|
||||
<Label for="password">{{ $t('settings.password.new_password') }}</Label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="New password"
|
||||
:placeholder="trans('settings.password.new_password_placeholder')"
|
||||
/>
|
||||
<InputError :message="errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password_confirmation">Confirm password</Label>
|
||||
<Label for="password_confirmation">{{ $t('settings.password.confirm_password') }}</Label>
|
||||
<Input
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="Confirm password"
|
||||
:placeholder="trans('settings.password.confirm_password_placeholder')"
|
||||
/>
|
||||
<InputError :message="errors.password_confirmation" />
|
||||
</div>
|
||||
|
|
@ -87,7 +89,7 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
|||
:disabled="processing"
|
||||
data-test="update-password-button"
|
||||
>
|
||||
Save password
|
||||
{{ $t('settings.password.save') }}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Form, Head, Link, usePage } from '@inertiajs/vue3';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||
import DeleteUser from '@/components/DeleteUser.vue';
|
||||
|
|
@ -25,30 +27,30 @@ defineProps<Props>();
|
|||
const page = usePage();
|
||||
const user = page.props.auth.user;
|
||||
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
const breadcrumbItems = computed<BreadcrumbItem[]>(() => [
|
||||
{
|
||||
title: 'Profile settings',
|
||||
title: trans('settings.profile.title'),
|
||||
href: edit().url,
|
||||
},
|
||||
];
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbItems">
|
||||
<Head title="Profile settings" />
|
||||
<Head :title="$t('settings.profile.title')" />
|
||||
|
||||
<h1 class="sr-only">Profile Settings</h1>
|
||||
<h1 class="sr-only">{{ $t('settings.profile.title') }}</h1>
|
||||
|
||||
<SettingsLayout>
|
||||
<div class="space-y-10">
|
||||
<div class="space-y-6">
|
||||
<HeadingSmall
|
||||
title="Profile information"
|
||||
description="Update your name and email address"
|
||||
:title="$t('settings.profile.heading')"
|
||||
:description="$t('settings.profile.description')"
|
||||
/>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Avatar</Label>
|
||||
<Label>{{ $t('settings.profile.avatar') }}</Label>
|
||||
<PhotoUpload
|
||||
:model-id="user.id"
|
||||
model-type="App\Models\User"
|
||||
|
|
@ -65,20 +67,20 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
|||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Name</Label>
|
||||
<Label for="name">{{ $t('settings.profile.name') }}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
:default-value="user.name"
|
||||
required
|
||||
autocomplete="name"
|
||||
placeholder="Full name"
|
||||
:placeholder="trans('settings.profile.name_placeholder')"
|
||||
/>
|
||||
<InputError :message="errors.name" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email address</Label>
|
||||
<Label for="email">{{ $t('settings.profile.email') }}</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
|
|
@ -86,20 +88,20 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
|||
:default-value="user.email"
|
||||
required
|
||||
autocomplete="username"
|
||||
placeholder="Email address"
|
||||
:placeholder="trans('settings.profile.email_placeholder')"
|
||||
/>
|
||||
<InputError :message="errors.email" />
|
||||
</div>
|
||||
|
||||
<div v-if="mustVerifyEmail && !user.email_verified_at">
|
||||
<p class="-mt-4 text-sm text-muted-foreground">
|
||||
Your email address is unverified.
|
||||
{{ $t('settings.profile.email_unverified') }}
|
||||
<Link
|
||||
:href="send()"
|
||||
as="button"
|
||||
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||
>
|
||||
Click here to resend the verification email.
|
||||
{{ $t('settings.profile.resend_verification') }}
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
|
|
@ -107,8 +109,7 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
|||
v-if="status === 'verification-link-sent'"
|
||||
class="mt-2 text-sm font-medium text-green-600"
|
||||
>
|
||||
A new verification link has been sent to your email
|
||||
address.
|
||||
{{ $t('settings.profile.verification_sent') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -116,7 +117,7 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
|||
:disabled="processing"
|
||||
data-test="update-profile-button"
|
||||
>
|
||||
Save
|
||||
{{ $t('settings.profile.save') }}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { trans } from 'laravel-vue-i18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import HeadingSmall from '@/components/HeadingSmall.vue';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
|
|
@ -31,12 +33,12 @@ interface Props {
|
|||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
const breadcrumbItems = computed<BreadcrumbItem[]>(() => [
|
||||
{
|
||||
title: 'Workspace settings',
|
||||
title: trans('settings.workspace.title'),
|
||||
href: settings().url,
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
const form = useForm({
|
||||
name: props.workspace.name,
|
||||
|
|
@ -50,19 +52,19 @@ const submit = () => {
|
|||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbItems">
|
||||
<Head title="Workspace settings" />
|
||||
<Head :title="$t('settings.workspace.title')" />
|
||||
|
||||
<h1 class="sr-only">Workspace Settings</h1>
|
||||
<h1 class="sr-only">{{ $t('settings.workspace.title') }}</h1>
|
||||
|
||||
<SettingsLayout>
|
||||
<div class="flex flex-col space-y-6">
|
||||
<HeadingSmall
|
||||
title="Workspace settings"
|
||||
description="Update your workspace name, logo, and timezone"
|
||||
:title="$t('settings.workspace.heading')"
|
||||
:description="$t('settings.workspace.description')"
|
||||
/>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Logo</Label>
|
||||
<Label>{{ $t('settings.workspace.logo') }}</Label>
|
||||
<PhotoUpload
|
||||
:model-id="workspace.id"
|
||||
model-type="App\Models\Workspace"
|
||||
|
|
@ -75,19 +77,19 @@ const submit = () => {
|
|||
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Name</Label>
|
||||
<Label for="name">{{ $t('settings.workspace.name') }}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
placeholder="My Workspace"
|
||||
:placeholder="trans('settings.workspace.name_placeholder')"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="timezone">Timezone</Label>
|
||||
<Label for="timezone">{{ $t('settings.workspace.timezone') }}</Label>
|
||||
<TimezoneCombobox
|
||||
v-model="form.timezone"
|
||||
:timezones="timezones"
|
||||
|
|
@ -96,7 +98,7 @@ const submit = () => {
|
|||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<Button :disabled="form.processing">Save</Button>
|
||||
<Button :disabled="form.processing">{{ $t('settings.workspace.save') }}</Button>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-in-out"
|
||||
|
|
@ -108,7 +110,7 @@ const submit = () => {
|
|||
v-show="form.recentlySuccessful"
|
||||
class="text-sm text-neutral-600"
|
||||
>
|
||||
Saved.
|
||||
{{ $t('settings.workspace.saved') }}
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@
|
|||
test('language has fillable attributes', function () {
|
||||
$language = Language::factory()->create([
|
||||
'name' => 'Portuguese',
|
||||
'code' => 'pt-BR',
|
||||
'code' => 'pt-br',
|
||||
]);
|
||||
|
||||
expect($language->name)->toBe('Portuguese');
|
||||
expect($language->code)->toBe('pt-BR');
|
||||
expect($language->code)->toBe('pt-br');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue