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_SECRET=
|
||||||
PINTEREST_CLIENT_REDIRECT="${APP_URL}/accounts/pinterest/callback"
|
PINTEREST_CLIENT_REDIRECT="${APP_URL}/accounts/pinterest/callback"
|
||||||
|
|
||||||
# Unsplash (https://unsplash.com/developers)
|
|
||||||
UNSPLASH_ACCESS_KEY=
|
|
||||||
UNSPLASH_SECRET_KEY=
|
|
||||||
|
|
||||||
# Vite
|
# Vite
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,3 @@ GOOGLE_CLIENT_SECRET=test-google-client-secret
|
||||||
|
|
||||||
PINTEREST_CLIENT_ID=test-pinterest-client-id
|
PINTEREST_CLIENT_ID=test-pinterest-client-id
|
||||||
PINTEREST_CLIENT_SECRET=test-pinterest-client-secret
|
PINTEREST_CLIENT_SECRET=test-pinterest-client-secret
|
||||||
|
|
||||||
UNSPLASH_ACCESS_KEY=test-unsplash-access-key
|
|
||||||
UNSPLASH_SECRET_KEY=test-unsplash-secret-key
|
|
||||||
|
|
@ -12,7 +12,7 @@ class LanguageSeeder extends Seeder
|
||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
Language::create(['name' => 'English (US)', 'code' => 'en-US']);
|
Language::create(['name' => 'English', 'code' => 'en']);
|
||||||
Language::create(['name' => 'Português (BR)', 'code' => 'pt-BR']);
|
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 { createApp, h } from 'vue';
|
||||||
|
|
||||||
import { initializeTheme } from './composables/useAppearance';
|
import { initializeTheme } from './composables/useAppearance';
|
||||||
|
import dayjs from './dayjs';
|
||||||
|
|
||||||
configureEcho({
|
configureEcho({
|
||||||
broadcaster: 'reverb',
|
broadcaster: 'reverb',
|
||||||
|
|
@ -23,6 +24,10 @@ createInertiaApp({
|
||||||
import.meta.glob<DefineComponent>('./pages/**/*.vue'),
|
import.meta.glob<DefineComponent>('./pages/**/*.vue'),
|
||||||
),
|
),
|
||||||
setup({ el, App, props, plugin }) {
|
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) })
|
createApp({ render: () => h(App, props) })
|
||||||
.use(i18nVue, {
|
.use(i18nVue, {
|
||||||
resolve: async (lang: string) => {
|
resolve: async (lang: string) => {
|
||||||
|
|
|
||||||
|
|
@ -344,7 +344,7 @@ function handleLogout() {
|
||||||
<div v-if="currentWorkspace" class="px-2 py-2">
|
<div v-if="currentWorkspace" class="px-2 py-2">
|
||||||
<Link :href="createPost.url()">
|
<Link :href="createPost.url()">
|
||||||
<Button :size="sidebarState === 'collapsed' ? 'icon' : 'default'" class="w-full">
|
<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>
|
<span v-if="sidebarState === 'expanded'">{{ $t('sidebar.create_post') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Form } from '@inertiajs/vue3';
|
import { Form } from '@inertiajs/vue3';
|
||||||
|
import { trans } from 'laravel-vue-i18n';
|
||||||
import { useTemplateRef } from 'vue';
|
import { useTemplateRef } from 'vue';
|
||||||
|
|
||||||
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||||
|
|
@ -25,23 +26,23 @@ const passwordInput = useTemplateRef('passwordInput');
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<HeadingSmall
|
<HeadingSmall
|
||||||
title="Delete account"
|
:title="$t('settings.delete_account.heading')"
|
||||||
description="Delete your account and all of its resources"
|
:description="$t('settings.delete_account.description')"
|
||||||
/>
|
/>
|
||||||
<div
|
<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"
|
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">
|
<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">
|
<p class="text-sm">
|
||||||
Please proceed with caution, this cannot be undone.
|
{{ $t('settings.delete_account.warning_message') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger as-child>
|
<DialogTrigger as-child>
|
||||||
<Button variant="destructive" data-test="delete-user-button"
|
<Button variant="destructive" data-test="delete-user-button">
|
||||||
>Delete account</Button
|
{{ $t('settings.delete_account.button') }}
|
||||||
>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Form
|
<Form
|
||||||
|
|
@ -55,29 +56,22 @@ const passwordInput = useTemplateRef('passwordInput');
|
||||||
v-slot="{ errors, processing, reset, clearErrors }"
|
v-slot="{ errors, processing, reset, clearErrors }"
|
||||||
>
|
>
|
||||||
<DialogHeader class="space-y-3">
|
<DialogHeader class="space-y-3">
|
||||||
<DialogTitle
|
<DialogTitle>{{ $t('settings.delete_account.modal_title') }}</DialogTitle>
|
||||||
>Are you sure you want to delete your
|
|
||||||
account?</DialogTitle
|
|
||||||
>
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Once your account is deleted, all of its
|
{{ $t('settings.delete_account.modal_description') }}
|
||||||
resources and data will also be permanently
|
|
||||||
deleted. Please enter your password to confirm
|
|
||||||
you would like to permanently delete your
|
|
||||||
account.
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="password" class="sr-only"
|
<Label for="password" class="sr-only">
|
||||||
>Password</Label
|
{{ $t('settings.delete_account.password') }}
|
||||||
>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
ref="passwordInput"
|
ref="passwordInput"
|
||||||
placeholder="Password"
|
:placeholder="trans('settings.delete_account.password_placeholder')"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.password" />
|
<InputError :message="errors.password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -93,7 +87,7 @@ const passwordInput = useTemplateRef('passwordInput');
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Cancel
|
{{ $t('settings.delete_account.cancel') }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
|
|
||||||
|
|
@ -103,7 +97,7 @@ const passwordInput = useTemplateRef('passwordInput');
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
data-test="confirm-delete-user-button"
|
data-test="confirm-delete-user-button"
|
||||||
>
|
>
|
||||||
Delete account
|
{{ $t('settings.delete_account.confirm') }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { router } from '@inertiajs/vue3';
|
import { router } from '@inertiajs/vue3';
|
||||||
import { IconAlertCircle, IconCheck, IconExternalLink, IconRefresh, IconTrash } from '@tabler/icons-vue';
|
import { IconAlertCircle, IconCheck, IconExternalLink, IconRefresh, IconTrash } from '@tabler/icons-vue';
|
||||||
|
import { trans } from 'laravel-vue-i18n';
|
||||||
import { computed, onMounted, onUnmounted } from 'vue';
|
import { computed, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
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 }}
|
@{{ platform.account.username || platform.account.display_name }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="text-sm text-muted-foreground">
|
<p v-else class="text-sm text-muted-foreground">
|
||||||
Not connected
|
{{ trans('accounts.not_connected') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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" />
|
<IconAlertCircle class="h-4 w-4 mt-0.5 shrink-0" />
|
||||||
<div class="flex-1 min-w-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">
|
<p v-if="platform.account.error_message" class="text-xs truncate opacity-80">
|
||||||
{{ platform.account.error_message }}
|
{{ platform.account.error_message }}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -214,7 +215,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Reconnect account</p>
|
<p>{{ trans('accounts.reconnect_account') }}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
@ -230,7 +231,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
|
||||||
</a>
|
</a>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>View profile</p>
|
<p>{{ trans('accounts.view_profile') }}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
@ -245,7 +246,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Disconnect</p>
|
<p>{{ trans('accounts.disconnect') }}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
@ -256,7 +257,7 @@ const isDisconnected = (account: SocialAccount | null): boolean => {
|
||||||
<!-- Not Connected State -->
|
<!-- Not Connected State -->
|
||||||
<div v-else class="border-t px-4 py-3">
|
<div v-else class="border-t px-4 py-3">
|
||||||
<Button variant="outline" class="w-full" size="sm" @click="openOAuthPopup(platform.value)">
|
<Button variant="outline" class="w-full" size="sm" @click="openOAuthPopup(platform.value)">
|
||||||
Connect
|
{{ trans('accounts.connect') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import weekday from 'dayjs/plugin/weekday';
|
||||||
|
|
||||||
// Import locales
|
// Import locales
|
||||||
import 'dayjs/locale/en';
|
import 'dayjs/locale/en';
|
||||||
|
import "dayjs/locale/pt-br"
|
||||||
|
|
||||||
// Extend dayjs with plugins
|
// Extend dayjs with plugins
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
@ -25,11 +26,13 @@ dayjs.extend(advancedFormat);
|
||||||
dayjs.extend(weekday);
|
dayjs.extend(weekday);
|
||||||
dayjs.extend(isBetween);
|
dayjs.extend(isBetween);
|
||||||
|
|
||||||
dayjs.locale('en');
|
|
||||||
|
|
||||||
// Set Monday as first day of week (to match Carbon/Laravel)
|
// Set Monday as first day of week (to match Carbon/Laravel)
|
||||||
dayjs.updateLocale('en', {
|
dayjs.updateLocale('en', {
|
||||||
weekStart: 1,
|
weekStart: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dayjs.updateLocale('pt-br', {
|
||||||
|
weekStart: 1,
|
||||||
|
});
|
||||||
|
|
||||||
export default dayjs;
|
export default dayjs;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link, usePage } from '@inertiajs/vue3';
|
import { Link, usePage } from '@inertiajs/vue3';
|
||||||
|
import { trans } from 'laravel-vue-i18n';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import Heading from '@/components/Heading.vue';
|
import Heading from '@/components/Heading.vue';
|
||||||
|
|
@ -19,11 +20,11 @@ const canManageWorkspace = computed(() => auth.value.role !== 'member');
|
||||||
const navItems = computed<NavItem[]>(() => {
|
const navItems = computed<NavItem[]>(() => {
|
||||||
const items: NavItem[] = [
|
const items: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Profile',
|
title: trans('settings.nav.profile'),
|
||||||
href: editProfile(),
|
href: editProfile(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Password',
|
title: trans('settings.nav.password'),
|
||||||
href: editPassword(),
|
href: editPassword(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -31,18 +32,18 @@ const navItems = computed<NavItem[]>(() => {
|
||||||
if (canManageWorkspace.value) {
|
if (canManageWorkspace.value) {
|
||||||
items.push(
|
items.push(
|
||||||
{
|
{
|
||||||
title: 'Workspace',
|
title: trans('settings.nav.workspace'),
|
||||||
href: workspaceSettings(),
|
href: workspaceSettings(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Members',
|
title: trans('settings.nav.members'),
|
||||||
href: members(),
|
href: members(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!page.props.selfHosted) {
|
if (!page.props.selfHosted) {
|
||||||
items.push({
|
items.push({
|
||||||
title: 'Billing',
|
title: trans('settings.nav.billing'),
|
||||||
href: billing(),
|
href: billing(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -58,8 +59,8 @@ const { urlIsActive } = useActiveUrl();
|
||||||
<div class="px-4 py-6">
|
<div class="px-4 py-6">
|
||||||
<div class="flex flex-col items-center gap-6">
|
<div class="flex flex-col items-center gap-6">
|
||||||
<Heading
|
<Heading
|
||||||
title="Settings"
|
:title="$t('settings.title')"
|
||||||
description="Manage your profile and account settings"
|
:description="$t('settings.description')"
|
||||||
class="w-full max-w-2xl"
|
class="w-full max-w-2xl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IconInfoCircle } from '@tabler/icons-vue';
|
import { IconInfoCircle } from '@tabler/icons-vue';
|
||||||
|
import { trans } from 'laravel-vue-i18n';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
|
@ -29,13 +30,13 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PopupLayout title="Connect Bluesky">
|
<PopupLayout :title="$t('accounts.bluesky.title')">
|
||||||
<div class="max-w-md mx-auto">
|
<div class="max-w-md mx-auto">
|
||||||
<div class="flex items-center gap-3 mb-6">
|
<div class="flex items-center gap-3 mb-6">
|
||||||
<img src="/images/accounts/bluesky.png" alt="Bluesky" class="h-12 w-12" />
|
<img src="/images/accounts/bluesky.png" alt="Bluesky" class="h-12 w-12" />
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl font-bold tracking-tight">Connect Bluesky</h1>
|
<h1 class="text-xl font-bold tracking-tight">{{ $t('accounts.bluesky.title') }}</h1>
|
||||||
<p class="text-sm text-muted-foreground">Enter your credentials to connect</p>
|
<p class="text-sm text-muted-foreground">{{ $t('accounts.bluesky.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -43,9 +44,9 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
||||||
<input type="hidden" name="_token" :value="csrfToken" />
|
<input type="hidden" name="_token" :value="csrfToken" />
|
||||||
|
|
||||||
<div class="space-y-2">
|
<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"
|
<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 />
|
required />
|
||||||
<p v-if="errors?.identifier" class="text-sm text-destructive">
|
<p v-if="errors?.identifier" class="text-sm text-destructive">
|
||||||
{{ errors.identifier }}
|
{{ errors.identifier }}
|
||||||
|
|
@ -53,9 +54,9 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<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"
|
<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 />
|
required />
|
||||||
<p v-if="errors?.password" class="text-sm text-destructive">
|
<p v-if="errors?.password" class="text-sm text-destructive">
|
||||||
{{ errors.password }}
|
{{ errors.password }}
|
||||||
|
|
@ -64,15 +65,11 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
||||||
|
|
||||||
<Alert>
|
<Alert>
|
||||||
<IconInfoCircle class="h-4 w-4" />
|
<IconInfoCircle class="h-4 w-4" />
|
||||||
<AlertDescription class="inline">
|
<AlertDescription class="inline" v-html="$t('accounts.bluesky.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>.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Button type="submit" :disabled="isSubmitting" class="w-full">
|
<Button type="submit" :disabled="isSubmitting" class="w-full">
|
||||||
{{ isSubmitting ? 'Connecting...' : 'Connect Bluesky' }}
|
{{ isSubmitting ? $t('accounts.bluesky.submitting') : $t('accounts.bluesky.submit') }}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IconBrandFacebook, IconCheck } from '@tabler/icons-vue';
|
import { IconBrandFacebook, IconCheck } from '@tabler/icons-vue';
|
||||||
|
import { trans } from 'laravel-vue-i18n';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
|
@ -40,7 +41,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PopupLayout title="Select Facebook Page">
|
<PopupLayout :title="$t('accounts.facebook.title')">
|
||||||
<!-- Hidden form for regular POST submission -->
|
<!-- Hidden form for regular POST submission -->
|
||||||
<form ref="formRef" :action="selectFacebookPage.url()" method="POST" class="hidden">
|
<form ref="formRef" :action="selectFacebookPage.url()" method="POST" class="hidden">
|
||||||
<input type="hidden" name="_token" :value="csrfToken" />
|
<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">
|
<div class="flex items-center gap-3">
|
||||||
<img src="/images/accounts/facebook.png" alt="Facebook" class="h-10 w-10" />
|
<img src="/images/accounts/facebook.png" alt="Facebook" class="h-10 w-10" />
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl font-bold tracking-tight">Select Facebook Page</h1>
|
<h1 class="text-xl font-bold tracking-tight">{{ $t('accounts.facebook.title') }}</h1>
|
||||||
<p class="text-sm text-muted-foreground">Choose which page you want to connect</p>
|
<p class="text-sm text-muted-foreground">{{ $t('accounts.facebook.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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" />
|
<IconBrandFacebook class="h-7 w-7 text-muted-foreground" />
|
||||||
</div>
|
</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">
|
<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>
|
</p>
|
||||||
</div>
|
</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">
|
<p v-if="page.username" class="text-sm text-muted-foreground truncate">
|
||||||
facebook.com/{{ page.username }}
|
facebook.com/{{ page.username }}
|
||||||
</p>
|
</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>
|
||||||
<div class="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div class="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head } from '@inertiajs/vue3';
|
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 ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
|
||||||
import SocialAccountsGrid, { type Platform } from '@/components/SocialAccountsGrid.vue';
|
import SocialAccountsGrid, { type Platform } from '@/components/SocialAccountsGrid.vue';
|
||||||
|
|
@ -23,9 +24,9 @@ defineProps<Props>();
|
||||||
|
|
||||||
const deleteModal = ref<InstanceType<typeof ConfirmDeleteModal> | null>(null);
|
const deleteModal = ref<InstanceType<typeof ConfirmDeleteModal> | null>(null);
|
||||||
|
|
||||||
const breadcrumbs: BreadcrumbItemType[] = [
|
const breadcrumbs = computed<BreadcrumbItemType[]>(() => [
|
||||||
{ title: 'Accounts', href: accounts.url() },
|
{ title: trans('accounts.title'), href: accounts.url() },
|
||||||
];
|
]);
|
||||||
|
|
||||||
const handleDisconnect = (accountId: string) => {
|
const handleDisconnect = (accountId: string) => {
|
||||||
deleteModal.value?.open({
|
deleteModal.value?.open({
|
||||||
|
|
@ -35,14 +36,14 @@ const handleDisconnect = (accountId: string) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Connected Accounts" />
|
<Head :title="$t('accounts.page_title')" />
|
||||||
|
|
||||||
<AppLayout :breadcrumbs="breadcrumbs">
|
<AppLayout :breadcrumbs="breadcrumbs">
|
||||||
<div class="flex flex-col gap-8 p-6">
|
<div class="flex flex-col gap-8 p-6">
|
||||||
<div>
|
<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">
|
<p class="text-muted-foreground">
|
||||||
Connect your social networks to schedule and publish posts
|
{{ $t('accounts.description') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -55,9 +56,9 @@ const handleDisconnect = (accountId: string) => {
|
||||||
|
|
||||||
<ConfirmDeleteModal
|
<ConfirmDeleteModal
|
||||||
ref="deleteModal"
|
ref="deleteModal"
|
||||||
title="Disconnect Account"
|
:title="$t('accounts.disconnect_modal.title')"
|
||||||
description="Are you sure you want to disconnect this account? You can reconnect it at any time."
|
:description="$t('accounts.disconnect_modal.description')"
|
||||||
action="Disconnect"
|
:action="$t('accounts.disconnect_modal.confirm')"
|
||||||
cancel="Cancel"
|
:cancel="$t('accounts.disconnect_modal.cancel')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IconBuilding, IconCheck } from '@tabler/icons-vue';
|
import { IconBuilding, IconCheck } from '@tabler/icons-vue';
|
||||||
|
import { trans } from 'laravel-vue-i18n';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
|
@ -40,7 +41,7 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PopupLayout title="Select LinkedIn Page">
|
<PopupLayout :title="$t('accounts.linkedin.title')">
|
||||||
<!-- Hidden form for regular POST submission -->
|
<!-- Hidden form for regular POST submission -->
|
||||||
<form ref="formRef" :action="selectLinkedInPage.url()" method="POST" class="hidden">
|
<form ref="formRef" :action="selectLinkedInPage.url()" method="POST" class="hidden">
|
||||||
<input type="hidden" name="_token" :value="csrfToken" />
|
<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">
|
<div class="flex items-center gap-3">
|
||||||
<img src="/images/accounts/linkedin.png" alt="LinkedIn" class="h-10 w-10" />
|
<img src="/images/accounts/linkedin.png" alt="LinkedIn" class="h-10 w-10" />
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl font-bold tracking-tight">Select LinkedIn Page</h1>
|
<h1 class="text-xl font-bold tracking-tight">{{ $t('accounts.linkedin.title') }}</h1>
|
||||||
<p class="text-sm text-muted-foreground">Choose which page you want to connect</p>
|
<p class="text-sm text-muted-foreground">{{ $t('accounts.linkedin.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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" />
|
<IconBuilding class="h-7 w-7 text-muted-foreground" />
|
||||||
</div>
|
</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">
|
<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>
|
</p>
|
||||||
</div>
|
</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">
|
<p v-if="org.vanity_name" class="text-sm text-muted-foreground truncate">
|
||||||
linkedin.com/company/{{ org.vanity_name }}
|
linkedin.com/company/{{ org.vanity_name }}
|
||||||
</p>
|
</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>
|
||||||
<div class="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div class="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IconInfoCircle } from '@tabler/icons-vue';
|
import { IconInfoCircle } from '@tabler/icons-vue';
|
||||||
|
import { trans } from 'laravel-vue-i18n';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
|
@ -28,13 +29,13 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PopupLayout title="Connect Mastodon">
|
<PopupLayout :title="$t('accounts.mastodon.title')">
|
||||||
<div class="max-w-md mx-auto">
|
<div class="max-w-md mx-auto">
|
||||||
<div class="flex items-center gap-3 mb-6">
|
<div class="flex items-center gap-3 mb-6">
|
||||||
<img src="/images/accounts/mastodon.png" alt="Mastodon" class="h-12 w-12" />
|
<img src="/images/accounts/mastodon.png" alt="Mastodon" class="h-12 w-12" />
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl font-bold tracking-tight">Connect Mastodon</h1>
|
<h1 class="text-xl font-bold tracking-tight">{{ $t('accounts.mastodon.title') }}</h1>
|
||||||
<p class="text-sm text-muted-foreground">Enter your Mastodon instance</p>
|
<p class="text-sm text-muted-foreground">{{ $t('accounts.mastodon.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -48,13 +49,13 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
||||||
<input type="hidden" name="_token" :value="csrfToken" />
|
<input type="hidden" name="_token" :value="csrfToken" />
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="instance">Instance URL</Label>
|
<Label for="instance">{{ $t('accounts.mastodon.instance_url') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="instance"
|
id="instance"
|
||||||
name="instance"
|
name="instance"
|
||||||
v-model="instance"
|
v-model="instance"
|
||||||
type="url"
|
type="url"
|
||||||
placeholder="https://mastodon.social"
|
:placeholder="trans('accounts.mastodon.instance_placeholder')"
|
||||||
:class="{ 'border-destructive': errors?.instance }"
|
:class="{ 'border-destructive': errors?.instance }"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -65,11 +66,11 @@ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribut
|
||||||
|
|
||||||
<Alert>
|
<Alert>
|
||||||
<IconInfoCircle class="h-4 w-4" />
|
<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>
|
</Alert>
|
||||||
|
|
||||||
<Button type="submit" :disabled="isSubmitting" class="w-full">
|
<Button type="submit" :disabled="isSubmitting" class="w-full">
|
||||||
{{ isSubmitting ? 'Connecting...' : 'Continue with Mastodon' }}
|
{{ isSubmitting ? $t('accounts.mastodon.submitting') : $t('accounts.mastodon.submit') }}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { IconPlus, IconHash, IconPencil, IconTrash } from '@tabler/icons-vue';
|
import { IconPlus, IconHash, IconPencil, IconTrash } from '@tabler/icons-vue';
|
||||||
import { trans } from 'laravel-vue-i18n';
|
import { trans } from 'laravel-vue-i18n';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
|
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
|
||||||
import CreateDialog from '@/components/hashtags/CreateDialog.vue';
|
import CreateDialog from '@/components/hashtags/CreateDialog.vue';
|
||||||
|
|
@ -37,9 +37,9 @@ const isCreateDialogOpen = ref(false);
|
||||||
const isEditDialogOpen = ref(false);
|
const isEditDialogOpen = ref(false);
|
||||||
const editingHashtag = ref<Hashtag | null>(null);
|
const editingHashtag = ref<Hashtag | null>(null);
|
||||||
|
|
||||||
const breadcrumbs: BreadcrumbItemType[] = [
|
const breadcrumbs = computed<BreadcrumbItemType[]>(() => [
|
||||||
{ title: trans('hashtags.title'), href: hashtagsIndex.url() },
|
{ title: trans('hashtags.title'), href: hashtagsIndex.url() },
|
||||||
];
|
]);
|
||||||
|
|
||||||
const openEditDialog = (hashtag: Hashtag) => {
|
const openEditDialog = (hashtag: Hashtag) => {
|
||||||
editingHashtag.value = hashtag;
|
editingHashtag.value = hashtag;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { IconPlus, IconTag, IconPencil, IconTrash } from '@tabler/icons-vue';
|
import { IconPlus, IconTag, IconPencil, IconTrash } from '@tabler/icons-vue';
|
||||||
import { trans } from 'laravel-vue-i18n';
|
import { trans } from 'laravel-vue-i18n';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
|
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
|
||||||
import CreateDialog from '@/components/labels/CreateDialog.vue';
|
import CreateDialog from '@/components/labels/CreateDialog.vue';
|
||||||
|
|
@ -37,9 +37,9 @@ const isCreateDialogOpen = ref(false);
|
||||||
const isEditDialogOpen = ref(false);
|
const isEditDialogOpen = ref(false);
|
||||||
const editingLabel = ref<Label | null>(null);
|
const editingLabel = ref<Label | null>(null);
|
||||||
|
|
||||||
const breadcrumbs: BreadcrumbItemType[] = [
|
const breadcrumbs = computed<BreadcrumbItemType[]>(() => [
|
||||||
{ title: trans('labels.title'), href: labelsIndex.url() },
|
{ title: trans('labels.title'), href: labelsIndex.url() },
|
||||||
];
|
]);
|
||||||
|
|
||||||
const openEditDialog = (label: Label) => {
|
const openEditDialog = (label: Label) => {
|
||||||
editingLabel.value = label;
|
editingLabel.value = label;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, Link, router } from '@inertiajs/vue3';
|
import { Head, Link, router } from '@inertiajs/vue3';
|
||||||
import { IconChevronLeft, IconChevronRight, IconPlus } from '@tabler/icons-vue';
|
import { IconChevronLeft, IconChevronRight, IconPlus } from '@tabler/icons-vue';
|
||||||
|
import { trans } from 'laravel-vue-i18n';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
@ -46,9 +47,19 @@ interface Props {
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const breadcrumbs: BreadcrumbItemType[] = [
|
const breadcrumbs = computed<BreadcrumbItemType[]>(() => [
|
||||||
{ title: 'Calendar', href: calendar.url() },
|
{ 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
|
// Week view computed
|
||||||
const weekStart = computed(() => dayjs(props.currentWeekStart));
|
const weekStart = computed(() => dayjs(props.currentWeekStart));
|
||||||
|
|
@ -193,7 +204,7 @@ const formatTime = (scheduledAt: string): string => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<Head title="Calendar" />
|
<Head :title="$t('calendar.title')" />
|
||||||
|
|
||||||
<AppLayout :breadcrumbs="breadcrumbs">
|
<AppLayout :breadcrumbs="breadcrumbs">
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
|
|
@ -209,7 +220,7 @@ const formatTime = (scheduledAt: string): string => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" @click="goToToday">
|
<Button variant="outline" size="sm" @click="goToToday">
|
||||||
Today
|
{{ $t('calendar.today') }}
|
||||||
</Button>
|
</Button>
|
||||||
<h1 class="text-lg font-semibold">
|
<h1 class="text-lg font-semibold">
|
||||||
{{ headerTitle }}
|
{{ headerTitle }}
|
||||||
|
|
@ -218,14 +229,13 @@ const formatTime = (scheduledAt: string): string => {
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Tabs :default-value="view" @update:model-value="switchView">
|
<Tabs :default-value="view" @update:model-value="switchView">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="week">Week</TabsTrigger>
|
<TabsTrigger value="week">{{ $t('calendar.week') }}</TabsTrigger>
|
||||||
<TabsTrigger value="month">Month</TabsTrigger>
|
<TabsTrigger value="month">{{ $t('calendar.month') }}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Link :href="createPost.url()">
|
<Link :href="createPost.url()">
|
||||||
<Button>
|
<Button>
|
||||||
<IconPlus class="h-4 w-4" />
|
{{ $t('calendar.new_post') }}
|
||||||
New Post
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -237,15 +247,14 @@ const formatTime = (scheduledAt: string): string => {
|
||||||
:class="{ 'bg-primary/5': isToday(day) }">
|
:class="{ 'bg-primary/5': isToday(day) }">
|
||||||
<!-- Day Header -->
|
<!-- Day Header -->
|
||||||
<div class="flex flex-col items-center py-3 border-b bg-muted/30">
|
<div class="flex flex-col items-center py-3 border-b bg-muted/30">
|
||||||
<span class="text-xs font-medium text-muted-foreground uppercase">
|
<span class="text-xs font-medium text-muted-foreground">
|
||||||
{{ day.format('ddd') }}
|
{{ day.format('dddd') }}
|
||||||
</span>
|
</span>
|
||||||
<span class="mt-1 flex items-center justify-center w-8 h-8 text-sm font-semibold rounded-full"
|
<span class="mt-1 text-sm font-semibold" :class="{
|
||||||
:class="{
|
'text-primary': isToday(day),
|
||||||
'bg-primary text-primary-foreground': isToday(day),
|
'text-foreground': !isToday(day),
|
||||||
'text-foreground': !isToday(day),
|
}">
|
||||||
}">
|
{{ day.format('D/MMMM') }}
|
||||||
{{ day.format('D') }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -279,7 +288,7 @@ const formatTime = (scheduledAt: string): string => {
|
||||||
|
|
||||||
<!-- Content Preview -->
|
<!-- Content Preview -->
|
||||||
<p class="text-xs line-clamp-2 opacity-80">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -288,11 +297,11 @@ const formatTime = (scheduledAt: string): string => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Month View -->
|
<!-- 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 -->
|
<!-- Weekday Headers -->
|
||||||
<div class="grid grid-cols-7 divide-x border-b bg-muted/30">
|
<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"
|
<div v-for="day in weekdayNames" :key="day"
|
||||||
class="py-3 text-center text-xs font-medium text-muted-foreground uppercase">
|
class="py-3 text-center text-xs font-medium text-muted-foreground">
|
||||||
{{ day }}
|
{{ day }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -343,7 +352,7 @@ const formatTime = (scheduledAt: string): string => {
|
||||||
</Link>
|
</Link>
|
||||||
<div v-if="getPostsForDay(day).length > 3"
|
<div v-if="getPostsForDay(day).length > 3"
|
||||||
class="text-xs text-muted-foreground px-2 py-0.5">
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import dayjs from '@/dayjs';
|
import dayjs from '@/dayjs';
|
||||||
import AppLayout from '@/layouts/AppLayout.vue';
|
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 { index as postsIndex, create as createPost, edit as editPost, destroy as destroyPost } from '@/actions/App/Http/Controllers/PostController';
|
||||||
import { type BreadcrumbItemType } from '@/types';
|
import { type BreadcrumbItemType } from '@/types';
|
||||||
|
|
||||||
|
|
@ -161,15 +160,8 @@ const handleDelete = (post: Post) => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<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()">
|
<Link :href="createPost.url()">
|
||||||
<Button>
|
<Button>
|
||||||
<IconPlus class="mr-2 h-4 w-4" />
|
|
||||||
New Post
|
New Post
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, useForm, router } from '@inertiajs/vue3';
|
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 { IconUserPlus, IconUsers, IconMail, IconTrash, IconCrown, IconUser, IconShield } from '@tabler/icons-vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import HeadingSmall from '@/components/HeadingSmall.vue';
|
import HeadingSmall from '@/components/HeadingSmall.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
@ -49,9 +51,9 @@ interface Props {
|
||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
|
|
||||||
const breadcrumbItems: BreadcrumbItem[] = [
|
const breadcrumbItems = computed<BreadcrumbItem[]>(() => [
|
||||||
{ title: 'Members', href: membersRoute.url() },
|
{ title: trans('settings.members.title'), href: membersRoute.url() },
|
||||||
];
|
]);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
email: '',
|
email: '',
|
||||||
|
|
@ -68,7 +70,7 @@ function submitInvite() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelInvite(inviteId: string) {
|
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), {
|
router.delete(destroyInvite.url(inviteId), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
});
|
});
|
||||||
|
|
@ -76,7 +78,7 @@ function cancelInvite(inviteId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRemoveMember(memberId: 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), {
|
router.delete(removeMember.url(memberId), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
});
|
});
|
||||||
|
|
@ -84,12 +86,7 @@ function handleRemoveMember(memberId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRoleLabel(role: string): string {
|
function getRoleLabel(role: string): string {
|
||||||
const labels: Record<string, string> = {
|
return trans(`settings.members.roles.${role}`);
|
||||||
owner: 'Owner',
|
|
||||||
admin: 'Admin',
|
|
||||||
member: 'Member',
|
|
||||||
};
|
|
||||||
return labels[role] || role;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRoleIcon(role: string) {
|
function getRoleIcon(role: string) {
|
||||||
|
|
@ -100,36 +97,36 @@ function getRoleIcon(role: string) {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout :breadcrumbs="breadcrumbItems">
|
<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>
|
<SettingsLayout>
|
||||||
<div class="flex flex-col space-y-6">
|
<div class="flex flex-col space-y-6">
|
||||||
<HeadingSmall
|
<HeadingSmall
|
||||||
title="Team members"
|
:title="$t('settings.members.heading')"
|
||||||
description="Manage members and invites for this workspace"
|
:description="$t('settings.members.description')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2">
|
||||||
<IconUserPlus class="h-5 w-5" />
|
<IconUserPlus class="h-5 w-5" />
|
||||||
Invite Member
|
{{ $t('settings.members.invite.title') }}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Send an email invite to add collaborators
|
{{ $t('settings.members.invite.description') }}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form @submit.prevent="submitInvite" class="space-y-4">
|
<form @submit.prevent="submitInvite" class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="email">Email</Label>
|
<Label for="email">{{ $t('settings.members.invite.email') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
v-model="form.email"
|
v-model="form.email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="collaborator@email.com"
|
:placeholder="trans('settings.members.invite.email_placeholder')"
|
||||||
:class="{ 'border-red-500': form.errors.email }"
|
:class="{ 'border-red-500': form.errors.email }"
|
||||||
/>
|
/>
|
||||||
<p v-if="form.errors.email" class="text-sm text-red-500">
|
<p v-if="form.errors.email" class="text-sm text-red-500">
|
||||||
|
|
@ -138,10 +135,10 @@ function getRoleIcon(role: string) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="role">Role</Label>
|
<Label for="role">{{ $t('settings.members.invite.role') }}</Label>
|
||||||
<Select v-model="form.role">
|
<Select v-model="form.role">
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a role" />
|
<SelectValue :placeholder="trans('settings.members.invite.role_placeholder')" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
|
|
@ -157,7 +154,7 @@ function getRoleIcon(role: string) {
|
||||||
|
|
||||||
<Button type="submit" :disabled="form.processing" class="w-full">
|
<Button type="submit" :disabled="form.processing" class="w-full">
|
||||||
<IconMail class="mr-2 h-4 w-4" />
|
<IconMail class="mr-2 h-4 w-4" />
|
||||||
Send Invite
|
{{ $t('settings.members.invite.submit') }}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
@ -167,15 +164,15 @@ function getRoleIcon(role: string) {
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2">
|
||||||
<IconMail class="h-5 w-5" />
|
<IconMail class="h-5 w-5" />
|
||||||
Pending Invites
|
{{ $t('settings.members.pending.title') }}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Invites awaiting acceptance
|
{{ $t('settings.members.pending.description') }}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div v-if="invites.length === 0" class="text-center py-6 text-muted-foreground">
|
<div v-if="invites.length === 0" class="text-center py-6 text-muted-foreground">
|
||||||
No pending invites
|
{{ $t('settings.members.pending.empty') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="space-y-3">
|
<div v-else class="space-y-3">
|
||||||
<div
|
<div
|
||||||
|
|
@ -205,10 +202,10 @@ function getRoleIcon(role: string) {
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2">
|
||||||
<IconUsers class="h-5 w-5" />
|
<IconUsers class="h-5 w-5" />
|
||||||
Members
|
{{ $t('settings.members.list.title') }}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
People with access to this workspace
|
{{ $t('settings.members.list.description') }}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
@ -223,7 +220,7 @@ function getRoleIcon(role: string) {
|
||||||
<p class="text-sm text-muted-foreground">{{ owner.email }}</p>
|
<p class="text-sm text-muted-foreground">{{ owner.email }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge>Owner</Badge>
|
<Badge>{{ $t('settings.members.roles.owner') }}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -241,7 +238,7 @@ function getRoleIcon(role: string) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="outline" class="capitalize">{{ member.role }}</Badge>
|
<Badge variant="outline">{{ getRoleLabel(member.role) }}</Badge>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
@ -253,7 +250,7 @@ function getRoleIcon(role: string) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="members.length === 0" class="text-center py-6 text-muted-foreground">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Form, Head } from '@inertiajs/vue3';
|
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 PasswordController from '@/actions/App/Http/Controllers/Settings/PasswordController';
|
||||||
import HeadingSmall from '@/components/HeadingSmall.vue';
|
import HeadingSmall from '@/components/HeadingSmall.vue';
|
||||||
|
|
@ -12,25 +14,25 @@ import SettingsLayout from '@/layouts/settings/Layout.vue';
|
||||||
import { edit } from '@/routes/user-password';
|
import { edit } from '@/routes/user-password';
|
||||||
import { type BreadcrumbItem } from '@/types';
|
import { type BreadcrumbItem } from '@/types';
|
||||||
|
|
||||||
const breadcrumbItems: BreadcrumbItem[] = [
|
const breadcrumbItems = computed<BreadcrumbItem[]>(() => [
|
||||||
{
|
{
|
||||||
title: 'Password settings',
|
title: trans('settings.password.title'),
|
||||||
href: edit().url,
|
href: edit().url,
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout :breadcrumbs="breadcrumbItems">
|
<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>
|
<SettingsLayout>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<HeadingSmall
|
<HeadingSmall
|
||||||
title="Update password"
|
:title="$t('settings.password.heading')"
|
||||||
description="Ensure your account is using a long, random password to stay secure"
|
:description="$t('settings.password.description')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
|
|
@ -48,37 +50,37 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
v-slot="{ errors, processing }"
|
v-slot="{ errors, processing }"
|
||||||
>
|
>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="current_password">Current password</Label>
|
<Label for="current_password">{{ $t('settings.password.current_password') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="current_password"
|
id="current_password"
|
||||||
name="current_password"
|
name="current_password"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
placeholder="Current password"
|
:placeholder="trans('settings.password.current_password_placeholder')"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.current_password" />
|
<InputError :message="errors.current_password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="password">New password</Label>
|
<Label for="password">{{ $t('settings.password.new_password') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
placeholder="New password"
|
:placeholder="trans('settings.password.new_password_placeholder')"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.password" />
|
<InputError :message="errors.password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="password_confirmation">Confirm password</Label>
|
<Label for="password_confirmation">{{ $t('settings.password.confirm_password') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password_confirmation"
|
id="password_confirmation"
|
||||||
name="password_confirmation"
|
name="password_confirmation"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
placeholder="Confirm password"
|
:placeholder="trans('settings.password.confirm_password_placeholder')"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.password_confirmation" />
|
<InputError :message="errors.password_confirmation" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -87,7 +89,7 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
data-test="update-password-button"
|
data-test="update-password-button"
|
||||||
>
|
>
|
||||||
Save password
|
{{ $t('settings.password.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Form, Head, Link, usePage } from '@inertiajs/vue3';
|
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 ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||||
import DeleteUser from '@/components/DeleteUser.vue';
|
import DeleteUser from '@/components/DeleteUser.vue';
|
||||||
|
|
@ -25,30 +27,30 @@ defineProps<Props>();
|
||||||
const page = usePage();
|
const page = usePage();
|
||||||
const user = page.props.auth.user;
|
const user = page.props.auth.user;
|
||||||
|
|
||||||
const breadcrumbItems: BreadcrumbItem[] = [
|
const breadcrumbItems = computed<BreadcrumbItem[]>(() => [
|
||||||
{
|
{
|
||||||
title: 'Profile settings',
|
title: trans('settings.profile.title'),
|
||||||
href: edit().url,
|
href: edit().url,
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout :breadcrumbs="breadcrumbItems">
|
<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>
|
<SettingsLayout>
|
||||||
<div class="space-y-10">
|
<div class="space-y-10">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<HeadingSmall
|
<HeadingSmall
|
||||||
title="Profile information"
|
:title="$t('settings.profile.heading')"
|
||||||
description="Update your name and email address"
|
:description="$t('settings.profile.description')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label>Avatar</Label>
|
<Label>{{ $t('settings.profile.avatar') }}</Label>
|
||||||
<PhotoUpload
|
<PhotoUpload
|
||||||
:model-id="user.id"
|
:model-id="user.id"
|
||||||
model-type="App\Models\User"
|
model-type="App\Models\User"
|
||||||
|
|
@ -65,20 +67,20 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
v-slot="{ errors, processing }"
|
v-slot="{ errors, processing }"
|
||||||
>
|
>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="name">Name</Label>
|
<Label for="name">{{ $t('settings.profile.name') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
:default-value="user.name"
|
:default-value="user.name"
|
||||||
required
|
required
|
||||||
autocomplete="name"
|
autocomplete="name"
|
||||||
placeholder="Full name"
|
:placeholder="trans('settings.profile.name_placeholder')"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.name" />
|
<InputError :message="errors.name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="email">Email address</Label>
|
<Label for="email">{{ $t('settings.profile.email') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -86,20 +88,20 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
:default-value="user.email"
|
:default-value="user.email"
|
||||||
required
|
required
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
placeholder="Email address"
|
:placeholder="trans('settings.profile.email_placeholder')"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.email" />
|
<InputError :message="errors.email" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="mustVerifyEmail && !user.email_verified_at">
|
<div v-if="mustVerifyEmail && !user.email_verified_at">
|
||||||
<p class="-mt-4 text-sm text-muted-foreground">
|
<p class="-mt-4 text-sm text-muted-foreground">
|
||||||
Your email address is unverified.
|
{{ $t('settings.profile.email_unverified') }}
|
||||||
<Link
|
<Link
|
||||||
:href="send()"
|
:href="send()"
|
||||||
as="button"
|
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"
|
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>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -107,8 +109,7 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
v-if="status === 'verification-link-sent'"
|
v-if="status === 'verification-link-sent'"
|
||||||
class="mt-2 text-sm font-medium text-green-600"
|
class="mt-2 text-sm font-medium text-green-600"
|
||||||
>
|
>
|
||||||
A new verification link has been sent to your email
|
{{ $t('settings.profile.verification_sent') }}
|
||||||
address.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -116,7 +117,7 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
||||||
:disabled="processing"
|
:disabled="processing"
|
||||||
data-test="update-profile-button"
|
data-test="update-profile-button"
|
||||||
>
|
>
|
||||||
Save
|
{{ $t('settings.profile.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, useForm } from '@inertiajs/vue3';
|
import { Head, useForm } from '@inertiajs/vue3';
|
||||||
|
import { trans } from 'laravel-vue-i18n';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import HeadingSmall from '@/components/HeadingSmall.vue';
|
import HeadingSmall from '@/components/HeadingSmall.vue';
|
||||||
import InputError from '@/components/InputError.vue';
|
import InputError from '@/components/InputError.vue';
|
||||||
|
|
@ -31,12 +33,12 @@ interface Props {
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const breadcrumbItems: BreadcrumbItem[] = [
|
const breadcrumbItems = computed<BreadcrumbItem[]>(() => [
|
||||||
{
|
{
|
||||||
title: 'Workspace settings',
|
title: trans('settings.workspace.title'),
|
||||||
href: settings().url,
|
href: settings().url,
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
name: props.workspace.name,
|
name: props.workspace.name,
|
||||||
|
|
@ -50,19 +52,19 @@ const submit = () => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout :breadcrumbs="breadcrumbItems">
|
<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>
|
<SettingsLayout>
|
||||||
<div class="flex flex-col space-y-6">
|
<div class="flex flex-col space-y-6">
|
||||||
<HeadingSmall
|
<HeadingSmall
|
||||||
title="Workspace settings"
|
:title="$t('settings.workspace.heading')"
|
||||||
description="Update your workspace name, logo, and timezone"
|
:description="$t('settings.workspace.description')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label>Logo</Label>
|
<Label>{{ $t('settings.workspace.logo') }}</Label>
|
||||||
<PhotoUpload
|
<PhotoUpload
|
||||||
:model-id="workspace.id"
|
:model-id="workspace.id"
|
||||||
model-type="App\Models\Workspace"
|
model-type="App\Models\Workspace"
|
||||||
|
|
@ -75,19 +77,19 @@ const submit = () => {
|
||||||
|
|
||||||
<form @submit.prevent="submit" class="space-y-6">
|
<form @submit.prevent="submit" class="space-y-6">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="name">Name</Label>
|
<Label for="name">{{ $t('settings.workspace.name') }}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
required
|
required
|
||||||
placeholder="My Workspace"
|
:placeholder="trans('settings.workspace.name_placeholder')"
|
||||||
/>
|
/>
|
||||||
<InputError class="mt-2" :message="form.errors.name" />
|
<InputError class="mt-2" :message="form.errors.name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="timezone">Timezone</Label>
|
<Label for="timezone">{{ $t('settings.workspace.timezone') }}</Label>
|
||||||
<TimezoneCombobox
|
<TimezoneCombobox
|
||||||
v-model="form.timezone"
|
v-model="form.timezone"
|
||||||
:timezones="timezones"
|
:timezones="timezones"
|
||||||
|
|
@ -96,7 +98,7 @@ const submit = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Button :disabled="form.processing">Save</Button>
|
<Button :disabled="form.processing">{{ $t('settings.workspace.save') }}</Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
enter-active-class="transition ease-in-out"
|
enter-active-class="transition ease-in-out"
|
||||||
|
|
@ -108,7 +110,7 @@ const submit = () => {
|
||||||
v-show="form.recentlySuccessful"
|
v-show="form.recentlySuccessful"
|
||||||
class="text-sm text-neutral-600"
|
class="text-sm text-neutral-600"
|
||||||
>
|
>
|
||||||
Saved.
|
{{ $t('settings.workspace.saved') }}
|
||||||
</p>
|
</p>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@
|
||||||
test('language has fillable attributes', function () {
|
test('language has fillable attributes', function () {
|
||||||
$language = Language::factory()->create([
|
$language = Language::factory()->create([
|
||||||
'name' => 'Portuguese',
|
'name' => 'Portuguese',
|
||||||
'code' => 'pt-BR',
|
'code' => 'pt-br',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect($language->name)->toBe('Portuguese');
|
expect($language->name)->toBe('Portuguese');
|
||||||
expect($language->code)->toBe('pt-BR');
|
expect($language->code)->toBe('pt-br');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue