formatting, browser tests
This commit is contained in:
parent
9a9a901d46
commit
d44bcf6fda
@ -19,7 +19,7 @@ configureEcho({
|
|||||||
forceTLS: false,
|
forceTLS: false,
|
||||||
enabledTransports: ['ws', 'wss'],
|
enabledTransports: ['ws', 'wss'],
|
||||||
});
|
});
|
||||||
if(window){
|
if (window) {
|
||||||
(window as any).echoConfigured = true;
|
(window as any).echoConfigured = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,17 +42,10 @@ function submit() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="c-add-mutation-form">
|
<div class="c-add-mutation-form">
|
||||||
<h4 class="c-add-mutation-form__title">
|
<h4 class="c-add-mutation-form__title">Add Mutation</h4>
|
||||||
Add Mutation
|
<form @submit.prevent="submit" class="c-add-mutation-form__form">
|
||||||
</h4>
|
|
||||||
<form
|
|
||||||
@submit.prevent="submit"
|
|
||||||
class="c-add-mutation-form__form"
|
|
||||||
>
|
|
||||||
<div class="c-add-mutation-form__field">
|
<div class="c-add-mutation-form__field">
|
||||||
<label
|
<label for="amount" class="c-add-mutation-form__label"
|
||||||
for="amount"
|
|
||||||
class="c-add-mutation-form__label"
|
|
||||||
>Amount</label
|
>Amount</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -61,15 +54,16 @@ function submit() {
|
|||||||
type="number"
|
type="number"
|
||||||
class="c-add-mutation-form__input"
|
class="c-add-mutation-form__input"
|
||||||
/>
|
/>
|
||||||
<div v-if="form.errors.amount" class="c-add-mutation-form__error">
|
<div
|
||||||
|
v-if="form.errors.amount"
|
||||||
|
class="c-add-mutation-form__error"
|
||||||
|
>
|
||||||
{{ form.errors.amount }}
|
{{ form.errors.amount }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="c-add-mutation-form__field">
|
<div class="c-add-mutation-form__field">
|
||||||
<label
|
<label for="description" class="c-add-mutation-form__label"
|
||||||
for="description"
|
|
||||||
class="c-add-mutation-form__label"
|
|
||||||
>Description</label
|
>Description</label
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
@ -88,8 +82,7 @@ function submit() {
|
|||||||
|
|
||||||
<!-- Media Uploads for Mutations -->
|
<!-- Media Uploads for Mutations -->
|
||||||
<div class="c-add-mutation-form__field">
|
<div class="c-add-mutation-form__field">
|
||||||
<label
|
<label class="c-add-mutation-form__label"
|
||||||
class="c-add-mutation-form__label"
|
|
||||||
>Attach Proof Media (Photos/Videos)</label
|
>Attach Proof Media (Photos/Videos)</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import {
|
|||||||
} from '@/components/ui/tooltip';
|
} from '@/components/ui/tooltip';
|
||||||
import UserMenuContent from '@/components/UserMenuContent.vue';
|
import UserMenuContent from '@/components/UserMenuContent.vue';
|
||||||
import { useCurrentUrl } from '@/composables/useCurrentUrl';
|
import { useCurrentUrl } from '@/composables/useCurrentUrl';
|
||||||
|
import { route } from 'ziggy-js';
|
||||||
import { getInitials } from '@/composables/useInitials';
|
import { getInitials } from '@/composables/useInitials';
|
||||||
import { toUrl } from '@/lib/utils';
|
import { toUrl } from '@/lib/utils';
|
||||||
import { dashboard } from '@/routes';
|
import { dashboard } from '@/routes';
|
||||||
@ -239,7 +240,7 @@ const rightNavItems: NavItem[] = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="page.props.unreadNotificationsCount > 0"
|
v-if="(page.props as any).unreadNotificationsCount > 0"
|
||||||
class="relative"
|
class="relative"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
@ -260,7 +261,9 @@ const rightNavItems: NavItem[] = [
|
|||||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white">
|
<span
|
||||||
|
class="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white"
|
||||||
|
>
|
||||||
{{ page.props.unreadNotificationsCount }}
|
{{ page.props.unreadNotificationsCount }}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
import { useForm } from '@inertiajs/vue3';
|
import { useForm } from '@inertiajs/vue3';
|
||||||
import { useEcho, echoIsConfigured, configureEcho } from '@laravel/echo-vue';
|
import { useEcho, echoIsConfigured, configureEcho } from '@laravel/echo-vue';
|
||||||
import { Paperclip } from '@lucide/vue';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { route } from 'ziggy-js';
|
import { route } from 'ziggy-js';
|
||||||
|
import { Paperclip, Info } from '@lucide/vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
chat: {
|
chat: {
|
||||||
@ -52,7 +52,6 @@ useEcho(`chats.${props.chat.id}`, 'MessageSent', (e: any) => {
|
|||||||
|
|
||||||
function handleFileChange(event: Event) {
|
function handleFileChange(event: Event) {
|
||||||
const files = (event.target as HTMLInputElement).files;
|
const files = (event.target as HTMLInputElement).files;
|
||||||
|
|
||||||
if (files) {
|
if (files) {
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
form.media.push(files[i]);
|
form.media.push(files[i]);
|
||||||
@ -68,7 +67,6 @@ function submit() {
|
|||||||
form.post(route('chats.messages.store', props.chat.id), {
|
form.post(route('chats.messages.store', props.chat.id), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
if (fileInput.value) {
|
if (fileInput.value) {
|
||||||
fileInput.value.value = '';
|
fileInput.value.value = '';
|
||||||
}
|
}
|
||||||
@ -100,8 +98,16 @@ function closeLightbox() {
|
|||||||
<div
|
<div
|
||||||
v-for="message in chat.messages"
|
v-for="message in chat.messages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
class="c-chat__message"
|
:class="[
|
||||||
|
'c-chat__message',
|
||||||
|
{
|
||||||
|
'c-chat__message--system':
|
||||||
|
message.content.startsWith('System:'),
|
||||||
|
},
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
|
<!-- Standard User Chat Message -->
|
||||||
|
<template v-if="!message.content.startsWith('System:')">
|
||||||
<div class="c-chat__message-header">
|
<div class="c-chat__message-header">
|
||||||
<span class="c-chat__message-author">{{
|
<span class="c-chat__message-author">{{
|
||||||
message.user.name
|
message.user.name
|
||||||
@ -144,6 +150,25 @@ function closeLightbox() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Subtle Activity Log System Message -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="c-chat__system-inner">
|
||||||
|
<Info class="c-chat__system-icon" />
|
||||||
|
<span class="c-chat__system-text">
|
||||||
|
{{ message.content.replace(/^System:\s*/, '') }}
|
||||||
|
</span>
|
||||||
|
<span class="c-chat__system-time">
|
||||||
|
{{
|
||||||
|
new Date(message.created_at).toLocaleTimeString(
|
||||||
|
[],
|
||||||
|
{ hour: '2-digit', minute: '2-digit' },
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="chat.messages.length === 0" class="c-chat__empty">
|
<div v-if="chat.messages.length === 0" class="c-chat__empty">
|
||||||
No messages yet.
|
No messages yet.
|
||||||
@ -260,6 +285,26 @@ function closeLightbox() {
|
|||||||
@apply overflow-hidden bg-white p-4 shadow-sm sm:rounded-lg dark:bg-gray-800;
|
@apply overflow-hidden bg-white p-4 shadow-sm sm:rounded-lg dark:bg-gray-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-chat__message--system {
|
||||||
|
@apply border-0 bg-transparent p-0 shadow-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-chat__system-inner {
|
||||||
|
@apply flex items-center gap-2 rounded-md border border-neutral-200/60 bg-neutral-50 px-3 py-1.5 text-xs text-neutral-500 dark:border-neutral-800/40 dark:bg-neutral-900/10 dark:text-neutral-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-chat__system-icon {
|
||||||
|
@apply size-3.5 shrink-0 text-neutral-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-chat__system-text {
|
||||||
|
@apply flex-1 font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-chat__system-time {
|
||||||
|
@apply shrink-0 text-[10px] text-neutral-400;
|
||||||
|
}
|
||||||
|
|
||||||
.c-chat__message-header {
|
.c-chat__message-header {
|
||||||
@apply flex justify-between;
|
@apply flex justify-between;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,11 +40,12 @@ function submit() {
|
|||||||
<div class="c-create-ledger-form__body">
|
<div class="c-create-ledger-form__body">
|
||||||
<h3 class="c-create-ledger-form__title">Create a New Ledger</h3>
|
<h3 class="c-create-ledger-form__title">Create a New Ledger</h3>
|
||||||
|
|
||||||
<form @submit.prevent="submit" class="c-create-ledger-form__form">
|
<form
|
||||||
|
@submit.prevent="submit"
|
||||||
|
class="c-create-ledger-form__form"
|
||||||
|
>
|
||||||
<div class="c-create-ledger-form__field">
|
<div class="c-create-ledger-form__field">
|
||||||
<label
|
<label for="name" class="c-create-ledger-form__label"
|
||||||
for="name"
|
|
||||||
class="c-create-ledger-form__label"
|
|
||||||
>Name</label
|
>Name</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -62,9 +63,7 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="c-create-ledger-form__field">
|
<div class="c-create-ledger-form__field">
|
||||||
<label
|
<label for="rules" class="c-create-ledger-form__label"
|
||||||
for="rules"
|
|
||||||
class="c-create-ledger-form__label"
|
|
||||||
>Rules</label
|
>Rules</label
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
@ -92,9 +91,15 @@ function submit() {
|
|||||||
id="alignment"
|
id="alignment"
|
||||||
class="c-create-ledger-form__select"
|
class="c-create-ledger-form__select"
|
||||||
>
|
>
|
||||||
<option value="positive">Positive (Higher Score is Better)</option>
|
<option value="positive">
|
||||||
<option value="neutral">Neutral (Frictionless / Standard)</option>
|
Positive (Higher Score is Better)
|
||||||
<option value="negative">Negative (Lower Score is Better / Demerits)</option>
|
</option>
|
||||||
|
<option value="neutral">
|
||||||
|
Neutral (Frictionless / Standard)
|
||||||
|
</option>
|
||||||
|
<option value="negative">
|
||||||
|
Negative (Lower Score is Better / Demerits)
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div
|
<div
|
||||||
v-if="form.errors.alignment"
|
v-if="form.errors.alignment"
|
||||||
@ -106,8 +111,7 @@ function submit() {
|
|||||||
|
|
||||||
<!-- Media Uploads for Ledgers -->
|
<!-- Media Uploads for Ledgers -->
|
||||||
<div class="c-create-ledger-form__field">
|
<div class="c-create-ledger-form__field">
|
||||||
<label
|
<label class="c-create-ledger-form__label"
|
||||||
class="c-create-ledger-form__label"
|
|
||||||
>Attach Cover/Rules Media</label
|
>Attach Cover/Rules Media</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -126,9 +130,10 @@ function submit() {
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="c-create-ledger-form__media-preview-item"
|
class="c-create-ledger-form__media-preview-item"
|
||||||
>
|
>
|
||||||
<span class="c-create-ledger-form__media-preview-name">{{
|
<span
|
||||||
file.name
|
class="c-create-ledger-form__media-preview-name"
|
||||||
}}</span>
|
>{{ file.name }}</span
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="removeLedgerFile(index)"
|
@click="removeLedgerFile(index)"
|
||||||
|
|||||||
@ -17,9 +17,7 @@ defineProps<{
|
|||||||
<template>
|
<template>
|
||||||
<div class="c-ledger-list">
|
<div class="c-ledger-list">
|
||||||
<div class="c-ledger-list__header">
|
<div class="c-ledger-list__header">
|
||||||
<h4 class="c-ledger-list__title">
|
<h4 class="c-ledger-list__title">Ledgers</h4>
|
||||||
Ledgers
|
|
||||||
</h4>
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="c-ledger-list__grid">
|
<ul class="c-ledger-list__grid">
|
||||||
<li
|
<li
|
||||||
@ -131,19 +129,19 @@ defineProps<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-ledger-list__alignment-badge {
|
.c-ledger-list__alignment-badge {
|
||||||
@apply text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded;
|
@apply rounded px-1.5 py-0.5 text-[10px] font-semibold tracking-wide uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-ledger-list__alignment-badge--positive {
|
.c-ledger-list__alignment-badge--positive {
|
||||||
@apply text-green-600 bg-green-50/50 dark:text-green-400 dark:bg-green-950/20;
|
@apply bg-green-50/50 text-green-600 dark:bg-green-950/20 dark:text-green-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-ledger-list__alignment-badge--negative {
|
.c-ledger-list__alignment-badge--negative {
|
||||||
@apply text-red-600 bg-red-50/50 dark:text-red-400 dark:bg-red-950/20;
|
@apply bg-red-50/50 text-red-600 dark:bg-red-950/20 dark:text-red-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-ledger-list__alignment-badge--neutral {
|
.c-ledger-list__alignment-badge--neutral {
|
||||||
@apply text-gray-500 bg-gray-50/50 dark:text-gray-400 dark:bg-neutral-800/20;
|
@apply bg-gray-50/50 text-gray-500 dark:bg-neutral-800/20 dark:text-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-ledger-list__media-list {
|
.c-ledger-list__media-list {
|
||||||
|
|||||||
@ -50,12 +50,16 @@ function getAmountClass(amount: number): string {
|
|||||||
const alignment = props.ledgerAlignment || 'neutral';
|
const alignment = props.ledgerAlignment || 'neutral';
|
||||||
|
|
||||||
if (alignment === 'positive') {
|
if (alignment === 'positive') {
|
||||||
return amount > 0 ? 'c-mutation-list__item-amount--positive' : 'c-mutation-list__item-amount--negative';
|
return amount > 0
|
||||||
|
? 'c-mutation-list__item-amount--positive'
|
||||||
|
: 'c-mutation-list__item-amount--negative';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alignment === 'negative') {
|
if (alignment === 'negative') {
|
||||||
// Lower is better: negative amount is positive/favorable, positive amount is negative/unfavorable
|
// Lower is better: negative amount is positive/favorable, positive amount is negative/unfavorable
|
||||||
return amount < 0 ? 'c-mutation-list__item-amount--positive' : 'c-mutation-list__item-amount--negative';
|
return amount < 0
|
||||||
|
? 'c-mutation-list__item-amount--positive'
|
||||||
|
: 'c-mutation-list__item-amount--negative';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neutral alignment
|
// Neutral alignment
|
||||||
@ -65,9 +69,7 @@ function getAmountClass(amount: number): string {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="c-mutation-list">
|
<div class="c-mutation-list">
|
||||||
<h4 class="c-mutation-list__title">
|
<h4 class="c-mutation-list__title">Mutations</h4>
|
||||||
Mutations
|
|
||||||
</h4>
|
|
||||||
<ul class="c-mutation-list__list">
|
<ul class="c-mutation-list__list">
|
||||||
<li
|
<li
|
||||||
v-for="mutation in mutations"
|
v-for="mutation in mutations"
|
||||||
|
|||||||
@ -9,9 +9,7 @@ defineProps<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="c-participants-list">
|
<div class="c-participants-list">
|
||||||
<h4 class="c-participants-list__title">
|
<h4 class="c-participants-list__title">Participants</h4>
|
||||||
Participants
|
|
||||||
</h4>
|
|
||||||
<ul class="c-participants-list__grid">
|
<ul class="c-participants-list__grid">
|
||||||
<li
|
<li
|
||||||
v-for="participant in participants"
|
v-for="participant in participants"
|
||||||
|
|||||||
@ -62,7 +62,7 @@ function formatTime(isoString: string): string {
|
|||||||
'c-dashboard__badge-type',
|
'c-dashboard__badge-type',
|
||||||
entity.type === 'Dynamic'
|
entity.type === 'Dynamic'
|
||||||
? 'c-dashboard__badge-type--dynamic'
|
? 'c-dashboard__badge-type--dynamic'
|
||||||
: 'c-dashboard__badge-type--ledger'
|
: 'c-dashboard__badge-type--ledger',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ entity.type }}
|
{{ entity.type }}
|
||||||
@ -71,7 +71,10 @@ function formatTime(isoString: string): string {
|
|||||||
{{ entity.unread_count }} New
|
{{ entity.unread_count }} New
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Link :href="entity.url" class="c-dashboard__entity-link">
|
<Link
|
||||||
|
:href="entity.url"
|
||||||
|
class="c-dashboard__entity-link"
|
||||||
|
>
|
||||||
<h3 class="c-dashboard__entity-title">
|
<h3 class="c-dashboard__entity-title">
|
||||||
{{ entity.name }}
|
{{ entity.name }}
|
||||||
</h3>
|
</h3>
|
||||||
@ -103,7 +106,9 @@ function formatTime(isoString: string): string {
|
|||||||
v-if="entity.context_activities.length > 0"
|
v-if="entity.context_activities.length > 0"
|
||||||
class="c-dashboard__divider"
|
class="c-dashboard__divider"
|
||||||
>
|
>
|
||||||
<span class="c-dashboard__divider-text">New Activity Below</span>
|
<span class="c-dashboard__divider-text"
|
||||||
|
>New Activity Below</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New / Unread Activities -->
|
<!-- New / Unread Activities -->
|
||||||
@ -131,9 +136,7 @@ function formatTime(isoString: string): string {
|
|||||||
|
|
||||||
<!-- Empty Caught-Up State -->
|
<!-- Empty Caught-Up State -->
|
||||||
<div v-else class="c-dashboard__empty-state">
|
<div v-else class="c-dashboard__empty-state">
|
||||||
<div class="c-dashboard__empty-icon">
|
<div class="c-dashboard__empty-icon">🔒</div>
|
||||||
🔒
|
|
||||||
</div>
|
|
||||||
<p class="c-dashboard__empty-text">
|
<p class="c-dashboard__empty-text">
|
||||||
All chambers are currently quiet.
|
All chambers are currently quiet.
|
||||||
</p>
|
</p>
|
||||||
@ -149,7 +152,7 @@ function formatTime(isoString: string): string {
|
|||||||
@reference "../../css/app.css";
|
@reference "../../css/app.css";
|
||||||
|
|
||||||
.c-dashboard {
|
.c-dashboard {
|
||||||
@apply py-8 px-4 sm:px-6 lg:px-8;
|
@apply px-4 py-8 sm:px-6 lg:px-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__container {
|
.c-dashboard__container {
|
||||||
@ -157,7 +160,7 @@ function formatTime(isoString: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__title {
|
.c-dashboard__title {
|
||||||
@apply text-xl font-bold tracking-tight text-neutral-900 dark:text-neutral-100 mb-6;
|
@apply mb-6 text-xl font-bold tracking-tight text-neutral-900 dark:text-neutral-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__grid {
|
.c-dashboard__grid {
|
||||||
@ -169,15 +172,15 @@ function formatTime(isoString: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__card-header {
|
.c-dashboard__card-header {
|
||||||
@apply p-6 border-b border-neutral-100 dark:border-neutral-800/30;
|
@apply border-b border-neutral-100 p-6 dark:border-neutral-800/30;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__entity-meta {
|
.c-dashboard__entity-meta {
|
||||||
@apply flex items-center gap-2 mb-2;
|
@apply mb-2 flex items-center gap-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__badge-type {
|
.c-dashboard__badge-type {
|
||||||
@apply text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded;
|
@apply rounded px-1.5 py-0.5 text-[10px] font-bold tracking-wider uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__badge-type--dynamic {
|
.c-dashboard__badge-type--dynamic {
|
||||||
@ -189,7 +192,7 @@ function formatTime(isoString: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__unread-count {
|
.c-dashboard__unread-count {
|
||||||
@apply text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded bg-red-100 text-red-800 dark:bg-red-950/20 dark:text-red-400;
|
@apply rounded bg-red-100 px-1.5 py-0.5 text-[10px] font-bold tracking-wider text-red-800 uppercase dark:bg-red-950/20 dark:text-red-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__entity-link {
|
.c-dashboard__entity-link {
|
||||||
@ -201,23 +204,23 @@ function formatTime(isoString: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__activity-list {
|
.c-dashboard__activity-list {
|
||||||
@apply flex-1 p-6 space-y-4;
|
@apply flex-1 space-y-4 p-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__activity-item {
|
.c-dashboard__activity-item {
|
||||||
@apply p-4 rounded-lg border;
|
@apply rounded-lg border p-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__activity-item--read {
|
.c-dashboard__activity-item--read {
|
||||||
@apply bg-neutral-50/50 border-neutral-200 opacity-60 dark:bg-neutral-950/20 dark:border-neutral-800/50;
|
@apply border-neutral-200 bg-neutral-50/50 opacity-60 dark:border-neutral-800/50 dark:bg-neutral-950/20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__activity-item--unread {
|
.c-dashboard__activity-item--unread {
|
||||||
@apply bg-red-50/10 border-red-200/50 dark:bg-red-950/5 dark:border-red-900/30;
|
@apply border-red-200/50 bg-red-50/10 dark:border-red-900/30 dark:bg-red-950/5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__activity-meta {
|
.c-dashboard__activity-meta {
|
||||||
@apply flex items-center gap-2 mb-1.5 text-xs;
|
@apply mb-1.5 flex items-center gap-2 text-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__activity-user {
|
.c-dashboard__activity-user {
|
||||||
@ -229,7 +232,7 @@ function formatTime(isoString: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__new-badge {
|
.c-dashboard__new-badge {
|
||||||
@apply ml-auto text-[9px] font-bold text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-950/30 px-1 py-0.5 rounded;
|
@apply ml-auto rounded bg-red-100 px-1 py-0.5 text-[9px] font-bold text-red-600 dark:bg-red-950/30 dark:text-red-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__activity-desc {
|
.c-dashboard__activity-desc {
|
||||||
@ -237,11 +240,11 @@ function formatTime(isoString: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__divider {
|
.c-dashboard__divider {
|
||||||
@apply relative flex items-center justify-center my-4;
|
@apply relative my-4 flex items-center justify-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__divider-text {
|
.c-dashboard__divider-text {
|
||||||
@apply bg-white dark:bg-neutral-900 px-3 text-[10px] font-bold uppercase tracking-widest text-neutral-400 dark:text-neutral-500 z-10;
|
@apply z-10 bg-white px-3 text-[10px] font-bold tracking-widest text-neutral-400 uppercase dark:bg-neutral-900 dark:text-neutral-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__divider::before {
|
.c-dashboard__divider::before {
|
||||||
@ -250,11 +253,11 @@ function formatTime(isoString: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__empty-state {
|
.c-dashboard__empty-state {
|
||||||
@apply flex flex-col items-center justify-center p-12 text-center rounded-2xl border border-dashed border-neutral-200 dark:border-neutral-800 min-h-[300px];
|
@apply flex min-h-[300px] flex-col items-center justify-center rounded-2xl border border-dashed border-neutral-200 p-12 text-center dark:border-neutral-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__empty-icon {
|
.c-dashboard__empty-icon {
|
||||||
@apply text-4xl mb-4;
|
@apply mb-4 text-4xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__empty-text {
|
.c-dashboard__empty-text {
|
||||||
@ -262,6 +265,6 @@ function formatTime(isoString: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-dashboard__empty-subtext {
|
.c-dashboard__empty-subtext {
|
||||||
@apply mt-1 text-sm text-neutral-500 dark:text-neutral-500 max-w-md;
|
@apply mt-1 max-w-md text-sm text-neutral-500 dark:text-neutral-500;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -30,13 +30,16 @@ function submit() {
|
|||||||
<div class="c-dynamics-create__container">
|
<div class="c-dynamics-create__container">
|
||||||
<div class="c-dynamics-create__card">
|
<div class="c-dynamics-create__card">
|
||||||
<div class="c-dynamics-create__body">
|
<div class="c-dynamics-create__body">
|
||||||
<h3 class="c-dynamics-create__title">Create a New Dynamic</h3>
|
<h3 class="c-dynamics-create__title">
|
||||||
|
Create a New Dynamic
|
||||||
|
</h3>
|
||||||
|
|
||||||
<form @submit.prevent="submit" class="c-dynamics-create__form">
|
<form
|
||||||
|
@submit.prevent="submit"
|
||||||
|
class="c-dynamics-create__form"
|
||||||
|
>
|
||||||
<div class="c-dynamics-create__field">
|
<div class="c-dynamics-create__field">
|
||||||
<label
|
<label for="name" class="c-dynamics-create__label"
|
||||||
for="name"
|
|
||||||
class="c-dynamics-create__label"
|
|
||||||
>Name</label
|
>Name</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -54,9 +57,7 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="c-dynamics-create__field">
|
<div class="c-dynamics-create__field">
|
||||||
<label
|
<label for="rules" class="c-dynamics-create__label"
|
||||||
for="rules"
|
|
||||||
class="c-dynamics-create__label"
|
|
||||||
>Rules</label
|
>Rules</label
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@ -51,7 +51,9 @@ const breadcrumbs = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p class="c-dynamics-index__empty">You don't have any dynamics yet.</p>
|
<p class="c-dynamics-index__empty">
|
||||||
|
You don't have any dynamics yet.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ const props = defineProps<{
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
score: number;
|
score: number;
|
||||||
|
alignment: string;
|
||||||
media?: Array<{ id: number; url: string; mime_type: string }>;
|
media?: Array<{ id: number; url: string; mime_type: string }>;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -188,7 +188,8 @@ function isOwnerUser(userId: number): boolean {
|
|||||||
v-else-if="ledger.alignment === 'negative'"
|
v-else-if="ledger.alignment === 'negative'"
|
||||||
class="c-ledger-show__alignment-badge c-ledger-show__alignment-badge--negative"
|
class="c-ledger-show__alignment-badge c-ledger-show__alignment-badge--negative"
|
||||||
>
|
>
|
||||||
▼ Negative Alignment — A lower score is better (demerits / infractions).
|
▼ Negative Alignment — A lower score is better
|
||||||
|
(demerits / infractions).
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
@ -254,10 +255,7 @@ function isOwnerUser(userId: number): boolean {
|
|||||||
class="c-ledger-show__lightbox"
|
class="c-ledger-show__lightbox"
|
||||||
@click="closeLightbox"
|
@click="closeLightbox"
|
||||||
>
|
>
|
||||||
<button
|
<button @click="closeLightbox" class="c-ledger-show__lightbox-close">
|
||||||
@click="closeLightbox"
|
|
||||||
class="c-ledger-show__lightbox-close"
|
|
||||||
>
|
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
<div class="c-ledger-show__lightbox-content" @click.stop>
|
<div class="c-ledger-show__lightbox-content" @click.stop>
|
||||||
@ -325,19 +323,19 @@ function isOwnerUser(userId: number): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-ledger-show__alignment-badge {
|
.c-ledger-show__alignment-badge {
|
||||||
@apply text-xs font-semibold uppercase tracking-wide px-2.5 py-1 rounded;
|
@apply rounded px-2.5 py-1 text-xs font-semibold tracking-wide uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-ledger-show__alignment-badge--positive {
|
.c-ledger-show__alignment-badge--positive {
|
||||||
@apply text-green-600 bg-green-50/50 dark:text-green-400 dark:bg-green-950/20;
|
@apply bg-green-50/50 text-green-600 dark:bg-green-950/20 dark:text-green-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-ledger-show__alignment-badge--negative {
|
.c-ledger-show__alignment-badge--negative {
|
||||||
@apply text-red-600 bg-red-50/50 dark:text-red-400 dark:bg-red-950/20;
|
@apply bg-red-50/50 text-red-600 dark:bg-red-950/20 dark:text-red-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-ledger-show__alignment-badge--neutral {
|
.c-ledger-show__alignment-badge--neutral {
|
||||||
@apply text-gray-500 bg-gray-50/50 dark:text-gray-400 dark:bg-neutral-800/20;
|
@apply bg-gray-50/50 text-gray-500 dark:bg-neutral-800/20 dark:text-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-ledger-show__media-list {
|
.c-ledger-show__media-list {
|
||||||
|
|||||||
39
tests/Browser/AuthenticationTest.php
Normal file
39
tests/Browser/AuthenticationTest.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Browser;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Laravel\Dusk\Browser;
|
||||||
|
use Tests\DuskTestCase;
|
||||||
|
|
||||||
|
test('user can register, log in, and log out', function () {
|
||||||
|
$this->browse(function (Browser $browser) {
|
||||||
|
// 1. Test Registration
|
||||||
|
$browser->visit('/register')
|
||||||
|
->waitForText('Create an account')
|
||||||
|
->type('name', 'New Browser User')
|
||||||
|
->type('email', 'newbrowseruser@example.com')
|
||||||
|
->type('password', 'password')
|
||||||
|
->type('password_confirmation', 'password')
|
||||||
|
->press('Create account')
|
||||||
|
->waitForLocation('/dashboard')
|
||||||
|
->assertPathIs('/dashboard')
|
||||||
|
->assertSee('New Browser User');
|
||||||
|
|
||||||
|
// 2. Test Logout
|
||||||
|
// Open the user menu (trigger button shows initials or user name)
|
||||||
|
$browser->click('button[aria-haspopup="menu"]')
|
||||||
|
->waitForText('Log out')
|
||||||
|
->clickLink('Log out')
|
||||||
|
->waitForLocation('/login') // Fortify logs out and redirects to login or home
|
||||||
|
->assertPathIs('/login');
|
||||||
|
|
||||||
|
// 3. Test Login
|
||||||
|
$browser->type('email', 'newbrowseruser@example.com')
|
||||||
|
->type('password', 'password')
|
||||||
|
->press('Log in')
|
||||||
|
->waitForLocation('/dashboard')
|
||||||
|
->assertPathIs('/dashboard')
|
||||||
|
->assertSee('New Browser User');
|
||||||
|
});
|
||||||
|
});
|
||||||
83
tests/Browser/AuthorizationTest.php
Normal file
83
tests/Browser/AuthorizationTest.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Browser;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Dynamic;
|
||||||
|
use App\Models\Ledger;
|
||||||
|
use App\Models\Mutation;
|
||||||
|
use Laravel\Dusk\Browser;
|
||||||
|
use Tests\DuskTestCase;
|
||||||
|
|
||||||
|
test('access control and actions are enforced for owners and participants', function () {
|
||||||
|
// Create database state
|
||||||
|
$owner = User::factory()->create([
|
||||||
|
'name' => 'Owner Alice',
|
||||||
|
'email' => 'alice-owner@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$participant = User::factory()->create([
|
||||||
|
'name' => 'Participant Bob',
|
||||||
|
'email' => 'bob-sub@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$outsider = User::factory()->create([
|
||||||
|
'name' => 'Outsider Charlie',
|
||||||
|
'email' => 'charlie-outsider@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$dynamic = Dynamic::create([
|
||||||
|
'name' => 'Private Club',
|
||||||
|
'rules' => 'Strict access control.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
|
||||||
|
$dynamic->participants()->attach($participant->id, ['role' => 'participant']);
|
||||||
|
|
||||||
|
$ledger = Ledger::create([
|
||||||
|
'dynamic_id' => $dynamic->id,
|
||||||
|
'name' => 'Rules Compliance',
|
||||||
|
'rules' => 'Score rules.',
|
||||||
|
'score' => 100,
|
||||||
|
'alignment' => 'neutral',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->browse(function (Browser $sessionOwner, Browser $sessionParticipant, Browser $sessionOutsider) use ($dynamic, $ledger, $owner, $participant, $outsider) {
|
||||||
|
|
||||||
|
// 1. Test Outsider trying to access dynamic they DO NOT belong to (should be forbidden / 403)
|
||||||
|
$sessionOutsider->loginAs($outsider)
|
||||||
|
->visit(route('dynamics.show', $dynamic))
|
||||||
|
->assertSee('403') // Laravel / Inertia forbidden page
|
||||||
|
->assertDontSee('Private Club');
|
||||||
|
|
||||||
|
// 2. Test Participant accessing dynamic they DO belong to (should be allowed)
|
||||||
|
$sessionParticipant->loginAs($participant)
|
||||||
|
->visit(route('dynamics.show', $dynamic))
|
||||||
|
->waitForText('Private Club')
|
||||||
|
->assertSee('Private Club')
|
||||||
|
->assertSee('Participant Bob');
|
||||||
|
|
||||||
|
// 3. Test Participant adding a mutation suggestion
|
||||||
|
$sessionParticipant->visit(route('dynamics.ledgers.show', [$dynamic, $ledger]))
|
||||||
|
->waitForText('Add Mutation')
|
||||||
|
->type('amount', '20')
|
||||||
|
->type('description', 'Cleaned the main room')
|
||||||
|
->press('Add Mutation')
|
||||||
|
->waitForText('PENDING')
|
||||||
|
->assertSee('PENDING') // Mutation should show up as pending
|
||||||
|
->assertDontSee('Approve'); // Standard participant should NOT see approve button!
|
||||||
|
|
||||||
|
// 4. Test Owner logging in, seeing the pending suggestion, and approving it!
|
||||||
|
$sessionOwner->loginAs($owner)
|
||||||
|
->visit(route('dynamics.ledgers.show', [$dynamic, $ledger]))
|
||||||
|
->waitForText('Cleaned the main room')
|
||||||
|
->assertSee('PENDING')
|
||||||
|
->assertSee('Approve') // Owner should see the Approve button!
|
||||||
|
->press('Approve')
|
||||||
|
->waitForText('Score: 120') // Score updated from 100 to 120 after approval!
|
||||||
|
->assertDontSee('PENDING'); // No longer pending!
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user