mirror of
https://github.com/whekin/household-bot.git
synced 2026-04-01 00:04:02 +00:00
feat: add quick payment action and improve copy button UX
Mini App Home Screen: - Add 'Record Payment' button to utilities and rent period cards - Pre-fill payment amount with member's share (rentShare/utilityShare) - Modal dialog with amount input and currency display - Toast notifications for copy and payment success/failure feedback Copy Button Improvements: - Increase spacing between icon and text (4px → 8px) - Add hover background and padding for better touch target - Green background highlight when copied (in addition to icon color change) - Toast notification appears when copying any value Backend: - Add /api/miniapp/payments/add endpoint for quick payments - Payment notifications sent to 'reminders' topic in Telegram - Include member name, payment type, amount, and period in notification Files: - New: apps/miniapp/src/components/ui/toast.tsx - Modified: apps/miniapp/src/routes/home.tsx, apps/miniapp/src/index.css, apps/miniapp/src/theme.css, apps/miniapp/src/i18n.ts, apps/bot/src/miniapp-billing.ts, apps/bot/src/server.ts Quality Gates: ✅ format, lint, typecheck, build, test Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -9,11 +9,22 @@ import { NavigationTabs } from './navigation-tabs'
|
||||
import { Badge } from '../ui/badge'
|
||||
import { Button, IconButton } from '../ui/button'
|
||||
import { Modal } from '../ui/dialog'
|
||||
import { Field } from '../ui/field'
|
||||
import { Input } from '../ui/input'
|
||||
|
||||
export function AppShell(props: ParentProps) {
|
||||
const { readySession } = useSession()
|
||||
const { copy, locale, setLocale } = useI18n()
|
||||
const { effectiveIsAdmin, testingRolePreview, setTestingRolePreview } = useDashboard()
|
||||
const {
|
||||
dashboard,
|
||||
effectiveIsAdmin,
|
||||
testingRolePreview,
|
||||
setTestingRolePreview,
|
||||
testingPeriodOverride,
|
||||
setTestingPeriodOverride,
|
||||
testingTodayOverride,
|
||||
setTestingTodayOverride
|
||||
} = useDashboard()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [testingSurfaceOpen, setTestingSurfaceOpen] = createSignal(false)
|
||||
@@ -157,6 +168,43 @@ export function AppShell(props: ParentProps) {
|
||||
{copy().testingPreviewResidentAction ?? ''}
|
||||
</Button>
|
||||
</div>
|
||||
<article class="testing-card__section">
|
||||
<span>{copy().testingPeriodCurrentLabel ?? ''}</span>
|
||||
<strong>{dashboard()?.period ?? '—'}</strong>
|
||||
</article>
|
||||
<div class="testing-card__actions" style={{ 'flex-direction': 'column', gap: '12px' }}>
|
||||
<Field label={copy().testingPeriodOverrideLabel ?? ''} wide>
|
||||
<Input
|
||||
placeholder={copy().testingPeriodOverridePlaceholder ?? ''}
|
||||
value={testingPeriodOverride() ?? ''}
|
||||
onInput={(e) => {
|
||||
const next = e.currentTarget.value.trim()
|
||||
setTestingPeriodOverride(next.length > 0 ? next : null)
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={copy().testingTodayOverrideLabel ?? ''} wide>
|
||||
<Input
|
||||
placeholder={copy().testingTodayOverridePlaceholder ?? ''}
|
||||
value={testingTodayOverride() ?? ''}
|
||||
onInput={(e) => {
|
||||
const next = e.currentTarget.value.trim()
|
||||
setTestingTodayOverride(next.length > 0 ? next : null)
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<div class="modal-action-row">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setTestingPeriodOverride(null)
|
||||
setTestingTodayOverride(null)
|
||||
}}
|
||||
>
|
||||
{copy().testingClearOverridesAction ?? ''}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</main>
|
||||
|
||||
50
apps/miniapp/src/components/ui/toast.tsx
Normal file
50
apps/miniapp/src/components/ui/toast.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Show, createEffect, onCleanup } from 'solid-js'
|
||||
|
||||
import { cn } from '../../lib/cn'
|
||||
|
||||
export interface ToastOptions {
|
||||
message: string
|
||||
type?: 'success' | 'info' | 'error'
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export interface ToastState {
|
||||
visible: boolean
|
||||
message: string
|
||||
type: 'success' | 'info' | 'error'
|
||||
}
|
||||
|
||||
const toastVariants = {
|
||||
success: 'toast--success',
|
||||
info: 'toast--info',
|
||||
error: 'toast--error'
|
||||
}
|
||||
|
||||
export function Toast(props: { state: ToastState; onClose: () => void }) {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
createEffect(() => {
|
||||
if (props.state.visible) {
|
||||
timeoutId = setTimeout(
|
||||
() => {
|
||||
props.onClose()
|
||||
},
|
||||
props.state.type === 'error' ? 4000 : 2000
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Show when={props.state.visible}>
|
||||
<div role="status" aria-live="polite" class={cn('toast', toastVariants[props.state.type])}>
|
||||
<span class="toast__message">{props.state.message}</span>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user