diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 4dcb4390..bb4f312b 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,19 +2,20 @@ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react/jsx-runtime', - 'plugin:react-hooks/recommended', + "eslint:recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, - settings: { react: { version: '18.2' } }, - plugins: ['react-refresh'], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parserOptions: { ecmaVersion: "latest", sourceType: "module" }, + settings: { react: { version: "18.2" } }, + plugins: ["react-refresh"], rules: { - 'react-refresh/only-export-components': [ - 'warn', + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], + "react/prop-types": "off", }, -} +}; diff --git a/README.md b/README.md index 4f270fdd..f7061d5e 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,22 @@ -

- - Project Banner Image - -

- # Survey Project -Replace this readme with your own information about your project. - -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. - -## Getting Started with the Project - -### Dependency Installation & Startup Development Server +The project involved building a React survey with at least three questions, using different input types (radio buttons, dropdown). Upon submission, users are shown a summary of their answers. The focus was on practicing React state and controlled forms while ensuring accessibility, responsiveness, and clean code. -Once cloned, navigate to the project's root directory and this project uses npm (Node Package Manager) to manage its dependencies. +Collaboration was done via GitHub and we worked on separate branches and then merged the changes to main. -The command below is a combination of installing dependencies, opening up the project on VS Code and it will run a development server on your terminal. +### The Process -```bash -npm i && code . && npm run dev -``` +Prior to our first meeting, Helene set up the design on Figma and also created a flowchart to help us visualize the data flow and state management for our application. -### The Problem +We started with setting up the structure of the project by pair programming using LiveShare and a Slack huddle. When that was done, we had a semi-working flow with state manangement. We could then divide the tasks between us. -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? +One challenge was that our schedules rarely aligned, so after our first meeting we had to find a way to make progress in our own time while avoiding merge conflicts. We set up a Canvas in Slack with an organized to-do list and assigned ourselves various tasks throughout the week. We also included detailed comments in our code and this helped to promote clarity and communication. ### View it live -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. +[View it live »](https://happiness-survey.netlify.app/) -## Instructions +### Collaborators - - See instructions of this project - +Helene Westrin +Joyce Kuo \ No newline at end of file diff --git a/index.html b/index.html index f55e6b37..b309a3b8 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - Survey - Project - Week 6 + Happiness survey by Joyce & Helene
diff --git a/instructions.md b/instructions.md index 197816a6..cb8a8ec1 100644 --- a/instructions.md +++ b/instructions.md @@ -15,6 +15,7 @@ You don't have to use a lot of components to start with when doing this project. How you design your page is up to you, but take accessibility into account when you are styling your form elements - so inputs should have labels and should be easily readable and usable. We STRONGLY recommend having some kind of design or sketch before starting to code. ## How to get started + 1. **Person A** forks the repo 2. **Person A** invites person B as a collaborator to the repo (Settings -> Collaborators and teams -> Add people) 3. **Person A** clones the repo @@ -24,6 +25,7 @@ How you design your page is up to you, but take accessibility into account when 7. When you're done with a feature, it's time to merge! You choose if you want to work with PRs and code reviews or if you want to do this more verbally before you merge. ## Requirements: + - Your app should consist of at least 3 questions. - At least one question should use radio buttons. - At least one question should use a select dropdown. @@ -37,23 +39,27 @@ How you design your page is up to you, but take accessibility into account when - Make your app responsive (it should look good on devices from 320px width up to 1600px) ## Stretch goals + So you’ve completed the requirements? Great job! Ensure you've committed and pushed a version of your project before starting on the stretch goals. Remember that the stretch goals are optional. ### Intermediate Stretch Goals + - Use a form element you haven't tried before (such as a [range slider](https://www.w3schools.com/howto/howto_js_rangeslider.asp)) and connect it to React state. You can find a list of input types [here](https://www.w3schools.com/html/html_form_input_types.asp). - Add validation to your survey! Use either HTML input validation attributes (such as `required`) or implement custom logic when the user clicks the submit button to make the form fields have validations. If you choose to implement your own validation, you should also make sure to show error messages in a nice way. - Create a button that, when clicked, will scroll down to the top of the next question in the survey (if possible) ### Advanced Stretch Goals + - Visualize to the user how far through the survey they are and how much is left by creating a progress bar - Use Regex validation for some input on your survey - Show different questions depending on the answer to a specific question - Create a multi-step form. Example 👇 Show each question on its own 'page' with a continue button to progress to the next question (like how typeform does it). If you decide to split your form into sections, then one approach you could take is to try to think of these sections as a single `useState` hook which you can use to conditionally render different groups of inputs. For example, you could have some state like `const [section, setSection] = useState('firstQuestion')` and then when the user presses a button to progress, you'd use the `setSection()` function to progress them to the second question, etc. Then, in your JSX, you could conditionally render, like this: + ``` const Example = () => { const [section, setSection] = useState('firstQuestion') - + return (
{section === 'firstQuestion' && ( @@ -61,7 +67,7 @@ So you’ve completed the requirements? Great job! Ensure you've committed and p First question...
)} - + {section === 'secondQuestion' && (
Second question... @@ -71,4 +77,5 @@ So you’ve completed the requirements? Great job! Ensure you've committed and p ) } ``` + As always, there are many ways to approach this! This is just one suggestion. diff --git a/package.json b/package.json index 0f1554df..48a12309 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-aria-components": "^1.4.1", + "react-dom": "^18.2.0", + "vite-plugin-svgr": "^4.2.0" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 1091d431..fa34fb08 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,3 +1,53 @@ +import { useState } from "react"; +import { Home } from "./components/Home"; +import { Survey } from "./components/Survey"; +import { Results } from "./components/Results"; + export const App = () => { - return
Find me in src/app.jsx!
; + const [surveyStarted, setSurveyStarted] = useState(false); + const [currentStep, setCurrentStep] = useState(1); + const [userAnswers, setUserAnswers] = useState({ + answer1: "", + answer2: "", + answer3: "", + }); + + const handleSurveySubmit = (event) => { + event.preventDefault(); + setCurrentStep(-1); //Switch to results + }; + + // Function to clear answers and start over + const resetSurvey = () => { + setSurveyStarted(false); + setCurrentStep(1); + setUserAnswers({ + answer1: "", + answer2: "", + answer3: "", + }); + }; + + return ( + <> + {!surveyStarted ? ( + + ) : currentStep === -1 ? ( + + ) : ( + + )} + + ); }; diff --git a/src/assets/arrow-down.svg b/src/assets/arrow-down.svg new file mode 100644 index 00000000..c9eaeb17 --- /dev/null +++ b/src/assets/arrow-down.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/checkmark.svg b/src/assets/checkmark.svg new file mode 100644 index 00000000..9cd7a7f6 --- /dev/null +++ b/src/assets/checkmark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/favicon.svg b/src/assets/favicon.svg new file mode 100644 index 00000000..efc227d2 --- /dev/null +++ b/src/assets/favicon.svg @@ -0,0 +1 @@ +🌸 \ No newline at end of file diff --git a/src/assets/fonts/FamiljenGrotesk-Bold.eot b/src/assets/fonts/FamiljenGrotesk-Bold.eot new file mode 100644 index 00000000..bf777023 Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-Bold.eot differ diff --git a/src/assets/fonts/FamiljenGrotesk-Bold.ttf b/src/assets/fonts/FamiljenGrotesk-Bold.ttf new file mode 100644 index 00000000..590609b0 Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-Bold.ttf differ diff --git a/src/assets/fonts/FamiljenGrotesk-Bold.woff b/src/assets/fonts/FamiljenGrotesk-Bold.woff new file mode 100644 index 00000000..8ef344a3 Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-Bold.woff differ diff --git a/src/assets/fonts/FamiljenGrotesk-Bold.woff2 b/src/assets/fonts/FamiljenGrotesk-Bold.woff2 new file mode 100644 index 00000000..6f838c8c Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-Bold.woff2 differ diff --git a/src/assets/fonts/FamiljenGrotesk-Regular.eot b/src/assets/fonts/FamiljenGrotesk-Regular.eot new file mode 100644 index 00000000..48cdf426 Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-Regular.eot differ diff --git a/src/assets/fonts/FamiljenGrotesk-Regular.ttf b/src/assets/fonts/FamiljenGrotesk-Regular.ttf new file mode 100644 index 00000000..69fd7e2c Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-Regular.ttf differ diff --git a/src/assets/fonts/FamiljenGrotesk-Regular.woff b/src/assets/fonts/FamiljenGrotesk-Regular.woff new file mode 100644 index 00000000..58d77f66 Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-Regular.woff differ diff --git a/src/assets/fonts/FamiljenGrotesk-Regular.woff2 b/src/assets/fonts/FamiljenGrotesk-Regular.woff2 new file mode 100644 index 00000000..6600b891 Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-Regular.woff2 differ diff --git a/src/assets/fonts/FamiljenGrotesk-SemiBold.eot b/src/assets/fonts/FamiljenGrotesk-SemiBold.eot new file mode 100644 index 00000000..d5158d6c Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-SemiBold.eot differ diff --git a/src/assets/fonts/FamiljenGrotesk-SemiBold.ttf b/src/assets/fonts/FamiljenGrotesk-SemiBold.ttf new file mode 100644 index 00000000..9e5ffd82 Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-SemiBold.ttf differ diff --git a/src/assets/fonts/FamiljenGrotesk-SemiBold.woff b/src/assets/fonts/FamiljenGrotesk-SemiBold.woff new file mode 100644 index 00000000..f13202ba Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-SemiBold.woff differ diff --git a/src/assets/fonts/FamiljenGrotesk-SemiBold.woff2 b/src/assets/fonts/FamiljenGrotesk-SemiBold.woff2 new file mode 100644 index 00000000..1cfe8235 Binary files /dev/null and b/src/assets/fonts/FamiljenGrotesk-SemiBold.woff2 differ diff --git a/src/assets/home.png b/src/assets/home.png new file mode 100644 index 00000000..44e3dc03 Binary files /dev/null and b/src/assets/home.png differ diff --git a/src/assets/results.png b/src/assets/results.png new file mode 100644 index 00000000..0709b453 Binary files /dev/null and b/src/assets/results.png differ diff --git a/src/components/Home.jsx b/src/components/Home.jsx new file mode 100644 index 00000000..2cba5b6f --- /dev/null +++ b/src/components/Home.jsx @@ -0,0 +1,18 @@ +import { Button } from "./ui/Button"; +import homeImage from "../assets/home.png"; + +export const Home = ({ setSurveyStarted }) => { + return ( +
+ Cartoon of a person relaxed and sleeping on the sofa with abstract shapes in the background +

The Science of Happiness

+

Everyday Joy Boosters Survey

+
+ ); +}; diff --git a/src/components/ProgressIndicator.css b/src/components/ProgressIndicator.css new file mode 100644 index 00000000..9fa6fa79 --- /dev/null +++ b/src/components/ProgressIndicator.css @@ -0,0 +1,35 @@ +.progress-indicator { + display: flex; + justify-content: center; + gap: 1.75rem; +} + +.circle { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: var(--pink-dark); + border: 2px solid var(--pink-dark); + font-size: 1.5rem; /* 24 (font-size you are aiming for) divided by 16 (default root font size) = 1.5 */ + font-weight: 700; + + span { + position: relative; + top: -2px; + } + + svg { + fill: var(--blue-dark); + position: relative; + top: 5px; + } +} + +.circle.active, +.circle.completed { + background-color: var(--pink-dark); + color: var(--blue-dark); +} diff --git a/src/components/ProgressIndicator.jsx b/src/components/ProgressIndicator.jsx new file mode 100644 index 00000000..c9c38676 --- /dev/null +++ b/src/components/ProgressIndicator.jsx @@ -0,0 +1,45 @@ +import "./ProgressIndicator.css"; +import Checkmark from "../assets/checkmark.svg?react"; + +export const ProgressIndicator = ({ currentStep }) => { + // Function that determines which CSS class to apply to each circle based on current step + const getCircleClass = (step) => { + if (step < currentStep) return "completed"; + if (step === currentStep) return "active"; + return ""; + }; + + const setAriaDisabled = (step) => { + if (step < currentStep || step > currentStep) return "true"; + if (step === currentStep) return "false"; + }; + + return ( +
+ {/* Map iterates over steps 1, 2, 3 and for each step, it calls getCircleClass(step) to determine appropriate class */} + {[1, 2, 3].map((step) => ( +
+ {/* If step is less than current step, it displays a checkmark, otherwise it displays the step number */} + + {step < currentStep ? ( + <> +

Step {step} (completed)

+ + + ) : ( + <> +

+ Step {step} +

+ + )} +
+
+ ))} +
+ ); +}; diff --git a/src/components/Results.jsx b/src/components/Results.jsx new file mode 100644 index 00000000..c15a2087 --- /dev/null +++ b/src/components/Results.jsx @@ -0,0 +1,37 @@ +import { Button } from "./ui/Button"; + +import resultsImage from "../assets/results.png"; + +export const Results = ({ + setSurveyStarted, + setCurrentStep, + userAnswers, + resetSurvey, +}) => { + return ( +
+ Cartoon of a person sitting on top of a stack of oversized books and reading a book +

Your results

+

+ For you, {userAnswers.answer1.toLowerCase()} is a great + way to feel happier, while{" "} + {userAnswers.answer2.toLowerCase()} can also brighten + your mood during the{" "} + {userAnswers.answer3.toLowerCase()}. +

+
+ ); +}; diff --git a/src/components/Survey.css b/src/components/Survey.css new file mode 100644 index 00000000..ccf30d5b --- /dev/null +++ b/src/components/Survey.css @@ -0,0 +1,144 @@ +.survey { + display: flex; + flex-flow: column; + width: 100%; + min-height: 100svh; + + @media (min-width: 1025px) { + flex-flow: row; + } + + > section { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + padding: 2rem 1rem; + + @media (min-width: 1025px) { + padding: 10vw; + } + } +} + +/* ******************** + Form styling +********************* */ + +.form-container { + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 1.25rem !important; + } + + form { + display: flex; + flex-flow: column wrap; + justify-content: space-between; + flex-grow: 1; + gap: 0.75rem; + + @media (min-width: 1025px) { + justify-content: center; + gap: 1.25rem; + } + + fieldset { + border: none; + display: flex; + gap: 0.75rem; + flex-direction: column; + margin: 0; + padding: 0; + + @media (min-width: 1025px) { + gap: 0.875rem; + } + } + + .error { + font-weight: 600; + margin: 0.25rem 0; + } + } + + @media (min-width: 1025px) { + button { + align-self: flex-start; + } + } +} + +input, +label:has(input[type="radio"]), +select { + color: var(--blue-dark); + background-color: var(--white); + border: 2px solid var(--blue-dark); + border-radius: 0.75rem; + + display: flex; + width: 100%; + min-height: 3.75rem; + padding: 1rem 1.25rem 1.375rem 1.25rem; + align-items: center; + gap: 0.625rem; + align-self: stretch; + + font-family: "Familjen Grotesk"; + font-size: 1.25rem; + font-weight: 600; + line-height: 110%; + + transition: box-shadow 0.2s ease-out; + + &:focus, + &:hover { + outline: none; + box-shadow: 0px 0px 0px 4px rgba(0, 49, 116, 0.2); + } +} + +label:has(input[type="radio"]) { + cursor: pointer; + &:before { + content: ""; + display: block; + width: 0; + height: 0; + transition: width 0.2s ease-out; + } +} + +/* Hide the actual radio button */ +input[type="radio"] { + display: none; +} + +/* Styling when radio button is checked */ +label:has(input[type="radio"]:checked) { + background-color: var(--blue-dark); + color: var(--white); + + &:before { + background-image: url("../assets/checkmark.svg"); + background-position: center; + width: 1.5rem; + height: 1.5rem; + position: relative; + top: 2px; + } + + &:hover { + box-shadow: none; + } +} + +select { + appearance: none; + background-image: url("../assets/arrow-down.svg"); + background-size: 1.5rem; + background-repeat: no-repeat; + background-position: right 10px center; +} diff --git a/src/components/Survey.jsx b/src/components/Survey.jsx new file mode 100644 index 00000000..2d425a8c --- /dev/null +++ b/src/components/Survey.jsx @@ -0,0 +1,211 @@ +import { useState, useEffect, useRef } from "react"; +import { SurveyHero } from "./SurveyHero"; +import { RadioButtonGroup } from "./ui/RadioButtonGroup"; +import { Button } from "./ui/Button"; +import "./Survey.css"; + +// The Survey component receives several props that control the current step of the survey, user answers, and form submission +export const Survey = ({ + currentStep, // Tracks which step (or question) the user is currently on + setCurrentStep, // Function to update the current step + userAnswers, // Object holding the user's answers to the questions + setUserAnswers, // Function to update the user's answers + onSubmit, // Callback function for handling form submission on the last step +}) => { + // State to track error messages + const [errorMessage, setErrorMessage] = useState(""); + + // Ref for the SurveyHero title (h1) + const heroTitleRef = useRef(null); + + useEffect(() => { + // When the current step changes, focus on the title element + if (heroTitleRef.current) { + setTimeout(() => { + heroTitleRef.current.focus(); + }, 150); + } + }, [currentStep]); + + // This function updates the user's answers when they interact with an input field (text, checkbox, radio, etc.) + const updateUserAnswers = (event) => { + const { name, value, type, checked } = event.target; // Extracting useful info from the event (which input changed) + setUserAnswers((previous) => ({ + ...previous, // Spread operator to retain previous answers while updating the new answer + [name]: type === "checkbox" ? checked : value, // For checkboxes, use 'checked' instead of 'value' + })); + setErrorMessage(""); // Clear error message when user interacts with form + }; + + // This function advances the user to the next question (increments currentStep by 1) + const onHandleNext = () => { + // If the question is not answered, do not proceed to the next step + if (!areAllFieldsValid()) return; + // Set the next step by increasing the current step by 1 + setCurrentStep(currentStep + 1); + }; + + // Check if the userAnswer object has empty values + const areAllFieldsValid = () => { + const requiredFields = Object.keys(userAnswers).filter((key) => + key.startsWith(`answer${currentStep}`) + ); + return requiredFields.every( + (key) => userAnswers[key] && userAnswers[key].trim() !== "" + ); + }; + + // Function that updates errorMessage state when user tries to click disabled button + const handleDisabledClick = () => { + setErrorMessage("Please answer the question before proceeding."); + }; + + const updateUserAnswersDirect = (name, value) => { + setUserAnswers((previous) => ({ + ...previous, + [name]: value, + })); + setErrorMessage(""); // Clear error message when user interacts with form + }; + + const handleKeyDown = (e, name, value) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); // Prevents default behavior + updateUserAnswersDirect(name, value.toLowerCase()); // Set the selected radio + } + }; + + // Options to pass into the RadioButtonGroup component + const stepTwoOptions = [ + "Listening to music", + "Spending time outdoors", + "Being around friends or family", + "Eating your favorite food", + "Learning something new", + ]; + + // JSX is returned here to render the appropriate question based on the current step + return ( +
+ {currentStep === 1 ? ( // Conditional rendering for Step 1 + <> + +
+
{ + event.preventDefault(); // Prevents the default form submission behavior (reloading the page) + onHandleNext(currentStep, setCurrentStep); // Move to the next step when the form is submitted + }} + > + + {/* Conditionally render

tag to display errorMessage if it has a truthy value */} + {errorMessage && ( +

+ {errorMessage} +

+ )} + {/* handleDisabledClick triggers if button is clicked while disabled */} +
+ + ) : currentStep === 2 ? ( // Conditional rendering for Step 2 + <> + +
+
{ + event.preventDefault(); + onHandleNext(currentStep, setCurrentStep); + }} + > + + {errorMessage && ( +

+ {errorMessage} +

+ )} +
+ + ) : currentStep === 3 ? ( + <> + +
+
{ + event.preventDefault(); + onSubmit(event); + }} + > + {/* This form submits the user's final answers */} + + {errorMessage && ( +

+ {errorMessage} +

+ )} +
+ + ) : ( +

Question not found! 😱

// Fallback for an invalid or unexpected currentStep value + )} +
+ ); +}; diff --git a/src/components/SurveyHero.css b/src/components/SurveyHero.css new file mode 100644 index 00000000..078001e7 --- /dev/null +++ b/src/components/SurveyHero.css @@ -0,0 +1,22 @@ +.hero { + color: var(--white); + background-color: var(--blue-dark); + min-height: 30vh; +} + +.hero-title { + color: var(--white); + font-size: 1.5rem; + line-height: 1.2; + text-align: center; + margin-top: 1.75rem; + + @media (min-width: 375px) { + font-size: 1.875rem; + } + + @media (min-width: 1025px) { + font-size: 2.5rem; + margin-top: 2.5rem; + } +} diff --git a/src/components/SurveyHero.jsx b/src/components/SurveyHero.jsx new file mode 100644 index 00000000..1c64476a --- /dev/null +++ b/src/components/SurveyHero.jsx @@ -0,0 +1,18 @@ +import { forwardRef } from "react"; +import { ProgressIndicator } from "./ProgressIndicator"; +import "./SurveyHero.css"; + +// Use forwardRef to allow ref to be passed to the SurveyHero component +export const SurveyHero = forwardRef(({ currentStep, question, id }, ref) => { + return ( +
+ +

