@tuturuuu/ui 0.30.2 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/biome.json +1 -1
  3. package/package.json +95 -49
  4. package/src/components/ui/calendar-app/calendar-client-page.test.tsx +57 -0
  5. package/src/components/ui/calendar-app/calendar-client-page.tsx +13 -4
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +7 -1
  7. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +47 -39
  8. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +14 -0
  9. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +4 -0
  10. package/src/components/ui/calendar-app/components/calendar-sync-attention-button.tsx +47 -0
  11. package/src/components/ui/calendar-app/components/calendar-sync-recovery.test.tsx +112 -0
  12. package/src/components/ui/calendar-app/components/calendar-sync-recovery.tsx +206 -0
  13. package/src/components/ui/calendar-app/components/calendar-types.ts +1 -1
  14. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +86 -90
  15. package/src/components/ui/custom/language-dropdown-item.tsx +6 -14
  16. package/src/components/ui/custom/nav-link.tsx +7 -7
  17. package/src/components/ui/custom/nav.tsx +15 -63
  18. package/src/components/ui/custom/notification-card.tsx +361 -0
  19. package/src/components/ui/custom/notification-list.tsx +178 -0
  20. package/src/components/ui/custom/notification-popover-client.tsx +12 -780
  21. package/src/components/ui/custom/notification-runtime.tsx +23 -0
  22. package/src/components/ui/custom/onboarding-settings-panel.tsx +8 -55
  23. package/src/components/ui/custom/satellite-account-switcher-menu.tsx +212 -0
  24. package/src/components/ui/custom/satellite-brand.tsx +122 -0
  25. package/src/components/ui/custom/satellite-content.tsx +148 -0
  26. package/src/components/ui/custom/satellite-footer-actions.tsx +350 -0
  27. package/src/components/ui/custom/satellite-language-item.tsx +26 -0
  28. package/src/components/ui/custom/satellite-navigation.tsx +86 -0
  29. package/src/components/ui/custom/satellite-notification-popover.tsx +272 -0
  30. package/src/components/ui/custom/satellite-onboarding-settings-panel.tsx +80 -0
  31. package/src/components/ui/custom/satellite-settings-dialog-shell.tsx +621 -0
  32. package/src/components/ui/custom/satellite-shell-utils.ts +163 -0
  33. package/src/components/ui/custom/satellite-shell.test.tsx +169 -0
  34. package/src/components/ui/custom/satellite-shell.tsx +50 -0
  35. package/src/components/ui/custom/satellite-theme-dropdown-items.tsx +92 -0
  36. package/src/components/ui/custom/satellite-user-menu-items.tsx +190 -0
  37. package/src/components/ui/custom/satellite-user-menu.tsx +93 -0
  38. package/src/components/ui/custom/satellite-workspace-section.tsx +46 -0
  39. package/src/components/ui/custom/settings/keyboard-shortcuts-settings.tsx +16 -0
  40. package/src/components/ui/custom/settings-dialog-shell.tsx +21 -600
  41. package/src/components/ui/custom/sidebar-context.tsx +5 -14
  42. package/src/components/ui/custom/sidebar-footer-actions.tsx +19 -322
  43. package/src/components/ui/custom/structure.tsx +14 -0
  44. package/src/components/ui/custom/system-language-dropdown-item.tsx +7 -14
  45. package/src/components/ui/custom/theme-dropdown-items.tsx +2 -85
  46. package/src/components/ui/custom/use-satellite-shell.tsx +165 -0
  47. package/src/components/ui/legacy/calendar/agenda-view.tsx +139 -47
  48. package/src/components/ui/legacy/calendar/calendar-content.test.tsx +116 -0
  49. package/src/components/ui/legacy/calendar/calendar-content.tsx +38 -74
  50. package/src/components/ui/legacy/calendar/calendar-event-tone.ts +20 -0
  51. package/src/components/ui/legacy/calendar/calendar-meeting-link.test.ts +27 -0
  52. package/src/components/ui/legacy/calendar/calendar-meeting-link.ts +34 -0
  53. package/src/components/ui/legacy/calendar/calendar-period.ts +28 -0
  54. package/src/components/ui/legacy/calendar/calendar-shortcuts.test.tsx +107 -0
  55. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +169 -0
  56. package/src/components/ui/legacy/calendar/event-modal-header.tsx +52 -0
  57. package/src/components/ui/legacy/calendar/event-modal.tsx +107 -156
  58. package/src/components/ui/legacy/calendar/month-calendar.tsx +242 -933
  59. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +25 -0
  60. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +32 -0
  61. package/src/components/ui/legacy/calendar/smart-calendar.tsx +3 -0
  62. package/src/components/ui/legacy/calendar/use-calendar-view-shortcuts.ts +54 -0
  63. package/src/components/ui/legacy/calendar/use-event-draft-session.test.tsx +59 -0
  64. package/src/components/ui/legacy/calendar/use-event-draft-session.ts +20 -0
  65. package/src/components/ui/legacy/calendar/year-calendar.tsx +34 -16
  66. package/src/components/ui/pwa-status.test.tsx +53 -0
  67. package/src/components/ui/pwa-status.tsx +113 -0
  68. package/src/components/ui/report-problem-dialog-content.tsx +350 -0
  69. package/src/components/ui/report-problem-dialog.tsx +6 -975
  70. package/src/components/ui/report-problem-model.ts +133 -0
  71. package/src/components/ui/report-problem-mutation.ts +122 -0
  72. package/src/components/ui/save-notification.test.tsx +36 -0
  73. package/src/components/ui/save-notification.tsx +17 -0
  74. package/src/components/ui/use-report-problem.ts +488 -0
  75. package/src/declarations.d.ts +2 -0
  76. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +46 -4
  77. package/src/hooks/__tests__/use-calendar-sync.test.tsx +24 -0
  78. package/src/hooks/calendar-date-utils.ts +11 -0
  79. package/src/hooks/calendar-event-lookup.test.ts +77 -0
  80. package/src/hooks/calendar-event-lookup.ts +47 -0
  81. package/src/hooks/calendar-range-cache.ts +52 -0
  82. package/src/hooks/pwa-install-prompt.ts +44 -0
  83. package/src/hooks/use-calendar-sync.tsx +23 -100
  84. package/src/hooks/use-calendar.tsx +21 -77
  85. package/src/hooks/use-notifications.ts +9 -24
  86. package/src/hooks/use-open-initial-calendar-event.ts +27 -0
  87. package/src/hooks/use-settings-dialog-shortcut.test.tsx +41 -0
  88. package/src/hooks/use-settings-dialog-shortcut.ts +12 -14
  89. package/src/hooks/use-workspace-presence.ts +0 -18
  90. package/src/lib/tasks-app-url.test.ts +25 -0
  91. package/src/lib/tasks-app-url.ts +8 -0
