diff --git a/package-lock.json b/package-lock.json index e3bf638..7ec18af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@bigcommerce/big-design-icons": "^0.23.2", "@bigcommerce/big-design-theme": "^0.19.2", "@google-ai/generativelanguage": "^0.2.1", + "@headlessui/react": "^1.7.16", "@heroicons/react": "^2.0.18", "@t3-oss/env-nextjs": "^0.3.1", "@types/jsonwebtoken": "^9.0.2", @@ -1257,6 +1258,21 @@ "node": ">=6" } }, + "node_modules/@headlessui/react": { + "version": "1.7.16", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.16.tgz", + "integrity": "sha512-2MphIAZdSUacZBT6EXk8AJkj+EuvaaJbtCyHTJrPsz8inhzCl7qeNPI1uk1AUvCgWylVtdN8cVVmnhUDPxPy3g==", + "dependencies": { + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/@heroicons/react": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.18.tgz", diff --git a/package.json b/package.json index 8ebf766..ffbf193 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@bigcommerce/big-design-icons": "^0.23.2", "@bigcommerce/big-design-theme": "^0.19.2", "@google-ai/generativelanguage": "^0.2.1", + "@headlessui/react": "^1.7.16", "@heroicons/react": "^2.0.18", "@t3-oss/env-nextjs": "^0.3.1", "@types/jsonwebtoken": "^9.0.2", @@ -78,4 +79,4 @@ "tls": false, "child_process": false } -} \ No newline at end of file +} diff --git a/public/images/logo.webp b/public/images/logo.webp new file mode 100644 index 0000000..08cf51c Binary files /dev/null and b/public/images/logo.webp differ diff --git a/public/images/partner-logo.png b/public/images/partner-logo.png new file mode 100644 index 0000000..d284d5f Binary files /dev/null and b/public/images/partner-logo.png differ diff --git a/src/components/HomePage.tsx b/src/components/HomePage.tsx index 0085457..e512596 100644 --- a/src/components/HomePage.tsx +++ b/src/components/HomePage.tsx @@ -1,26 +1,34 @@ 'use client'; -import { Flex } from '@bigcommerce/big-design'; -import { NextLink } from '~/components/NextLink'; +import '~/styles/home.css'; + +import Footer from '~/components/HomePage/Footer'; +import Header from '~/components/HomePage/Header'; export const HomePage = () => { return ( - -

Home Page TBD

- - - Accessing the products page for the first time without visiting the - product review through the app extension will result in access token - error. - -
- All Products -
-
+
+
+
+
+
+ {/* Hero content */} +
+ {/* Section header */} +
+

+ Welcome to +
+ + REVIEW PULSE + +

+
+
+
+
+
+
); }; diff --git a/src/components/HomePage/Dropdown.tsx b/src/components/HomePage/Dropdown.tsx new file mode 100644 index 0000000..9ff32db --- /dev/null +++ b/src/components/HomePage/Dropdown.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useState } from "react"; +import { Transition } from "@headlessui/react"; + +type DropdownProps = { + children: React.ReactNode; + title: string; +}; + +export default function Dropdown({ children, title }: DropdownProps) { + const [dropdownOpen, setDropdownOpen] = useState(false); + + return ( +
  • setDropdownOpen(true)} + onMouseLeave={() => setDropdownOpen(false)} + onFocus={() => setDropdownOpen(true)} + onBlur={() => setDropdownOpen(false)} + > + e.preventDefault()} + > + {title} + + + + + + {children} + +
  • + ); +} diff --git a/src/components/HomePage/Footer.tsx b/src/components/HomePage/Footer.tsx new file mode 100644 index 0000000..2fdddd9 --- /dev/null +++ b/src/components/HomePage/Footer.tsx @@ -0,0 +1,50 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +import Logo from '~/components/HomePage/Logo'; + +export default function Footer() { + return ( + + ); +} diff --git a/src/components/HomePage/Header.tsx b/src/components/HomePage/Header.tsx new file mode 100644 index 0000000..f20652e --- /dev/null +++ b/src/components/HomePage/Header.tsx @@ -0,0 +1,66 @@ +'use client'; +import Link from 'next/link'; + +import { useEffect, useState } from 'react'; +import Logo from '~/components/HomePage/Logo'; +import MobileMenu from '~/components/HomePage/MobileMenu'; + +export default function Header() { + const [top, setTop] = useState(true); + + // detect whether user has scrolled the page down by 10px + const scrollHandler = () => { + window.pageYOffset > 10 ? setTop(false) : setTop(true); + }; + + useEffect(() => { + scrollHandler(); + window.addEventListener('scroll', scrollHandler); + return () => window.removeEventListener('scroll', scrollHandler); + }, [top]); + + return ( +
    +
    +
    + {/* Site branding */} +
    + +
    + + {/* Desktop navigation */} + + + +
    +
    +
    + ); +} diff --git a/src/components/HomePage/Logo.tsx b/src/components/HomePage/Logo.tsx new file mode 100644 index 0000000..a689c11 --- /dev/null +++ b/src/components/HomePage/Logo.tsx @@ -0,0 +1,16 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +export default function Logo() { + return ( + + Review Pulse + + ); +} diff --git a/src/components/HomePage/MobileMenu.tsx b/src/components/HomePage/MobileMenu.tsx new file mode 100644 index 0000000..91cb2d3 --- /dev/null +++ b/src/components/HomePage/MobileMenu.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { Transition } from '@headlessui/react'; +import Link from 'next/link'; +import { useEffect, useRef, useState } from 'react'; + +export default function MobileMenu() { + const [mobileNavOpen, setMobileNavOpen] = useState(false); + + const trigger = useRef(null); + const mobileNav = useRef(null); + + // close the mobile menu on click outside + useEffect(() => { + const clickHandler = ({ target }: { target: EventTarget | null }): void => { + if (!mobileNav.current || !trigger.current) return; + if ( + !mobileNavOpen || + mobileNav.current.contains(target as Node) || + trigger.current.contains(target as Node) + ) + return; + setMobileNavOpen(false); + }; + document.addEventListener('click', clickHandler); + return () => document.removeEventListener('click', clickHandler); + }); + + // close the mobile menu if the esc key is pressed + useEffect(() => { + const keyHandler = ({ keyCode }: { keyCode: number }): void => { + if (!mobileNavOpen || keyCode !== 27) return; + setMobileNavOpen(false); + }; + document.addEventListener('keydown', keyHandler); + return () => document.removeEventListener('keydown', keyHandler); + }); + + return ( +
    + {/* Hamburger button */} + + + {/*Mobile navigation */} +
    + +
      +
    • + setMobileNavOpen(false)} + target="_blank" + > + Info Page + + + + +
    • +
    +
    +
    +
    + ); +} diff --git a/src/server/google-ai/generate-email.ts b/src/server/google-ai/generate-email.ts index f423719..3d7ef64 100644 --- a/src/server/google-ai/generate-email.ts +++ b/src/server/google-ai/generate-email.ts @@ -30,7 +30,7 @@ export async function generateAISuggestedEmail( ) { const promptEmailBody = `Role: E-commerce customer care expert analyzing product reviews and outputting a result as a string. -Task: Based on the input provided, generate a suggested email body response to the customer. Only provide information based on data you are provided with, don't invent or assume any facts, and don't include any placeholders. The email signature should be "Sincerely, the ildecimo team". +Task: Based on the input provided, generate a suggested email body response to the customer. Only provide information based on data you are provided with, don't invent or assume any facts, and don't include any placeholders. The email signature must be "Sincerely, the ildecimo team". Input Format: @@ -43,12 +43,12 @@ Input Format: Output Format: string Input Data: -- "Review Title:" ${options.title}, -- "Review Description": ${options.text}, -- "Review Rating": ${options.rating}, -- "Customer Name": ${options.customer}, +- "Review Title:" ${options.title} +- "Review Description": ${options.text} +- "Review Rating": ${options.rating} +- "Customer Name": ${options.customer} - "Email Type": ${options.emailType} - `; +`; const promptEmailSubject = ( emailBody: string @@ -104,7 +104,7 @@ Input Data: if (outputEmailBody) { const parsedOutput = generateEmailOutputSchema.safeParse({ subject: outputEmailSubject, - body: outputEmailBody, + body: outputEmailBody.replaceAll('\n', '%0D%0A'), }); if (!parsedOutput.success) { diff --git a/src/styles/additional-styles/range-slider.css b/src/styles/additional-styles/range-slider.css new file mode 100644 index 0000000..6a10824 --- /dev/null +++ b/src/styles/additional-styles/range-slider.css @@ -0,0 +1,57 @@ +/* Range slider */ +:root { + --range-thumb-size: 36px; +} + +input[type=range] { + appearance: none; + background: #ccc; + border-radius: 3px; + height: 6px; + margin-top: (--range-thumb-size - 6px) * 0.5; + margin-bottom: (--range-thumb-size - 6px) * 0.5; + --thumb-size: #{--range-thumb-size}; +} + +input[type=range]::-webkit-slider-thumb { + appearance: none; + -webkit-appearance: none; + background-color: #000; + background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 .5v7L12 4zM0 4l4 3.5v-7z' fill='%23FFF' fill-rule='nonzero'/%3E%3C/svg%3E"); + background-position: center; + background-repeat: no-repeat; + border: 0; + border-radius: 50%; + cursor: pointer; + height: --range-thumb-size; + width: --range-thumb-size; +} + +input[type=range]::-moz-range-thumb { + background-color: #000; + background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 .5v7L12 4zM0 4l4 3.5v-7z' fill='%23FFF' fill-rule='nonzero'/%3E%3C/svg%3E"); + background-position: center; + background-repeat: no-repeat; + border: 0; + border: none; + border-radius: 50%; + cursor: pointer; + height: --range-thumb-size; + width: --range-thumb-size; +} + +input[type=range]::-ms-thumb { + background-color: #000; + background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 .5v7L12 4zM0 4l4 3.5v-7z' fill='%23FFF' fill-rule='nonzero'/%3E%3C/svg%3E"); + background-position: center; + background-repeat: no-repeat; + border: 0; + border-radius: 50%; + cursor: pointer; + height: --range-thumb-size; + width: --range-thumb-size; +} + +input[type=range]::-moz-focus-outer { + border: 0; +} \ No newline at end of file diff --git a/src/styles/additional-styles/theme.css b/src/styles/additional-styles/theme.css new file mode 100644 index 0000000..bba8017 --- /dev/null +++ b/src/styles/additional-styles/theme.css @@ -0,0 +1,181 @@ +html { + scroll-behavior: smooth; +} + +.form-input:focus, +.form-textarea:focus, +.form-multiselect:focus, +.form-select:focus, +.form-checkbox:focus, +.form-radio:focus { + @apply ring-0; +} + +/* Hamburger button */ +.hamburger svg>*:nth-child(1), +.hamburger svg>*:nth-child(2), +.hamburger svg>*:nth-child(3) { + transform-origin: center; + transform: rotate(0deg); +} + +.hamburger svg>*:nth-child(1) { + transition: y 0.1s 0.25s ease-in, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19), opacity 0.1s ease-in; +} + +.hamburger svg>*:nth-child(2) { + transition: transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19); +} + +.hamburger svg>*:nth-child(3) { + transition: y 0.1s 0.25s ease-in, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19), width 0.1s 0.25s ease-in; +} + +.hamburger.active svg>*:nth-child(1) { + opacity: 0; + y: 11; + transform: rotate(225deg); + transition: y 0.1s ease-out, transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1), opacity 0.1s 0.12s ease-out; +} + +.hamburger.active svg>*:nth-child(2) { + transform: rotate(225deg); + transition: transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1); +} + +.hamburger.active svg>*:nth-child(3) { + y: 11; + transform: rotate(135deg); + transition: y 0.1s ease-out, transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1), width 0.1s ease-out; +} + +/* Pulsing animation */ +@keyframes pulseLoop { + 0% { opacity: .15; transform: scale(1) translateZ(0); } + 30% { opacity: .15; } + 60% { opacity: 0; } + 80% { opacity: 0; transform: scale(1.8) translateZ(0); } +} +@keyframes pulseMiniLoop { + 0% { opacity: 0; transform: scale(1) translateZ(0); } + 30% { opacity: .3; } + 50% { opacity: .3; } + 80% { opacity: 0; transform: scale(3) translateZ(0); } +} +.pulse { + transform: scale(1); + opacity: 0; + transform-origin: center; + animation: pulseLoop 10000ms linear infinite; +} +.pulse-mini { + animation: pulseMiniLoop 6000ms linear infinite; +} +.pulse-1 { + animation-delay: -3000ms; +} +.pulse-2 { + animation-delay: -6000ms; +} + +/* Animations delay */ +.animation-delay-500 { + animation-delay: 500ms !important; +} + +.animation-delay-1000 { + animation-delay: 1000ms !important; +} + +.translate-z-0 { + transform: translateZ(0); +} + +/* Custom AOS animations */ +[data-aos="zoom-y-out"] { + transform: scaleX(1.03); + opacity: 0; + transition-property: transform, opacity; +} + +@media screen { + html:not(.no-js) body [data-aos=fade-up] { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + + html:not(.no-js) body [data-aos=fade-down] { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + + html:not(.no-js) body [data-aos=fade-right] { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + html:not(.no-js) body [data-aos=fade-left] { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } + + html:not(.no-js) body [data-aos=fade-up-right] { + -webkit-transform: translate3d(-10px, 10px, 0); + transform: translate3d(-10px, 10px, 0); + } + + html:not(.no-js) body [data-aos=fade-up-left] { + -webkit-transform: translate3d(10px, 10px, 0); + transform: translate3d(10px, 10px, 0); + } + + html:not(.no-js) body [data-aos=fade-down-right] { + -webkit-transform: translate3d(-10px, -10px, 0); + transform: translate3d(-10px, -10px, 0); + } + + html:not(.no-js) body [data-aos=fade-down-left] { + -webkit-transform: translate3d(10px, -10px, 0); + transform: translate3d(10px, -10px, 0); + } + + html:not(.no-js) body [data-aos=zoom-in-up] { + -webkit-transform: translate3d(0, 10px, 0) scale(.6); + transform: translate3d(0, 10px, 0) scale(.6); + } + + html:not(.no-js) body [data-aos=zoom-in-down] { + -webkit-transform: translate3d(0, -10px, 0) scale(.6); + transform: translate3d(0, -10px, 0) scale(.6); + } + + html:not(.no-js) body [data-aos=zoom-in-right] { + -webkit-transform: translate3d(-10px, 0, 0) scale(.6); + transform: translate3d(-10px, 0, 0) scale(.6); + } + + html:not(.no-js) body [data-aos=zoom-in-left] { + -webkit-transform: translate3d(10px, 0, 0) scale(.6); + transform: translate3d(10px, 0, 0) scale(.6); + } + + html:not(.no-js) body [data-aos=zoom-out-up] { + -webkit-transform: translate3d(0, 10px, 0) scale(1.2); + transform: translate3d(0, 10px, 0) scale(1.2); + } + + html:not(.no-js) body [data-aos=zoom-out-down] { + -webkit-transform: translate3d(0, -10px, 0) scale(1.2); + transform: translate3d(0, -10px, 0) scale(1.2); + } + + html:not(.no-js) body [data-aos=zoom-out-right] { + -webkit-transform: translate3d(-10px, 0, 0) scale(1.2); + transform: translate3d(-10px, 0, 0) scale(1.2); + } + + html:not(.no-js) body [data-aos=zoom-out-left] { + -webkit-transform: translate3d(10px, 0, 0) scale(1.2); + transform: translate3d(10px, 0, 0) scale(1.2); + } +} \ No newline at end of file diff --git a/src/styles/additional-styles/toggle-switch.css b/src/styles/additional-styles/toggle-switch.css new file mode 100644 index 0000000..5d52d9d --- /dev/null +++ b/src/styles/additional-styles/toggle-switch.css @@ -0,0 +1,28 @@ +/* Switch element */ +.form-switch { + @apply relative select-none; + width: 68px; +} + +.form-switch label { + @apply block overflow-hidden cursor-pointer rounded; + height: 38px; +} + +.form-switch label>span:first-child { + @apply absolute block rounded shadow; + width: 30px; + height: 30px; + top: 4px; + left: 4px; + right: 50%; + transition: all .15s ease-out; +} + +.form-switch input[type="checkbox"]:checked+label { + @apply bg-blue-600; +} + +.form-switch input[type="checkbox"]:checked+label>span:first-child { + left: 34px; +} diff --git a/src/styles/additional-styles/utility-patterns.css b/src/styles/additional-styles/utility-patterns.css new file mode 100644 index 0000000..383a338 --- /dev/null +++ b/src/styles/additional-styles/utility-patterns.css @@ -0,0 +1,79 @@ +/* Typography */ +.h1 { + @apply text-4xl font-extrabold leading-tight tracking-tighter; +} + +.h2 { + @apply text-3xl font-extrabold leading-tight tracking-tighter; +} + +.h3 { + @apply text-3xl font-bold leading-tight; +} + +.h4 { + @apply text-2xl font-bold leading-snug tracking-tight; +} + +@screen md { + .h1 { + @apply text-5xl; + } + + .h2 { + @apply text-4xl; + } +} + +/* Buttons */ +.btn, +.btn-sm { + @apply font-medium inline-flex items-center justify-center border border-transparent rounded leading-snug transition duration-150 ease-in-out; +} + +.btn { + @apply px-8 py-3 shadow-lg; +} + +.btn-sm { + @apply px-4 py-2 shadow; +} + +/* Forms */ +.form-input, +.form-textarea, +.form-multiselect, +.form-select, +.form-checkbox, +.form-radio { + @apply bg-white border border-gray-300 focus:border-gray-500; +} + +.form-input, +.form-textarea, +.form-multiselect, +.form-select, +.form-checkbox { + @apply rounded; +} + +.form-input, +.form-textarea, +.form-multiselect, +.form-select { + @apply py-3 px-4; +} + +.form-input, +.form-textarea { + @apply placeholder-gray-500; +} + +.form-select { + @apply pr-10; +} + +.form-checkbox, +.form-radio { + @apply text-gray-800 rounded-sm; +} \ No newline at end of file diff --git a/src/styles/home.css b/src/styles/home.css new file mode 100644 index 0000000..4e9ec51 --- /dev/null +++ b/src/styles/home.css @@ -0,0 +1,5 @@ +/* Additional styles */ +@import 'additional-styles/utility-patterns.css'; +@import 'additional-styles/range-slider.css'; +@import 'additional-styles/toggle-switch.css'; +@import 'additional-styles/theme.css'; \ No newline at end of file