+ {question} +

+
+ ); +}); + +// Add a display name for better debugging +SurveyHero.displayName = "SurveyHero"; diff --git a/src/components/ui/Button.jsx b/src/components/ui/Button.jsx new file mode 100644 index 00000000..74f37e4e --- /dev/null +++ b/src/components/ui/Button.jsx @@ -0,0 +1,20 @@ +import "./button.css"; + +export const Button = ({ onClick, text, disabled, onDisabledClick }) => { + const handleClick = (event) => { + // If the button is disabled, prevent the default action and call the onDisabledClick function + if (disabled) { + event.preventDefault(); + if (onDisabledClick) { + onDisabledClick(); // Trigger custom function if provided + } + } else if (onClick) { + onClick(event); // Normal onClick behavior when not disabled + } + }; + return ( + + ); +}; diff --git a/src/components/ui/RadioButtonGroup.jsx b/src/components/ui/RadioButtonGroup.jsx new file mode 100644 index 00000000..e3ddb71d --- /dev/null +++ b/src/components/ui/RadioButtonGroup.jsx @@ -0,0 +1,31 @@ +export const RadioButtonGroup = ({ + name, + options, + userAnswers, + updateUserAnswers, + handleKeyDown, +}) => ( +
+ {options.map((option) => ( + + ))} +
+); diff --git a/src/components/ui/button.css b/src/components/ui/button.css new file mode 100644 index 00000000..07e72779 --- /dev/null +++ b/src/components/ui/button.css @@ -0,0 +1,45 @@ +button { + appearance: none; + border: none; + + display: flex; + justify-content: center; + align-items: center; + gap: 0.625rem; + cursor: pointer; + font-family: "Familjen Grotesk", "Helvetica Neue", Arial, sans-serif; + min-height: 3.75rem; + padding: 0.875rem 1.75rem 1.25rem 1.75rem; + + border-radius: 1rem; + background: var(--blue-dark); + color: var(--white); + font-size: 1.25rem; + font-weight: 600; + line-height: 110%; + + transition: 0.25s ease-out; + transition-property: background, color; +} + +button:not([aria-disabled="true"]):hover, +button:focus { + color: var(--blue-dark); + background-color: var(--pink-dark); + box-shadow: 0px 3.3px 5.3px rgba(0, 0, 0, 0.028), + 0px 11.2px 17.9px rgba(0, 0, 0, 0.042), 0px 50px 80px rgba(0, 0, 0, 0.07); +} + +button:active { + background-color: var(--pink-light); +} + +button:focus-visible { + outline: 2px solid var(--blue-dark); + outline-offset: 2px; +} + +button[aria-disabled="true"] { + cursor: not-allowed; + background-color: rgba(var(--blue-dark-rgb), 0.5); +} diff --git a/src/index.css b/src/index.css index 4558f538..60aa0334 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,128 @@ +@font-face { + font-family: "Familjen Grotesk"; + src: url("./assets/fonts/FamiljenGrotesk-Regular.woff2") format("woff2"), + url("./assets/fonts/FamiljenGrotesk-Regular.woff") format("woff"), + url("./assets/fonts/FamiljenGrotesk-Regular.ttf") format("truetype"); + font-weight: 400; + font-display: swap; + font-style: normal; +} + +@font-face { + font-family: "Familjen Grotesk"; + src: url("./assets/fonts/FamiljenGrotesk-SemiBold.woff2") format("woff2"), + url("./assets/fonts/FamiljenGrotesk-SemiBold.woff") format("woff"), + url("./assets/fonts/FamiljenGrotesk-SemiBold.ttf") format("truetype"); + font-weight: 600; + font-display: swap; + font-style: normal; +} + +@font-face { + font-family: "Familjen Grotesk"; + src: url("./assets/fonts/FamiljenGrotesk-Bold.woff2") format("woff2"), + url("./assets/fonts/FamiljenGrotesk-Bold.woff") format("woff"), + url("./assets/fonts/FamiljenGrotesk-Bold.ttf") format("truetype"); + font-weight: 700; + font-display: swap; + font-style: normal; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +&:focus-visible { + outline: solid 2px solid; + outline-offset: 2px; + outline-color: red; +} + :root { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; + --blue-dark: #003174; + --blue-dark-rgb: 0, 49, 116; + --blue-medium: #667fa2; + --pink-dark: #ffb4c7; + --pink-light: #fff0f4; + --white: #fff; +} + +html { + font-family: "Familjen Grotesk", "Helvetica Neue", Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; +body { + padding: 0; + margin: 0; + background-color: var(--pink-light); +} + +#root { + display: flex; + justify-content: center; + align-items: center; + min-height: 100svh; + width: 100%; +} + +h1 { + font-size: 2.5rem; + line-height: 1.05; + color: var(--blue-dark); + + &:focus, + &:focus-visible { + outline: none; + } + + @media (min-width: 1025px) { + font-size: 3.75rem; + } +} + +.sr-only { + position: absolute !important; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; + white-space: nowrap; +} + +.main-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 40rem; + text-align: center; + padding: 0 1rem 1rem; + + img { + width: 80vw; + max-width: 100%; + height: auto; + + @media (min-width: 768px) { + width: 25rem; + } + } + + h1 { + margin-block-start: 0; + margin-block-end: 0.35em; + } + + p { + color: var(--blue-dark); + font-size: 1.35rem; + margin: 0 0 1.5rem; + } } diff --git a/vite.config.js b/vite.config.js index 5a33944a..ba3d0846 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import svgr from "vite-plugin-svgr"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [react(), svgr()], +});