Skip to content

Commit af0644d

Browse files
committed
[Landing] Send quick feedback
1 parent f2209a8 commit af0644d

10 files changed

Lines changed: 388 additions & 72 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { constants } from '@brunolemos/devhub-core'
2+
import axios from 'axios'
3+
import classNames from 'classnames'
4+
import React, { useEffect, useRef, useState } from 'react'
5+
import { unstable_batchedUpdates as batch } from 'react-dom'
6+
7+
import { useAuth } from '../../context/AuthContext'
8+
import { getDefaultDevHubHeaders } from '../../helpers'
9+
import { TextInput, TextInputProps } from './TextInput'
10+
11+
export interface QuickFeedbackInputProps extends TextInputProps {}
12+
13+
export const quickFeedbackRowHeight = 42
14+
15+
export function QuickFeedbackInput(props: QuickFeedbackInputProps) {
16+
const isMountedRef = useRef(true)
17+
18+
const [feedbackText, setFeedbackText] = useState('')
19+
const [message, setMessage] = useState<React.ReactNode>('')
20+
const [placeholder, setPlaceholder] = useState('')
21+
22+
const {
23+
authData: { appToken },
24+
} = useAuth()
25+
26+
useEffect(() => {
27+
isMountedRef.current = true
28+
29+
return () => {
30+
isMountedRef.current = false
31+
}
32+
}, [])
33+
34+
async function trySendFeedback() {
35+
const response = await axios.post(
36+
`${constants.API_BASE_URL}/feedback`,
37+
{
38+
feedback: feedbackText || '',
39+
location: window.location.href || '',
40+
},
41+
{ headers: getDefaultDevHubHeaders({ appToken }) },
42+
)
43+
44+
if (!isMountedRef.current) return
45+
46+
const { data, status } = await response
47+
48+
if (status !== 200) {
49+
throw new Error(
50+
typeof data === 'string' ? data : 'Failed to send feedback.',
51+
)
52+
}
53+
}
54+
55+
async function handleSubmit() {
56+
if (!isMountedRef.current) return
57+
58+
if (!(feedbackText && feedbackText.trim())) return
59+
60+
try {
61+
setMessage('Sending...')
62+
63+
await trySendFeedback()
64+
if (!isMountedRef.current) return
65+
66+
if (feedbackText.toLowerCase().includes('expensive')) {
67+
setMessage('Got it...')
68+
69+
setTimeout(() => {
70+
setMessage('How much would you pay?')
71+
setTimeout(() => {
72+
batch(() => {
73+
setMessage('')
74+
setFeedbackText('')
75+
setPlaceholder('How much would you pay?')
76+
})
77+
}, 2000)
78+
}, 1000)
79+
80+
return
81+
}
82+
if (feedbackText.toLowerCase().includes(' caro')) {
83+
setMessage('Entendi...')
84+
setTimeout(() => {
85+
setMessage('Quanto você pagaria?')
86+
setTimeout(() => {
87+
batch(() => {
88+
setMessage('')
89+
setFeedbackText('')
90+
setPlaceholder('Quanto você pagaria?')
91+
})
92+
}, 2000)
93+
}, 1000)
94+
95+
return
96+
}
97+
98+
setTimeout(() => {
99+
setMessage('Thanks for your feedback!')
100+
101+
setTimeout(() => {
102+
setMessage(
103+
<p
104+
className={classNames(
105+
'text-center text-muted-65',
106+
props.className,
107+
)}
108+
style={{ minWidth: 250 }}
109+
>
110+
Send via{' '}
111+
<a
112+
className="text-muted-65"
113+
href={`https://twitter.com/messages/compose?recipient_id=1013342195087224832&text=${encodeURIComponent(
114+
feedbackText,
115+
)}`}
116+
target="_blank"
117+
>
118+
Twitter
119+
</a>{' '}
120+
or{' '}
121+
<a
122+
className="text-muted-65"
123+
href={constants.DEVHUB_LINKS.SLACK_INVITATION}
124+
target="_blank"
125+
>
126+
Slack
127+
</a>{' '}
128+
to get a response.
129+
</p>,
130+
)
131+
132+
setTimeout(() => {
133+
batch(() => {
134+
setMessage('')
135+
setFeedbackText('')
136+
setPlaceholder('')
137+
})
138+
}, 8000)
139+
}, 2000)
140+
}, 1000)
141+
} catch (error) {
142+
console.error(error)
143+
// bugsnag.notify(error)
144+
145+
setMessage(`Failed to send feedback. Please contact us.`)
146+
147+
setTimeout(() => {
148+
batch(() => {
149+
setMessage('')
150+
setFeedbackText('')
151+
setPlaceholder('')
152+
})
153+
}, 2000)
154+
}
155+
}
156+
157+
return (
158+
<div
159+
className="flex items-center justify-center"
160+
style={{ height: quickFeedbackRowHeight }}
161+
>
162+
{message ? (
163+
typeof message === 'string' ? (
164+
<p
165+
className={classNames('text-center text-muted-65', props.className)}
166+
style={{ minWidth: 250 }}
167+
>
168+
{message}
169+
</p>
170+
) : (
171+
message
172+
)
173+
) : (
174+
<form
175+
onSubmit={e => {
176+
handleSubmit()
177+
e.preventDefault()
178+
}}
179+
>
180+
<TextInput
181+
{...props}
182+
className={classNames('text-center', props.className)}
183+
onChange={e => {
184+
setFeedbackText(e.target.value)
185+
if (props.onChange) props.onChange(e)
186+
}}
187+
placeholder={
188+
placeholder || props.placeholder || 'Send quick feedback'
189+
}
190+
style={{ minWidth: 250, ...props.style }}
191+
/>
192+
</form>
193+
)}
194+
</div>
195+
)
196+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import classNames from 'classnames'
2+
import React, { useState } from 'react'
3+
4+
export interface TextInputProps
5+
extends React.InputHTMLAttributes<HTMLInputElement> {}
6+
7+
export function TextInput(props: TextInputProps) {
8+
const {} = props
9+
10+
const [isFocused, setIsFocused] = useState(false)
11+
12+
return (
13+
<input
14+
{...props}
15+
className={classNames(
16+
'bg-more-3 py-2 px-10 text-default placeholder-text-muted-65 border rounded-full overflow-hidden outline-none',
17+
isFocused ? ' shadow-md border-primary' : ' border-bg-less-3 shadow',
18+
props.className,
19+
)}
20+
onBlur={e => {
21+
setIsFocused(false)
22+
if (props.onBlur) props.onBlur(e)
23+
}}
24+
onFocus={e => {
25+
setIsFocused(true)
26+
if (props.onFocus) props.onFocus(e)
27+
}}
28+
/>
29+
)
30+
}

landing/src/components/layouts/LandingLayout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import Footer from '../sections/Footer'
1+
import FloatingFooter from '../sections/footer/FloatingFooter'
2+
import Footer from '../sections/footer/Footer'
23
import Header from '../sections/header/Header'
34

45
export interface LandingLayoutProps {
@@ -15,6 +16,7 @@ export default function LandingLayout(props: LandingLayoutProps) {
1516
<div className="pb-16 md:pb-32" />
1617

1718
<Footer />
19+
<FloatingFooter />
1820
</section>
1921
)
2022
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import classNames from 'classnames'
2+
import React, { useState } from 'react'
3+
4+
import { QuickFeedbackInput } from '../../common/QuickFeedbackInput'
5+
6+
export interface FloatingFooterProps {}
7+
8+
const fixed = true
9+
export default function FloatingFooter(_props: FloatingFooterProps) {
10+
const [isFocused, setIsFocused] = useState(false)
11+
12+
return (
13+
<>
14+
<section
15+
className={classNames(
16+
fixed && 'fixed bottom-0 left-0 right-0',
17+
'mt-10',
18+
)}
19+
style={{
20+
...(fixed && {
21+
minHeight: 42,
22+
...(isFocused && {
23+
backdropFilter: 'blur(10px)',
24+
WebkitBackdropFilter: 'blur(10px)',
25+
}),
26+
zIndex: 100,
27+
}),
28+
}}
29+
>
30+
{!!(fixed && isFocused) && (
31+
<div className="absolute inset-0 bg-default opacity-75" />
32+
)}
33+
34+
<div className="relative container py-2 overflow-x-visible">
35+
<div className="flex flex-row items-center justify-center sm:justify-end overflow-x-auto">
36+
<QuickFeedbackInput
37+
className={classNames(
38+
/* isFocused ? 'w-full' : */ 'w-full sm:w-auto',
39+
'text-center',
40+
)}
41+
onBlur={() => {
42+
setIsFocused(false)
43+
}}
44+
onFocus={() => {
45+
setIsFocused(true)
46+
}}
47+
/>
48+
</div>
49+
</div>
50+
</section>
51+
52+
{!!fixed && <div className="mt-10" style={{ minHeight: 42 }} />}
53+
</>
54+
)
55+
}

landing/src/components/sections/Footer.tsx renamed to landing/src/components/sections/footer/Footer.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ export default function Footer() {
7979
<div className="flex flex-col items-start w-1/2 sm:w-auto">
8080
<div className={twClasses.footerTitle}>Contact a human</div>
8181

82+
<a
83+
className={twClasses.footerLink}
84+
href="https://twitter.com/messages/compose?recipient_id=1013342195087224832"
85+
target="_blank"
86+
rel="noopener"
87+
>
88+
@devhub_app
89+
</a>
90+
8291
<a
8392
className={twClasses.footerLink}
8493
href="https://twitter.com/brunolemos"

0 commit comments

Comments
 (0)