@terrafirmagreg-team/discussions 1.2.1 → 1.2.3

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.
@@ -1,181 +1,181 @@
1
- 'use client';
2
- import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
3
- import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
4
- import { ThumbsDown, ThumbsUp } from 'lucide-react';
5
- import {
6
- type SyntheticEvent,
7
- useState,
8
- useTransition,
9
- } from 'react';
10
- import { Collapsible, CollapsibleContent } from '@terrafirmagreg-team/ui/components/ui/collapsible';
11
- import { cva } from 'class-variance-authority';
12
- import {
13
- actionResponse,
14
- pageFeedback,
15
- type ActionResponse,
16
- type PageFeedback,
17
- } from '../../lib/feedback-schema';
18
- import { z } from 'zod/mini';
19
- import { usePathname } from 'next/navigation';
20
- import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
21
- import { useSubmissionStorage } from '../../hooks/feedback/use-submission-storage';
22
-
23
- const rateButtonVariants = cva(
24
- 'inline-flex items-center gap-2 px-3 py-2 rounded-full font-medium border text-sm [&_svg]:size-4 disabled:cursor-not-allowed',
25
- {
26
- variants: {
27
- active: {
28
- true: 'bg-fd-accent text-fd-accent-foreground [&_svg]:fill-current',
29
- false: 'text-fd-muted-foreground',
30
- },
31
- },
32
- },
33
- );
34
-
35
- const pageFeedbackResult = z.extend(pageFeedback, {
36
- response: actionResponse,
37
- });
38
-
39
- export function Feedback({
40
- onSendAction,
41
- }: {
42
- onSendAction: (feedback: PageFeedback) => Promise<ActionResponse>;
43
- }) {
44
- const t = useTranslations();
45
- const pathname = usePathname();
46
- const { previous, setPrevious } = useSubmissionStorage(pathname, (v) => {
47
- const result = pageFeedbackResult.safeParse(v);
48
- return result.success ? result.data : null;
49
- });
50
- const [opinion, setOpinion] = useState<'good' | 'bad' | null>(null);
51
- const [message, setMessage] = useState('');
52
- const [isPending, startTransition] = useTransition();
53
-
54
- function submit(e?: SyntheticEvent) {
55
- if (opinion == null) return;
56
-
57
- startTransition(async () => {
58
- const feedback: PageFeedback = {
59
- url: location.href,
60
- opinion: opinion!,
61
- message,
62
- };
63
-
64
- const response = await onSendAction(feedback);
65
- setPrevious({
66
- response,
67
- ...feedback,
68
- });
69
- setMessage('');
70
- setOpinion(null);
71
- });
72
-
73
- e?.preventDefault();
74
- }
75
-
76
- const activeOpinion = previous?.opinion ?? opinion;
77
-
78
- return (
79
- <Collapsible
80
- open={opinion !== null || previous !== null}
81
- onOpenChange={(v) => {
82
- if (!v) setOpinion(null);
83
- }}
84
- className="border-y py-3"
85
- >
86
- <div className="flex flex-row items-center gap-2">
87
- <p className="text-sm font-medium pe-2">{t('How is this page?', { note: 'ui.feedback.question' })}</p>
88
- <button
89
- disabled={previous !== null}
90
- className={cn(
91
- rateButtonVariants({
92
- active: activeOpinion === 'good',
93
- }),
94
- )}
95
- onClick={() => {
96
- setOpinion('good');
97
- }}
98
- >
99
- <ThumbsUp />
100
- {t('Good', { note: 'ui.feedback.good' })}
101
- </button>
102
- <button
103
- disabled={previous !== null}
104
- className={cn(
105
- rateButtonVariants({
106
- active: activeOpinion === 'bad',
107
- }),
108
- )}
109
- onClick={() => {
110
- setOpinion('bad');
111
- }}
112
- >
113
- <ThumbsDown />
114
- {t('Bad', { note: 'ui.feedback.bad' })}
115
- </button>
116
- </div>
117
- <CollapsibleContent className="mt-3">
118
- {previous ? (
119
- <div className="px-3 py-6 flex flex-col items-center gap-3 bg-fd-card text-fd-muted-foreground text-sm text-center rounded-xl">
120
- <p>{t('Thank you for your feedback!', { note: 'ui.feedback.thanks' })}</p>
121
- <div className="flex flex-row items-center gap-2">
122
- <a
123
- href={previous.response?.githubUrl}
124
- rel="noreferrer noopener"
125
- target="_blank"
126
- className={cn(
127
- buttonVariants({
128
- variant: 'default',
129
- }),
130
- 'text-xs',
131
- )}
132
- >
133
- {t('View on GitHub', { note: 'ui.feedback.view_on_github' })}
134
- </a>
135
-
136
- <button
137
- className={cn(
138
- buttonVariants({
139
- variant: 'secondary',
140
- }),
141
- 'text-xs',
142
- )}
143
- onClick={() => {
144
- setOpinion(previous.opinion);
145
- setPrevious(null);
146
- }}
147
- >
148
- {t('Submit Again', { note: 'ui.feedback.submit_again' })}
149
- </button>
150
- </div>
151
- </div>
152
- ) : (
153
- <form className="flex flex-col gap-3" onSubmit={submit}>
154
- <textarea
155
- autoFocus
156
- required
157
- value={message}
158
- onChange={(e) => setMessage(e.target.value)}
159
- className="border rounded-lg bg-fd-secondary text-fd-secondary-foreground p-3 resize-none focus-visible:outline-none placeholder:text-fd-muted-foreground"
160
- placeholder={t('Leave your feedback…', { note: 'ui.feedback.placeholder' })}
161
- onKeyDown={(e) => {
162
- if (e.nativeEvent.isComposing || e.keyCode === 229) return;
163
- if (!e.shiftKey && e.key === 'Enter') {
164
- submit(e);
165
- }
166
- }}
167
- />
168
- <button
169
- type="submit"
170
- className={cn(buttonVariants({ variant: 'outline' }), 'w-fit px-3')}
171
- disabled={isPending}
172
- >
173
- {t('Submit', { note: 'ui.feedback.submit' })}
174
- </button>
175
- </form>
176
- )}
177
- </CollapsibleContent>
178
- </Collapsible>
179
- );
180
- }
181
-
1
+ 'use client';
2
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
3
+ import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
4
+ import { ThumbsDown, ThumbsUp } from 'lucide-react';
5
+ import {
6
+ type SyntheticEvent,
7
+ useState,
8
+ useTransition,
9
+ } from 'react';
10
+ import { Collapsible, CollapsibleContent } from '@terrafirmagreg-team/ui/components/ui/collapsible';
11
+ import { cva } from 'class-variance-authority';
12
+ import {
13
+ actionResponse,
14
+ pageFeedback,
15
+ type ActionResponse,
16
+ type PageFeedback,
17
+ } from '../../lib/feedback-schema';
18
+ import { z } from 'zod/mini';
19
+ import { usePathname } from 'next/navigation';
20
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
21
+ import { useSubmissionStorage } from '../../hooks/feedback/use-submission-storage';
22
+
23
+ const rateButtonVariants = cva(
24
+ 'inline-flex items-center gap-2 px-3 py-2 rounded-full font-medium border text-sm [&_svg]:size-4 disabled:cursor-not-allowed',
25
+ {
26
+ variants: {
27
+ active: {
28
+ true: 'bg-fd-accent text-fd-accent-foreground [&_svg]:fill-current',
29
+ false: 'text-fd-muted-foreground',
30
+ },
31
+ },
32
+ },
33
+ );
34
+
35
+ const pageFeedbackResult = z.extend(pageFeedback, {
36
+ response: actionResponse,
37
+ });
38
+
39
+ export function Feedback({
40
+ onSendAction,
41
+ }: {
42
+ onSendAction: (feedback: PageFeedback) => Promise<ActionResponse>;
43
+ }) {
44
+ const t = useTranslations();
45
+ const pathname = usePathname();
46
+ const { previous, setPrevious } = useSubmissionStorage(pathname, (v) => {
47
+ const result = pageFeedbackResult.safeParse(v);
48
+ return result.success ? result.data : null;
49
+ });
50
+ const [opinion, setOpinion] = useState<'good' | 'bad' | null>(null);
51
+ const [message, setMessage] = useState('');
52
+ const [isPending, startTransition] = useTransition();
53
+
54
+ function submit(e?: SyntheticEvent) {
55
+ if (opinion == null) return;
56
+
57
+ startTransition(async () => {
58
+ const feedback: PageFeedback = {
59
+ url: location.href,
60
+ opinion: opinion!,
61
+ message,
62
+ };
63
+
64
+ const response = await onSendAction(feedback);
65
+ setPrevious({
66
+ response,
67
+ ...feedback,
68
+ });
69
+ setMessage('');
70
+ setOpinion(null);
71
+ });
72
+
73
+ e?.preventDefault();
74
+ }
75
+
76
+ const activeOpinion = previous?.opinion ?? opinion;
77
+
78
+ return (
79
+ <Collapsible
80
+ open={opinion !== null || previous !== null}
81
+ onOpenChange={(v) => {
82
+ if (!v) setOpinion(null);
83
+ }}
84
+ className="border-y py-3"
85
+ >
86
+ <div className="flex flex-row items-center gap-2">
87
+ <p className="text-sm font-medium pe-2">{t('How is this page?', { note: 'ui.feedback.question' })}</p>
88
+ <button
89
+ disabled={previous !== null}
90
+ className={cn(
91
+ rateButtonVariants({
92
+ active: activeOpinion === 'good',
93
+ }),
94
+ )}
95
+ onClick={() => {
96
+ setOpinion('good');
97
+ }}
98
+ >
99
+ <ThumbsUp />
100
+ {t('Good', { note: 'ui.feedback.good' })}
101
+ </button>
102
+ <button
103
+ disabled={previous !== null}
104
+ className={cn(
105
+ rateButtonVariants({
106
+ active: activeOpinion === 'bad',
107
+ }),
108
+ )}
109
+ onClick={() => {
110
+ setOpinion('bad');
111
+ }}
112
+ >
113
+ <ThumbsDown />
114
+ {t('Bad', { note: 'ui.feedback.bad' })}
115
+ </button>
116
+ </div>
117
+ <CollapsibleContent className="mt-3">
118
+ {previous ? (
119
+ <div className="px-3 py-6 flex flex-col items-center gap-3 bg-fd-card text-fd-muted-foreground text-sm text-center rounded-xl">
120
+ <p>{t('Thank you for your feedback!', { note: 'ui.feedback.thanks' })}</p>
121
+ <div className="flex flex-row items-center gap-2">
122
+ <a
123
+ href={previous.response?.githubUrl}
124
+ rel="noreferrer noopener"
125
+ target="_blank"
126
+ className={cn(
127
+ buttonVariants({
128
+ variant: 'default',
129
+ }),
130
+ 'text-xs',
131
+ )}
132
+ >
133
+ {t('View on GitHub', { note: 'ui.feedback.view_on_github' })}
134
+ </a>
135
+
136
+ <button
137
+ className={cn(
138
+ buttonVariants({
139
+ variant: 'secondary',
140
+ }),
141
+ 'text-xs',
142
+ )}
143
+ onClick={() => {
144
+ setOpinion(previous.opinion);
145
+ setPrevious(null);
146
+ }}
147
+ >
148
+ {t('Submit Again', { note: 'ui.feedback.submit_again' })}
149
+ </button>
150
+ </div>
151
+ </div>
152
+ ) : (
153
+ <form className="flex flex-col gap-3" onSubmit={submit}>
154
+ <textarea
155
+ autoFocus
156
+ required
157
+ value={message}
158
+ onChange={(e) => setMessage(e.target.value)}
159
+ className="border rounded-lg bg-fd-secondary text-fd-secondary-foreground p-3 resize-none focus-visible:outline-none placeholder:text-fd-muted-foreground"
160
+ placeholder={t('Leave your feedback…', { note: 'ui.feedback.placeholder' })}
161
+ onKeyDown={(e) => {
162
+ if (e.nativeEvent.isComposing || e.keyCode === 229) return;
163
+ if (!e.shiftKey && e.key === 'Enter') {
164
+ submit(e);
165
+ }
166
+ }}
167
+ />
168
+ <button
169
+ type="submit"
170
+ className={cn(buttonVariants({ variant: 'outline' }), 'w-fit px-3')}
171
+ disabled={isPending}
172
+ >
173
+ {t('Submit', { note: 'ui.feedback.submit' })}
174
+ </button>
175
+ </form>
176
+ )}
177
+ </CollapsibleContent>
178
+ </Collapsible>
179
+ );
180
+ }
181
+
@@ -1,29 +1,29 @@
1
- 'use client';
2
-
3
- import { createElement } from 'react';
4
- import { LogIn, LogOut } from 'lucide-react';
5
- import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
6
- import { useCommentsEnabled } from './use-enabled';
7
- import { useCommentsSession } from './use-session';
8
- import {
9
- redirectToCommentsLogin,
10
- redirectToCommentsLogout,
11
- } from '../../lib/comments/auth';
12
- import type { SiteMenuItem } from '@terrafirmagreg-team/ui/components/blocks/nav/site-menu';
13
-
14
- const COMMENTS_API = '/api/comments';
15
-
16
- export function useCommentsMenuItem(): SiteMenuItem | false {
17
- const t = useTranslations();
18
- const enabled = useCommentsEnabled(`${COMMENTS_API}/config`);
19
- const { session } = useCommentsSession(COMMENTS_API, { enabled });
20
- if (!enabled) return false;
21
- return {
22
- label: session ? t('Sign out', { note: 'ui.sign_out' }) : t('Sign in', { note: 'ui.sign_in' }),
23
- icon: session ? createElement(LogOut) : createElement(LogIn),
24
- onClick: session
25
- ? () => redirectToCommentsLogout(COMMENTS_API)
26
- : () => redirectToCommentsLogin(COMMENTS_API),
27
- };
28
- }
29
-
1
+ 'use client';
2
+
3
+ import { createElement } from 'react';
4
+ import { LogIn, LogOut } from 'lucide-react';
5
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
6
+ import { useCommentsEnabled } from './use-enabled';
7
+ import { useCommentsSession } from './use-session';
8
+ import {
9
+ redirectToCommentsLogin,
10
+ redirectToCommentsLogout,
11
+ } from '../../lib/comments/auth';
12
+ import type { SiteMenuItem } from '@terrafirmagreg-team/ui/components/blocks/nav/site-menu';
13
+
14
+ const COMMENTS_API = '/api/comments';
15
+
16
+ export function useCommentsMenuItem(): SiteMenuItem | false {
17
+ const t = useTranslations();
18
+ const enabled = useCommentsEnabled(`${COMMENTS_API}/config`);
19
+ const { session } = useCommentsSession(COMMENTS_API, { enabled });
20
+ if (!enabled) return false;
21
+ return {
22
+ label: session ? t('Sign out', { note: 'ui.sign_out' }) : t('Sign in', { note: 'ui.sign_in' }),
23
+ icon: session ? createElement(LogOut) : createElement(LogIn),
24
+ onClick: session
25
+ ? () => redirectToCommentsLogout(COMMENTS_API)
26
+ : () => redirectToCommentsLogin(COMMENTS_API),
27
+ };
28
+ }
29
+
@@ -1,21 +1,21 @@
1
- 'use client';
2
-
3
- import { useMemo } from 'react';
4
- import useSWRImmutable from 'swr/immutable';
5
- import type { AuthInfoWithRole } from '@fuma-comment/server';
6
- import { createCommentsClient } from '../../lib/comments/client';
7
- import { usePageThread } from './use-thread';
8
-
9
- export function useCommentsSession(
10
- apiUrl = '/api/comments',
11
- options?: { page?: string; enabled?: boolean },
12
- ): { session: AuthInfoWithRole | null; isLoading: boolean } {
13
- const thread = usePageThread(options?.page);
14
- const client = useMemo(() => createCommentsClient(apiUrl), [apiUrl]);
15
- const query = useSWRImmutable(
16
- (options?.enabled ?? true) ? ['comments-session', thread] : null,
17
- () => client.getAuthSession({ page: thread }).catch(() => null),
18
- );
19
- return { session: query.data ?? null, isLoading: query.isLoading };
20
- }
21
-
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import useSWRImmutable from 'swr/immutable';
5
+ import type { AuthInfoWithRole } from '@fuma-comment/server';
6
+ import { createCommentsClient } from '../../lib/comments/client';
7
+ import { usePageThread } from './use-thread';
8
+
9
+ export function useCommentsSession(
10
+ apiUrl = '/api/comments',
11
+ options?: { page?: string; enabled?: boolean },
12
+ ): { session: AuthInfoWithRole | null; isLoading: boolean } {
13
+ const thread = usePageThread(options?.page);
14
+ const client = useMemo(() => createCommentsClient(apiUrl), [apiUrl]);
15
+ const query = useSWRImmutable(
16
+ (options?.enabled ?? true) ? ['comments-session', thread] : null,
17
+ () => client.getAuthSession({ page: thread }).catch(() => null),
18
+ );
19
+ return { session: query.data ?? null, isLoading: query.isLoading };
20
+ }
21
+