2020-05-10 00:21:04 -07:00
import React from "react" ;
2021-01-03 19:11:46 -08:00
import { ClassNames } from "@emotion/react" ;
2020-05-10 00:54:23 -07:00
import gql from "graphql-tag" ;
2020-08-12 00:37:31 -07:00
import {
Box ,
Button ,
2021-07-11 18:16:57 -07:00
Center ,
2020-08-12 00:37:31 -07:00
Flex ,
2021-01-18 06:31:27 -08:00
HStack ,
2021-01-18 15:56:24 -08:00
IconButton ,
2020-08-12 00:37:31 -07:00
Input ,
2021-01-18 15:56:24 -08:00
InputGroup ,
InputLeftElement ,
InputRightElement ,
2021-03-13 03:18:32 -08:00
Link ,
2021-01-19 15:46:21 -08:00
ListItem ,
2021-07-11 18:16:57 -07:00
Skeleton ,
2020-10-07 09:48:55 -07:00
Textarea ,
2021-07-12 04:46:07 -07:00
Tooltip ,
2021-01-19 15:46:21 -08:00
UnorderedList ,
2020-08-12 00:37:31 -07:00
useColorModeValue ,
useTheme ,
useToast ,
2020-10-22 16:01:54 -07:00
VStack ,
2020-12-25 09:08:33 -08:00
} from "@chakra-ui/react" ;
2021-01-18 15:56:24 -08:00
import { ArrowForwardIcon , SearchIcon } from "@chakra-ui/icons" ;
2020-09-06 18:12:34 -07:00
import { useHistory , useLocation } from "react-router-dom" ;
2021-01-18 06:31:27 -08:00
import { useLazyQuery , useQuery } from "@apollo/client" ;
2020-05-10 00:21:04 -07:00
2021-01-04 01:17:30 -08:00
import {
2021-01-18 06:46:18 -08:00
Delay ,
2021-01-18 06:31:27 -08:00
ErrorMessage ,
2021-01-04 01:17:30 -08:00
Heading1 ,
2021-01-18 06:31:27 -08:00
Heading2 ,
2021-05-05 00:22:28 -07:00
TestErrorSender ,
2021-01-04 01:17:30 -08:00
useCommonStyles ,
useLocalStorage ,
usePageTitle ,
} from "./util" ;
2020-07-22 22:07:45 -07:00
import OutfitPreview from "./components/OutfitPreview" ;
2020-10-09 08:01:46 -07:00
import SpeciesColorPicker from "./components/SpeciesColorPicker" ;
2021-01-18 06:31:27 -08:00
import SquareItemCard , {
SquareItemCardSkeleton ,
} from "./components/SquareItemCard" ;
2021-01-14 06:43:55 -08:00
import WIPCallout from "./components/WIPCallout" ;
2020-05-10 00:21:04 -07:00
2021-01-03 19:49:08 -08:00
import HomepageSplashImg from "./images/homepage-splash.png" ;
import HomepageSplashImg2x from "./images/homepage-splash@2x.png" ;
import FeedbackXweeImg from "./images/feedback-xwee.png" ;
import FeedbackXweeImg2x from "./images/feedback-xwee@2x.png" ;
2020-05-10 00:21:04 -07:00
function HomePage ( ) {
2020-09-11 23:53:57 -07:00
usePageTitle ( null ) ;
2020-07-31 16:57:37 -07:00
useSupportSetup ( ) ;
2020-05-17 23:26:00 -07:00
2020-07-22 22:07:45 -07:00
const [ previewState , setPreviewState ] = React . useState ( null ) ;
2020-05-10 00:21:04 -07:00
return (
2020-09-06 18:12:34 -07:00
< Flex direction = "column" align = "center" textAlign = "center" marginTop = "8" >
2020-05-17 23:26:00 -07:00
< Box
width = "200px"
height = "200px"
2020-07-20 21:32:42 -07:00
borderRadius = "lg"
2020-05-17 23:26:00 -07:00
boxShadow = "md"
2020-07-22 22:07:45 -07:00
overflow = "hidden"
>
< OutfitPreview
speciesId = { previewState ? . speciesId }
colorId = { previewState ? . colorId }
pose = { previewState ? . pose }
wornItemIds = { [ ] }
2020-09-13 04:12:14 -07:00
loadingDelayMs = { 1500 }
2020-07-22 22:07:45 -07:00
placeholder = {
< Box
as = "img"
src = { HomepageSplashImg }
srcSet = { ` ${ HomepageSplashImg } 1x, ${ HomepageSplashImg2x } 2x ` }
alt = ""
/ >
}
/ >
< / B o x >
2020-05-17 23:26:00 -07:00
< Box height = "4" / >
< Heading1 > Dress to Impress < / H e a d i n g 1 >
2020-10-07 07:52:34 -07:00
< Box fontSize = "lg" fontStyle = "italic" opacity = "0.85" role = "doc-subtitle" >
Design and share your Neopets outfits !
< / B o x >
2020-05-17 23:26:00 -07:00
< Box height = "8" / >
2020-07-22 22:07:45 -07:00
< StartOutfitForm onChange = { setPreviewState } / >
2020-05-17 23:26:00 -07:00
< Box height = "4" / >
< Box fontStyle = "italic" fontSize = "sm" >
or
< / B o x >
< Box height = "4" / >
< SubmitPetForm / >
2020-10-07 09:48:55 -07:00
< Box height = "16" / >
2021-01-18 06:31:27 -08:00
< NewItemsSection / >
< Box height = "16" / >
2020-10-07 09:48:55 -07:00
< FeedbackFormSection / >
2021-01-14 06:43:55 -08:00
< Box height = "16" / >
< WIPCallout details = "We started building this last year, but, well… what a year 😅 Anyway, this will eventually become the main site, at impress.openneo.net!" >
Maybe we ' ll rename it to Impress 2021 … or maybe not ! 🤔
< / W I P C a l l o u t >
2021-05-05 00:22:28 -07:00
< TestErrorSender / >
2020-05-17 23:26:00 -07:00
< / F l e x >
2020-05-10 00:21:04 -07:00
) ;
}
2020-07-22 22:07:45 -07:00
function StartOutfitForm ( { onChange } ) {
2020-05-10 00:21:04 -07:00
const history = useHistory ( ) ;
2020-05-23 13:23:24 -07:00
const idealPose = React . useMemo (
( ) => ( Math . random ( ) > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC" ) ,
[ ]
) ;
2020-05-10 00:21:04 -07:00
const [ speciesId , setSpeciesId ] = React . useState ( "1" ) ;
const [ colorId , setColorId ] = React . useState ( "8" ) ;
const [ isValid , setIsValid ] = React . useState ( true ) ;
2020-05-23 13:23:24 -07:00
const [ closestPose , setClosestPose ] = React . useState ( idealPose ) ;
2020-05-10 00:21:04 -07:00
2020-05-10 00:54:23 -07:00
const onSubmit = ( e ) => {
e . preventDefault ( ) ;
2020-05-10 00:21:04 -07:00
if ( ! isValid ) {
return ;
}
const params = new URLSearchParams ( {
species : speciesId ,
color : colorId ,
2020-05-23 13:23:24 -07:00
pose : closestPose ,
2020-05-10 00:21:04 -07:00
} ) ;
history . push ( ` /outfits/new? ${ params } ` ) ;
} ;
2020-08-12 00:37:31 -07:00
const buttonBgColor = useColorModeValue ( "green.600" , "green.300" ) ;
const buttonBgColorHover = useColorModeValue ( "green.700" , "green.200" ) ;
2020-05-10 00:21:04 -07:00
return (
< form onSubmit = { onSubmit } >
< Flex >
< SpeciesColorPicker
speciesId = { speciesId }
colorId = { colorId }
2020-05-23 13:23:24 -07:00
idealPose = { idealPose }
2020-05-10 00:21:04 -07:00
showPlaceholders
2020-09-21 02:43:58 -07:00
colorPlaceholderText = "Blue"
speciesPlaceholderText = "Acara"
2020-05-23 13:23:24 -07:00
onChange = { ( species , color , isValid , closestPose ) => {
2020-05-10 00:21:04 -07:00
setSpeciesId ( species . id ) ;
setColorId ( color . id ) ;
setIsValid ( isValid ) ;
2020-05-23 13:23:24 -07:00
setClosestPose ( closestPose ) ;
2020-07-22 22:07:45 -07:00
if ( isValid ) {
onChange ( {
speciesId : species . id ,
colorId : color . id ,
pose : closestPose ,
} ) ;
}
2020-05-10 00:21:04 -07:00
} }
/ >
< Box width = "4" / >
2020-05-18 01:21:46 -07:00
< Button
type = "submit"
2020-07-20 21:32:42 -07:00
colorScheme = "green"
2020-05-18 01:21:46 -07:00
disabled = { ! isValid }
2020-08-12 00:37:31 -07:00
backgroundColor = { buttonBgColor }
_hover = { { backgroundColor : buttonBgColorHover } }
2020-05-18 01:21:46 -07:00
>
2020-05-10 01:17:16 -07:00
Start
< / B u t t o n >
2020-05-10 00:21:04 -07:00
< / F l e x >
< / f o r m >
) ;
}
2020-05-10 00:54:23 -07:00
function SubmitPetForm ( ) {
const history = useHistory ( ) ;
const theme = useTheme ( ) ;
const toast = useToast ( ) ;
2021-04-16 03:15:24 -07:00
const location = useLocation ( ) ;
2020-05-10 00:54:23 -07:00
const [ petName , setPetName ] = React . useState ( "" ) ;
2021-04-16 03:15:24 -07:00
const [ loadPetQuery , { loading } ] = useLazyQuery (
2020-05-10 00:54:23 -07:00
gql `
2020-05-19 14:48:54 -07:00
query SubmitPetForm ( $petName : String ! ) {
2020-05-10 00:54:23 -07:00
petOnNeopetsDotCom ( petName : $petName ) {
color {
id
}
species {
id
}
2020-05-23 13:55:59 -07:00
pose
2020-05-10 00:54:23 -07:00
items {
id
}
}
}
` ,
{
fetchPolicy : "network-only" ,
onCompleted : ( data ) => {
if ( ! data ) return ;
2020-05-23 13:55:59 -07:00
const { species , color , pose , items } = data . petOnNeopetsDotCom ;
2020-05-10 00:54:23 -07:00
const params = new URLSearchParams ( {
name : petName ,
species : species . id ,
color : color . id ,
2020-05-23 13:55:59 -07:00
pose ,
2020-05-10 00:54:23 -07:00
} ) ;
for ( const item of items ) {
params . append ( "objects[]" , item . id ) ;
}
history . push ( ` /outfits/new? ${ params } ` ) ;
} ,
onError : ( ) => {
toast ( {
title : "We couldn't load that pet, sorry 😓" ,
description : "Is it spelled correctly?" ,
status : "error" ,
} ) ;
} ,
}
) ;
2021-04-16 03:15:24 -07:00
const loadPet = React . useCallback (
( petName ) => {
loadPetQuery ( { variables : { petName } } ) ;
2020-05-18 00:20:48 -07:00
2021-04-16 03:15:24 -07:00
// Start preloading the WardrobePage, too!
// eslint-disable-next-line no-unused-expressions
import ( "./WardrobePage" ) . catch ( ( e ) => {
// Let's just let this slide, because it's a preload error. Critical
// failures will happen elsewhere, and trigger reloads!
console . error ( e ) ;
} ) ;
} ,
[ loadPetQuery ]
) ;
2020-05-18 00:20:48 -07:00
2021-04-16 03:15:24 -07:00
// If the ?loadPet= query param is provided, auto-load the pet immediately.
// This isn't used in-app, but is a helpful hook for things like link-ins and
// custom search engines. (I feel like a route or a different UX would be
// better, but I don't really know enough to commit to one… let's just keep
// this simple for now, I think. We might change this someday!)
React . useEffect ( ( ) => {
const params = new URLSearchParams ( location . search ) ;
const petName = params . get ( "loadPet" ) ;
if ( petName ) {
setPetName ( petName ) ;
loadPet ( petName ) ;
}
} , [ location , loadPet ] ) ;
2020-05-10 00:54:23 -07:00
2021-01-04 01:17:30 -08:00
const { brightBackground } = useCommonStyles ( ) ;
2020-08-12 00:37:31 -07:00
const inputBorderColor = useColorModeValue ( "green.600" , "green.500" ) ;
const inputBorderColorHover = useColorModeValue ( "green.400" , "green.300" ) ;
const buttonBgColor = useColorModeValue ( "green.600" , "green.300" ) ;
const buttonBgColorHover = useColorModeValue ( "green.700" , "green.200" ) ;
2020-05-10 00:54:23 -07:00
return (
2021-01-03 19:11:46 -08:00
< ClassNames >
{ ( { css } ) => (
2021-04-16 03:15:24 -07:00
< form
onSubmit = { ( e ) => {
e . preventDefault ( ) ;
loadPet ( petName ) ;
} }
>
2021-01-03 19:11:46 -08:00
< Flex >
< Input
value = { petName }
onChange = { ( e ) => setPetName ( e . target . value ) }
isDisabled = { loading }
placeholder = "Enter a pet's name"
aria - label = "Enter a pet's name"
borderColor = { inputBorderColor }
_hover = { { borderColor : inputBorderColorHover } }
2021-01-04 01:17:30 -08:00
background = { brightBackground }
2021-01-03 19:11:46 -08:00
boxShadow = "md"
width = "14em"
className = { css `
& : : placeholder {
color : $ { theme . colors . gray [ "500" ] } ;
}
` }
/ >
< Box width = "4" / >
< Button
type = "submit"
colorScheme = "green"
isDisabled = { ! petName }
isLoading = { loading }
backgroundColor = { buttonBgColor } // for AA contrast
_hover = { { backgroundColor : buttonBgColorHover } }
>
Start
< / B u t t o n >
< / F l e x >
< / f o r m >
) }
< / C l a s s N a m e s >
2020-05-10 00:54:23 -07:00
) ;
}
2021-01-18 06:31:27 -08:00
function NewItemsSection ( ) {
return (
< Box width = "100%" >
2021-01-18 15:56:24 -08:00
< Flex align = "center" wrap = "wrap" >
< Heading2 flex = "0 0 auto" marginRight = "2" textAlign = "left" >
Latest items
< / H e a d i n g 2 >
< Box flex = "0 0 auto" marginLeft = "auto" width = "48" >
< ItemsSearchField / >
< / B o x >
< / F l e x >
2021-01-18 06:31:27 -08:00
< NewItemsSectionContent / >
< / B o x >
) ;
}
2021-01-18 15:56:24 -08:00
function ItemsSearchField ( ) {
const [ query , setQuery ] = React . useState ( "" ) ;
const { brightBackground } = useCommonStyles ( ) ;
const history = useHistory ( ) ;
return (
< form
onSubmit = { ( ) => {
if ( query ) {
history . push ( ` /items/search/ ${ encodeURIComponent ( query ) } ` ) ;
}
} }
>
2021-06-08 00:20:23 -07:00
< InputGroup size = "sm" >
2021-01-18 15:56:24 -08:00
< InputLeftElement pointerEvents = "none" >
< SearchIcon color = "gray.400" / >
< / I n p u t L e f t E l e m e n t >
< Input
value = { query }
2021-06-08 00:20:23 -07:00
backgroundColor = { query ? brightBackground : "transparent" }
_focus = { { backgroundColor : brightBackground } }
2021-01-18 15:56:24 -08:00
onChange = { ( e ) => setQuery ( e . target . value ) }
placeholder = "Search all items…"
borderRadius = "full"
/ >
< InputRightElement >
< IconButton
type = "submit"
variant = "ghost"
icon = { < ArrowForwardIcon / > }
aria - label = "Search"
minWidth = "1.5rem"
minHeight = "1.5rem"
width = "1.5rem"
height = "1.5rem"
borderRadius = "full"
opacity = { query ? 1 : 0 }
transition = "opacity 0.2s"
aria - hidden = { query ? "false" : "true" }
/ >
< / I n p u t R i g h t E l e m e n t >
< / I n p u t G r o u p >
< / f o r m >
) ;
}
2021-01-18 06:31:27 -08:00
function NewItemsSectionContent ( ) {
2021-01-18 07:15:00 -08:00
const { loading , error , data } = useQuery (
gql `
2021-01-21 14:57:21 -08:00
query NewItemsSection {
2021-07-11 21:32:14 -07:00
newestItems {
2021-01-18 07:15:00 -08:00
id
name
thumbnailUrl
2021-04-01 20:08:35 -07:00
isNc
2021-05-13 16:24:24 -07:00
isPb
2021-07-11 18:09:29 -07:00
speciesThatNeedModels {
id
name
}
2021-07-17 05:47:32 -07:00
babySpeciesThatNeedModels : speciesThatNeedModels ( colorId : "6" ) {
id
name
}
maraquanSpeciesThatNeedModels : speciesThatNeedModels ( colorId : "44" ) {
id
name
}
mutantSpeciesThatNeedModels : speciesThatNeedModels ( colorId : "46" ) {
id
name
}
2021-07-11 19:08:47 -07:00
compatibleBodiesAndTheirZones {
body {
id
representsAllBodies
species {
id
name
}
canonicalAppearance {
id
color {
id
name
isStandard
}
}
}
}
2021-06-08 01:59:56 -07:00
}
}
`
) ;
const { data : userData } = useQuery (
gql `
query NewItemsSection _UserData {
newestItems {
id
2021-06-08 01:03:09 -07:00
currentUserOwnsThis
currentUserWantsThis
2021-01-18 07:15:00 -08:00
}
2021-01-18 06:31:27 -08:00
}
2021-06-08 01:03:09 -07:00
` ,
2021-06-08 01:59:56 -07:00
{
context : { sendAuth : true } ,
onError : ( e ) =>
console . error ( "Error loading NewItemsSection_UserData, skipping:" , e ) ,
}
2021-01-18 07:15:00 -08:00
) ;
2021-01-18 06:31:27 -08:00
if ( loading ) {
2021-07-11 18:16:57 -07:00
const footer = (
< Center fontSize = "xs" height = "1.5em" >
< Skeleton height = "4px" width = "100%" / >
< / C e n t e r >
) ;
2021-01-18 06:31:27 -08:00
return (
2021-01-18 06:46:18 -08:00
< Delay >
< ItemCardHStack >
2021-07-11 18:09:29 -07:00
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } minHeightNumLines = { 3 } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } minHeightNumLines = { 3 } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } minHeightNumLines = { 3 } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } minHeightNumLines = { 3 } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } minHeightNumLines = { 3 } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } minHeightNumLines = { 3 } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } / >
< SquareItemCardSkeleton footer = { footer } / >
2021-01-18 06:46:18 -08:00
< / I t e m C a r d H S t a c k >
< / D e l a y >
2021-01-18 06:31:27 -08:00
) ;
}
if ( error ) {
return (
< ErrorMessage >
Couldn ' t load new items . Check your connection and try again !
< / E r r o r M e s s a g e >
) ;
}
2021-06-08 01:59:56 -07:00
// Merge in the results from the user data query, if available.
const newestItems = data . newestItems . map ( ( item ) => {
const itemUserData =
( userData ? . newestItems || [ ] ) . find ( ( i ) => i . id === item . id ) || { } ;
return { ... item , ... itemUserData } ;
} ) ;
2021-01-18 06:31:27 -08:00
return (
< ItemCardHStack >
2021-06-08 01:59:56 -07:00
{ newestItems . map ( ( item ) => (
2021-07-11 18:09:29 -07:00
< SquareItemCard
key = { item . id }
item = { item }
2021-07-11 18:16:57 -07:00
footer = { < ItemModelingSummary item = { item } / > }
2021-07-11 18:09:29 -07:00
/ >
2021-01-18 06:31:27 -08:00
) ) }
< / I t e m C a r d H S t a c k >
) ;
}
2021-07-11 18:16:57 -07:00
function ItemModelingSummary ( { item } ) {
2021-07-11 21:27:24 -07:00
// NOTE: To test this logic, I like to swap out `newestItems` in the query:
// `newestItems: items(ids: ["81546", "35082", "75149", "81797", "58741", "78953", "82427", "82727", "82726"])`
2021-07-17 05:47:32 -07:00
const numModelsNeeded =
item . speciesThatNeedModels . length +
item . babySpeciesThatNeedModels . length +
item . maraquanSpeciesThatNeedModels . length +
item . mutantSpeciesThatNeedModels . length ;
if ( numModelsNeeded > 0 ) {
2021-07-11 18:16:57 -07:00
return (
< Box fontSize = "xs" fontStyle = "italic" fontWeight = "600" opacity = "0.8" >
2021-07-17 05:47:32 -07:00
Need { numModelsNeeded } models
2021-07-11 18:16:57 -07:00
< / B o x >
) ;
}
2021-07-11 19:08:47 -07:00
const bodies = item . compatibleBodiesAndTheirZones . map ( ( bz ) => bz . body ) ;
const fitsAllPets = bodies . some ( ( b ) => b . representsAllBodies ) ;
if ( fitsAllPets ) {
return (
< Box fontSize = "xs" fontStyle = "italic" opacity = "0.8" >
2021-07-12 04:46:07 -07:00
Fits all pets
2021-07-11 19:08:47 -07:00
< / B o x >
) ;
}
2021-07-11 21:27:24 -07:00
// HACK: The Maraquan Mynci and the Blue Mynci have the same body, so to test
// whether something is *meant* for standard colors, we check for more
// than
const standardBodies = bodies . filter (
( b ) => b . canonicalAppearance . color . isStandard
) ;
const isMeantForStandardBodies = standardBodies . length >= 2 ;
2021-07-11 19:08:47 -07:00
const colors = bodies . map ( ( b ) => b . canonicalAppearance . color ) ;
const specialColor = colors . find ( ( c ) => ! c . isStandard ) ;
2021-07-11 21:27:24 -07:00
const hasSpecialColorOnly = ! isMeantForStandardBodies && specialColor != null ;
if ( hasSpecialColorOnly && bodies . length === 1 ) {
2021-07-11 19:08:47 -07:00
return (
< Box fontSize = "xs" fontStyle = "italic" opacity = "0.8" >
{ specialColor . name } { bodies [ 0 ] . species . name } only
< / B o x >
) ;
}
if ( bodies . length === 1 ) {
return (
< Box fontSize = "xs" fontStyle = "italic" opacity = "0.8" >
{ bodies [ 0 ] . species . name } only
< / B o x >
) ;
}
2021-07-11 21:27:24 -07:00
if ( hasSpecialColorOnly ) {
2021-07-11 19:08:47 -07:00
return (
< Box fontSize = "xs" fontStyle = "italic" opacity = "0.8" >
{ specialColor . name } only
< / B o x >
) ;
}
2021-07-11 18:16:57 -07:00
return (
< Box fontSize = "xs" fontStyle = "italic" opacity = "0.8" >
2021-07-12 04:46:07 -07:00
Fits all { " " }
< Tooltip
label = {
< Box fontSize = "xs" textAlign = "center" >
Not special colors like Baby , Maraquan , or Mutant .
< / B o x >
}
>
< Box display = "inline-block" borderBottom = "1px dotted" tabIndex = "0" >
basic
< / B o x >
< / T o o l t i p > { " " }
pets
2021-07-11 18:16:57 -07:00
< / B o x >
) ;
}
2021-01-18 06:31:27 -08:00
function ItemCardHStack ( { children } ) {
return (
// HACK: I wanted to just have an HStack with overflow:auto and internal
// paddingX, but the right-hand-side padding didn't seem to work
// during overflow. This was the best I could come up with...
< Flex maxWidth = "100%" overflow = "auto" paddingY = "4" >
< Box minWidth = "2" / >
< HStack align = "flex-start" spacing = "4" >
{ children }
< / H S t a c k >
< Box minWidth = "2" / >
< / F l e x >
) ;
}
2020-10-07 09:48:55 -07:00
function FeedbackFormSection ( ) {
2021-01-04 01:17:30 -08:00
const { brightBackground } = useCommonStyles ( ) ;
2020-10-22 16:01:54 -07:00
const pitchBorderColor = useColorModeValue ( "gray.300" , "green.400" ) ;
const formBorderColor = useColorModeValue ( "gray.300" , "blue.400" ) ;
2020-10-07 09:48:55 -07:00
return (
2020-10-22 16:01:54 -07:00
< VStack spacing = "4" alignItems = "stretch" >
2021-01-04 01:17:30 -08:00
< FeedbackFormContainer
background = { brightBackground }
borderColor = { pitchBorderColor }
>
2021-01-19 15:46:21 -08:00
< Flex >
< Box
padding = "2"
borderRadius = "lg"
overflow = "hidden"
flex = "0 0 auto"
marginTop = "4"
>
2020-10-22 16:01:54 -07:00
< Box
as = "img"
src = { FeedbackXweeImg }
srcSet = { ` ${ FeedbackXweeImg } 1x, ${ FeedbackXweeImg2x } 2x ` }
height = "90px"
width = "90px"
opacity = "0.9"
alt = ""
/ >
< / B o x >
< FeedbackFormPitch / >
< / F l e x >
< / F e e d b a c k F o r m C o n t a i n e r >
< FeedbackFormContainer
borderColor = { formBorderColor }
image = {
< Box
as = "img"
src = { FeedbackXweeImg }
srcSet = { ` ${ FeedbackXweeImg } 1x, ${ FeedbackXweeImg2x } 2x ` }
height = "90px"
width = "90px"
opacity = "0.9"
alt = ""
/ >
}
>
< FeedbackForm / >
< / F e e d b a c k F o r m C o n t a i n e r >
< / V S t a c k >
) ;
}
2021-01-04 01:17:30 -08:00
function FeedbackFormContainer ( { background , borderColor , children } ) {
2020-10-22 16:01:54 -07:00
return (
< Box
2020-10-07 09:48:55 -07:00
as = "section"
2021-01-04 01:17:30 -08:00
background = { background }
2020-10-07 09:48:55 -07:00
border = "1px solid"
borderColor = { borderColor }
borderRadius = "lg"
boxShadow = "lg"
maxWidth = "500px"
paddingLeft = "2"
paddingRight = "4"
paddingY = "2"
2021-01-04 01:17:30 -08:00
transition = "all 0.2s"
2020-10-07 09:48:55 -07:00
>
2020-10-22 16:01:54 -07:00
{ children }
< / B o x >
2020-10-07 09:48:55 -07:00
) ;
}
2020-10-22 16:01:54 -07:00
function FeedbackFormPitch ( ) {
2020-10-07 09:48:55 -07:00
return (
< Flex direction = "column" textAlign = "left" opacity = "0.9" >
< Box as = "header" > Hi friends ! Welcome to the beta ! < / B o x >
< Box as = "p" fontSize = "sm" >
This is the new Dress to Impress ! It ' s ready for the future , and it even
works great on mobile ! More coming soon !
< / B o x >
2021-01-19 15:46:21 -08:00
< Flex direction = { { base : "column" , sm : "row" } } >
< Box
as = "section"
fontSize = "sm"
marginY = "2"
flex = { { base : "0 0 auto" , sm : "0 1 50%" } }
>
< Box as = "h3" fontWeight = "600" >
2021-06-21 13:55:54 -07:00
New updates ( June 21 )
2021-01-19 15:46:21 -08:00
< / B o x >
< UnorderedList >
2021-06-21 13:55:54 -07:00
< ListItem > Faster outfit editing < / L i s t I t e m >
2021-06-18 18:35:53 -07:00
< ListItem > Better item list pages < / L i s t I t e m >
2021-05-05 00:23:41 -07:00
< ListItem > Outfit saving ! Try it out ! ! < / L i s t I t e m >
2021-03-13 03:18:32 -08:00
< ListItem >
< Link
href = "https://twitter.com/NeopetsDTI"
textDecoration = "underline"
>
See more on Twitter !
< / L i n k >
< / L i s t I t e m >
2021-01-19 15:46:21 -08:00
< / U n o r d e r e d L i s t >
< / B o x >
< Box width = "2" / >
< Box
as = "section"
fontSize = "sm"
marginY = "2"
flex = { { base : "0 0 auto" , sm : "0 1 50%" } }
>
< Box as = "h3" fontWeight = "600" >
Coming soon
< / B o x >
< UnorderedList >
2021-06-21 13:55:54 -07:00
< ListItem > Better outfit editing on large screens < / L i s t I t e m >
2021-06-18 18:35:53 -07:00
< ListItem > Item list editing < / L i s t I t e m >
2021-01-19 15:46:21 -08:00
< ListItem >
2021-05-05 00:23:41 -07:00
… a lot of little things { " " }
2021-01-19 15:46:21 -08:00
< span role = "img" aria - label = "Sweat smile emoji" >
😅
< / s p a n >
< / L i s t I t e m >
< / U n o r d e r e d L i s t >
< / B o x >
< / F l e x >
2020-10-22 16:01:54 -07:00
< Box fontSize = "sm" marginTop = "1" >
↓ Got ideas ? Send them to us , please ! { " " }
< span role = "img" aria - label = "Sparkle heart emoji" >
💖
< / s p a n >
2020-10-07 09:48:55 -07:00
< / B o x >
< / F l e x >
) ;
}
2020-10-22 16:01:54 -07:00
function FeedbackForm ( ) {
2020-10-07 09:48:55 -07:00
const [ content , setContent ] = React . useState ( "" ) ;
2020-10-09 08:18:18 -07:00
const [ email , setEmail ] = useLocalStorage ( "DTIFeedbackFormEmail" , "" ) ;
const [ isSending , setIsSending ] = React . useState ( false ) ;
2020-10-07 09:48:55 -07:00
const toast = useToast ( ) ;
2020-10-09 08:18:18 -07:00
const onSubmit = React . useCallback (
( e ) => {
e . preventDefault ( ) ;
fetch ( "/api/sendFeedback" , {
method : "POST" ,
2020-10-09 08:55:03 -07:00
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { content , email } ) ,
2020-10-09 08:18:18 -07:00
} )
. then ( ( res ) => {
if ( ! res . ok ) {
throw new Error ( ` /api/sendFeedback returned status ${ res . status } ` ) ;
}
setIsSending ( false ) ;
setContent ( "" ) ;
toast ( {
status : "success" ,
title : "Got it! We'll take a look soon." ,
description :
"Thanks for helping us get better! Best wishes to you and your " +
"pets!!" ,
} ) ;
} )
. catch ( ( e ) => {
setIsSending ( false ) ;
console . error ( e ) ;
toast ( {
status : "warning" ,
title : "Oops, we had an error sending this, sorry!" ,
description :
"We'd still love to hear from you! Please reach out to " +
"matchu@openneo.net with whatever's on your mind. Thanks and " +
"enjoy the site!" ,
duration : null ,
isClosable : true ,
} ) ;
} ) ;
setIsSending ( true ) ;
} ,
2020-10-22 16:01:54 -07:00
[ content , email , toast ]
2020-10-09 08:18:18 -07:00
) ;
2021-01-04 01:17:30 -08:00
const { brightBackground } = useCommonStyles ( ) ;
2020-10-07 09:48:55 -07:00
return (
< Box
as = "form"
// We use Grid here rather than our usual Flex, mainly so the fields will
// tab in the correct order!
display = "grid"
gridTemplateAreas = { ` "email send" "content content" ` }
gridTemplateColumns = "1fr auto"
gridGap = "2"
2020-10-09 08:18:18 -07:00
onSubmit = { onSubmit }
2020-10-07 09:48:55 -07:00
>
< Input
type = "email"
placeholder = "Email address (optional)"
size = "sm"
gridArea = "email"
2020-10-09 08:18:18 -07:00
value = { email }
onChange = { ( e ) => setEmail ( e . target . value ) }
2021-01-04 01:17:30 -08:00
background = { brightBackground }
2020-10-07 09:48:55 -07:00
/ >
< Textarea
size = "sm"
placeholder = { "I love…\nI wish…\nNext, you should add…" }
gridArea = "content"
value = { content }
onChange = { ( e ) => setContent ( e . target . value ) }
2021-01-04 01:17:30 -08:00
background = { brightBackground }
2020-10-07 09:48:55 -07:00
/ >
< Button
type = "submit"
size = "sm"
colorScheme = "blue"
gridArea = "send"
2020-10-22 16:01:54 -07:00
isDisabled = { content . trim ( ) . length === 0 }
2020-10-09 08:18:18 -07:00
isLoading = { isSending }
2020-10-07 09:48:55 -07:00
>
Send
< / B u t t o n >
< / B o x >
) ;
}
2020-07-31 16:57:37 -07:00
/ * *
* useSupportSetup helps our support staff get set up with special access .
* If you provide ? supportSecret = ... in the URL , we ' ll save it in a cookie and
* pop up a toast !
*
* This doesn 't guarantee the secret is correct, of course! We don' t bother to
* check that here ; the server will reject requests from bad support secrets .
* And there 's nothing especially secret in the support UI, so it' s okay if
* other people know about the tools and poke around a powerless interface !
* /
function useSupportSetup ( ) {
const location = useLocation ( ) ;
const toast = useToast ( ) ;
React . useEffect ( ( ) => {
const params = new URLSearchParams ( location . search ) ;
const supportSecret = params . get ( "supportSecret" ) ;
2020-08-01 00:42:36 -07:00
const existingSupportSecret = localStorage . getItem ( "supportSecret" ) ;
2020-07-31 16:57:37 -07:00
2020-08-01 00:42:36 -07:00
if ( supportSecret && supportSecret !== existingSupportSecret ) {
2020-07-31 16:57:37 -07:00
localStorage . setItem ( "supportSecret" , supportSecret ) ;
toast ( {
title : "Support secret saved!" ,
description :
` You should now see special Support UI across the site. ` +
` Thanks for your help! 💖 ` ,
status : "success" ,
duration : 10000 ,
isClosable : true ,
} ) ;
} else if ( supportSecret === "" ) {
localStorage . removeItem ( "supportSecret" ) ;
toast ( {
title : "Support secret deleted." ,
description : ` The Support UI will now stop appearing on this device. ` ,
status : "success" ,
duration : 10000 ,
isClosable : true ,
} ) ;
}
} , [ location , toast ] ) ;
}
2020-05-10 00:21:04 -07:00
export default HomePage ;