@@ -0,0 +1,133 @@
1
+ import type { InternalApiClientOptions } from '@tuturuuu/internal-api/client';
2
+ import type { Product, SupportType } from '@tuturuuu/types';
3
+ import type React from 'react';
4
+ import { z } from 'zod';
5
+ export interface ReportProblemFormData {
6
+ product: Product | '';
7
+ type: SupportType | '';
8
+ suggestion: string;
9
+ media: File[];
10
+ }
11
+
12
+ export const MAX_MEDIA_SIZE = 5 * 1024 * 1024; // 5MB
13
+ export const MAX_FILES = 5;
14
+
15
+ // Allowed media types
16
+ export const ALLOWED_IMAGE_TYPES = [
17
+ 'image/png',
18
+ 'image/jpeg',
19
+ 'image/webp',
20
+ 'image/gif',
21
+ ];
22
+ export const ALLOWED_VIDEO_TYPES = [
23
+ 'video/mp4',
24
+ 'video/webm',
25
+ 'video/quicktime',
26
+ ];
27
+
28
+ // Zod schema for form validation
29
+ export const reportProblemSchema = z.object({
30
+ product: z.enum(
31
+ [
32
+ 'web',
33
+ 'nova',
34
+ 'rewise',
35
+ 'calendar',
36
+ 'finance',
37
+ 'tudo',
38
+ 'tumeet',
39
+ 'shortener',
40
+ 'qr',
41
+ 'drive',
42
+ 'mail',
43
+ 'other',
44
+ ],
45
+ {
46
+ message: 'Please select a product',
47
+ }
48
+ ),
49
+ type: z.enum(['bug', 'feature-request'], {
50
+ message: 'Please select a support type',
51
+ }),
52
+ suggestion: z
53
+ .string()
54
+ .trim()
55
+ .min(1, 'Please describe the issue or suggestion')
56
+ .max(1000, 'Suggestion must be at most 1000 characters'),
57
+ media: z
58
+ .array(z.instanceof(File))
59
+ .max(MAX_FILES, `You can upload up to ${MAX_FILES} files`)
60
+ .refine(
61
+ (files) =>
62
+ files.every(
63
+ (f) =>
64
+ ALLOWED_IMAGE_TYPES.includes(f.type) ||
65
+ ALLOWED_VIDEO_TYPES.includes(f.type)
66
+ ),
67
+ {
68
+ message:
69
+ 'Only image (PNG, JPEG, WebP, GIF) and video (MP4, WebM, MOV) files are allowed',
70
+ }
71
+ )
72
+ .refine((files) => files.every((f) => f.size <= MAX_MEDIA_SIZE), {
73
+ message: 'Each file must be 5MB or less',
74
+ }),
75
+ });
76
+
77
+ export interface ReportProblemDialogProps {
78
+ ImageComponent?: React.ComponentType<{
79
+ src: string;
80
+ alt: string;
81
+ className: string;
82
+ width: number;
83
+ height: number;
84
+ }>;
85
+ t: (
86
+ key:
87
+ | 'affected-product-required'
88
+ | 'cancel'
89
+ | 'media-alt'
90
+ | 'media-optional'
91
+ | 'report-problem'
92
+ | 'report-problem-description'
93
+ | 'report-submitted-success'
94
+ | 'select-product-placeholder'
95
+ | 'submit-report'
96
+ | 'submitting'
97
+ | 'suggestion-improve'
98
+ | 'suggestion-placeholder',
99
+ values?: Record<string, string | number>
100
+ ) => string;
101
+ apiOptions?: InternalApiClientOptions;
102
+ open?: boolean;
103
+ onOpenChange?: (open: boolean) => void;
104
+ products?: Array<{ value: Product; label: string }>;
105
+ className?: string;
106
+ trigger?: React.ReactNode;
107
+ showTrigger?: boolean;
108
+ }
109
+
110
+ export const DEFAULT_PRODUCTS: Array<{ value: Product; label: string }> = [
111
+ { value: 'web', label: 'Web Dashboard' },
112
+ { value: 'nova', label: 'Nova' },
113
+ { value: 'rewise', label: 'Rewise' },
114
+ { value: 'calendar', label: 'Calendar' },
115
+ { value: 'finance', label: 'Finance' },
116
+ { value: 'tudo', label: 'Tuturuuu Tasks' },
117
+ { value: 'tumeet', label: 'Tuturuuu Meet' },
118
+ { value: 'shortener', label: 'URL Shortener' },
119
+ { value: 'qr', label: 'QR Code' },
120
+ { value: 'drive', label: 'Drive' },
121
+ { value: 'mail', label: 'Mail' },
122
+ { value: 'other', label: 'Other' },
123
+ ];
124
+
125
+ export const DEFAULT_SUPPORT_TYPES: Array<{
126
+ value: SupportType;
127
+ label: string;
128
+ }> = [
129
+ { value: 'bug', label: 'Bug Report' },
130
+ { value: 'feature-request', label: 'Feature Request' },
131
+ ];
132
+
133
+ export const LOCAL_STORAGE_KEY = 'report-problem-form-data';
@@ -0,0 +1,122 @@
1
+ import { useMutation } from '@tanstack/react-query';
2
+ import type { InternalApiClientOptions } from '@tuturuuu/internal-api/client';
3
+ import {
4
+ createReportUploadUrls,
5
+ deleteReportUploadPaths,
6
+ submitReport,
7
+ } from '@tuturuuu/internal-api/reports';
8
+ import type { Product, SupportType } from '@tuturuuu/types';
9
+
10
+ interface SubmitReportMutationInput {
11
+ product: Product;
12
+ type: SupportType;
13
+ suggestion: string;
14
+ subject: string;
15
+ media: File[];
16
+ }
17
+
18
+ export function useSubmitReportMutation(apiOptions?: InternalApiClientOptions) {
19
+ return useMutation({
20
+ mutationFn: async ({
21
+ product,
22
+ type,
23
+ suggestion,
24
+ subject,
25
+ media,
26
+ }: SubmitReportMutationInput) => {
27
+ const uploadedPaths: string[] = [];
28
+
29
+ const cleanupUploadedMedia = async () => {
30
+ if (uploadedPaths.length === 0) {
31
+ return;
32
+ }
33
+
34
+ try {
35
+ await deleteReportUploadPaths({ paths: uploadedPaths }, apiOptions);
36
+ } catch (cleanupError) {
37
+ console.error(
38
+ 'Failed to clean up uploaded support report media:',
39
+ cleanupError
40
+ );
41
+ }
42
+ };
43
+
44
+ try {
45
+ if (media.length > 0) {
46
+ const { uploads } = await createReportUploadUrls(
47
+ {
48
+ files: media.map((file) => ({
49
+ filename: file.name,
50
+ contentType: file.type,
51
+ size: file.size,
52
+ })),
53
+ },
54
+ apiOptions
55
+ );
56
+
57
+ if (uploads.length !== media.length) {
58
+ throw new Error('Failed to prepare media uploads');
59
+ }
60
+
61
+ for (const [index, upload] of uploads.entries()) {
62
+ const file = media[index];
63
+
64
+ if (!file) {
65
+ throw new Error('Missing media file during upload');
66
+ }
67
+
68
+ let uploadResponse = await fetch(upload.signedUrl, {
69
+ method: 'PUT',
70
+ cache: 'no-store',
71
+ headers: {
72
+ Authorization: `Bearer ${upload.token}`,
73
+ 'Content-Type': file.type || 'application/octet-stream',
74
+ },
75
+ body: file,
76
+ });
77
+
78
+ if (!uploadResponse.ok) {
79
+ uploadResponse = await fetch(upload.signedUrl, {
80
+ method: 'PUT',
81
+ cache: 'no-store',
82
+ headers: {
83
+ Authorization: `Bearer ${upload.token}`,
84
+ },
85
+ body: file,
86
+ });
87
+ }
88
+
89
+ if (!uploadResponse.ok) {
90
+ const text = await uploadResponse.text().catch(() => '');
91
+ throw new Error(
92
+ `Failed to upload media (${uploadResponse.status})${text ? `: ${text}` : ''}`
93
+ );
94
+ }
95
+
96
+ uploadedPaths.push(upload.path);
97
+ }
98
+ }
99
+
100
+ const result = await submitReport(
101
+ {
102
+ product,
103
+ type,
104
+ suggestion,
105
+ subject,
106
+ imagePaths: uploadedPaths,
107
+ },
108
+ apiOptions
109
+ );
110
+
111
+ if (!result.success) {
112
+ throw new Error(result.message || 'Failed to submit report');
113
+ }
114
+
115
+ return result;
116
+ } catch (error) {
117
+ await cleanupUploadedMedia();
118
+ throw error;
119
+ }
120
+ },
121
+ });
122
+ }
@@ -0,0 +1,36 @@
1
+ import { act, render, screen } from '@testing-library/react';
2
+ import { afterEach, expect, it, vi } from 'vitest';
3
+ import { notifySave } from './save-notification';
4
+ import { Toaster, toast } from './sonner';
5
+
6
+ afterEach(() => {
7
+ toast.dismiss();
8
+ vi.unstubAllGlobals();
9
+ });
10
+
11
+ it('renders persistent save failures in the toaster used by app layouts', async () => {
12
+ vi.stubGlobal(
13
+ 'matchMedia',
14
+ vi.fn(() => ({
15
+ matches: false,
16
+ addEventListener: vi.fn(),
17
+ removeEventListener: vi.fn(),
18
+ }))
19
+ );
20
+ render(<Toaster />);
21
+ act(() => {
22
+ notifySave({
23
+ title: 'Save failed',
24
+ description: 'Your draft is preserved. Try again.',
25
+ variant: 'destructive',
26
+ duration: Number.POSITIVE_INFINITY,
27
+ });
28
+ });
29
+ expect(await screen.findByText('Save failed')).toBeInTheDocument();
30
+ expect(
31
+ screen.getByText('Your draft is preserved. Try again.')
32
+ ).toBeInTheDocument();
33
+ expect(
34
+ screen.getByRole('button', { name: 'Close toast' })
35
+ ).toBeInTheDocument();
36
+ });
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { toast } from './sonner';
5
+
6
+ interface SaveNotification {
7
+ title: ReactNode;
8
+ description?: ReactNode;
9
+ variant?: 'default' | 'destructive';
10
+ duration?: number;
11
+ }
12
+
13
+ /** Save feedback must use the Sonner toaster mounted by the application shells. */
14
+ export function notifySave({ title, variant, ...options }: SaveNotification) {
15
+ const notify = variant === 'destructive' ? toast.error : toast.success;
16
+ return notify(title, { ...options, closeButton: true });
17
+ }