diff --git a/app/controllers/onboarding_controller.rb b/app/controllers/onboarding_controller.rb index 13b3b9f2..9d5317c9 100644 --- a/app/controllers/onboarding_controller.rb +++ b/app/controllers/onboarding_controller.rb @@ -67,21 +67,25 @@ def update if last_step?(step["key"]) complete_onboarding else - redirect_to onboarding_path + next_key = OnboardingConfig.step_keys[OnboardingConfig.step_keys.index(step["key"]) + 1] + redirect_to onboarding_path(step: next_key) end end private - # Allows navigating back to a previously answered step via ?step= param + # Allows navigating to a previously answered step or the next reachable step via ?step= param def requested_step return unless params[:step] step = OnboardingConfig.find_step(params[:step]) return unless step + step_index = OnboardingConfig.step_keys.index(step["key"]) answered_keys = current_user.onboarding_responses.pluck(:question_key) - step if answered_keys.include?(step["key"]) + + # Allow if this step is answered (revisiting) or the previous step is answered (advancing) + step if answered_keys.include?(step["key"]) || (step_index.zero? || answered_keys.include?(OnboardingConfig.step_keys[step_index - 1])) end def current_step diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 89cd8cd9..bc48faa6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -75,11 +75,26 @@ def create authorize @project if @project.save - # Redirect to path when created from the onboarding modal so it closes and tooltips update - destination = params[:return_to] == "path" ? path_path : @project - redirect_to destination, notice: "Project created." + if request.headers["X-InertiaUI-Modal"].present? && params[:return_to] != "path" + head :no_content + else + # Onboarding flows use return_to to land on /path autoopen the projects modal + destination = case params[:return_to] + when "path" + path_path + when "path_projects" + path_path(open: "projects", nudge: "read_docs") + else + projects_path + end + redirect_to destination, notice: "Project created." + end else - redirect_back fallback_location: new_project_path, inertia: { errors: @project.errors.messages } + if request.headers["X-InertiaUI-Modal"].present? && params[:return_to] != "path" + render json: { errors: @project.errors.messages }, status: :unprocessable_entity + else + redirect_back fallback_location: new_project_path, inertia: { errors: @project.errors.messages } + end end end @@ -104,16 +119,36 @@ def update authorize @project if @project.update(project_params) - redirect_to @project, notice: "Project updated." + if request.headers["X-InertiaUI-Modal"].present? + head :no_content + else + redirect_to @project, notice: "Project updated." + end else - redirect_back fallback_location: edit_project_path(@project), inertia: { errors: @project.errors.messages } + if request.headers["X-InertiaUI-Modal"].present? + render json: { errors: @project.errors.messages }, status: :unprocessable_entity + else + redirect_back fallback_location: edit_project_path(@project), inertia: { errors: @project.errors.messages } + end end end def destroy authorize @project - @project.discard - redirect_to projects_path, notice: "Project deleted." + + if @project.discard + if request.headers["X-InertiaUI-Modal"].present? + head :no_content + else + redirect_to projects_path, notice: "Project deleted." + end + else + if request.headers["X-InertiaUI-Modal"].present? + render json: { errors: @project.errors.messages }, status: :unprocessable_entity + else + redirect_back fallback_location: project_path(@project), inertia: { errors: @project.errors.messages } + end + end end private diff --git a/app/frontend/components/onboarding/SpeechBubble.tsx b/app/frontend/components/onboarding/SpeechBubble.tsx index f9c7b7a0..f072aaee 100644 --- a/app/frontend/components/onboarding/SpeechBubble.tsx +++ b/app/frontend/components/onboarding/SpeechBubble.tsx @@ -1,27 +1,74 @@ import type { ReactNode } from 'react' +type BubbleDirection = 'left' | 'right' | 'bottom' | 'top' + type BubbleProps = { text?: string children?: ReactNode bg?: string - dir?: string + dir?: BubbleDirection } -const SpeechBubble = ({ text, children, bg = 'white', dir = '' }: BubbleProps) => ( -
- {children ?? text} - {dir === '' ? ( - - - - - ) : ( - - - +const tailByDirection: Record< + BubbleDirection, + { + className: string + width: string + height: string + viewBox: string + outerPoints: string + innerPoints: string + } +> = { + bottom: { + className: 'absolute -bottom-4 left-1/2 -translate-x-1/2', + width: '24', + height: '16', + viewBox: '0 0 24 16', + outerPoints: '0,0 24,0 12,16', + innerPoints: '1,0 23,0 12,14', + }, + left: { + className: 'absolute -left-4 top-1/2 -translate-y-1/2', + width: '16', + height: '24', + viewBox: '0 0 16 24', + outerPoints: '16,0 16,24 0,12', + innerPoints: '16,1 16,23 2,12', + }, + right: { + className: 'absolute -right-4 top-1/2 -translate-y-1/2', + width: '16', + height: '24', + viewBox: '0 0 16 24', + outerPoints: '0,0 0,24 16,12', + innerPoints: '0,1 0,23 14,12', + }, + top: { + className: 'absolute -top-4 left-1/2 -translate-x-1/2', + width: '24', + height: '16', + viewBox: '0 0 24 16', + outerPoints: '12,0 24,16 0,16', + innerPoints: '12,2 23,16 1,16', + }, +} + +const SpeechBubble = ({ text, children, bg = 'white', dir = 'bottom' }: BubbleProps) => { + const tail = tailByDirection[dir] + + return ( +
+ + {children ?? text} + + + + + - )} -
-) +
+ ) +} export default SpeechBubble diff --git a/app/frontend/components/path/BgmPlayer.tsx b/app/frontend/components/path/BgmPlayer.tsx index 1c75352a..280c5197 100644 --- a/app/frontend/components/path/BgmPlayer.tsx +++ b/app/frontend/components/path/BgmPlayer.tsx @@ -176,7 +176,7 @@ export default function BgmPlayer({ hasProjects = false }: { hasProjects?: boole const track = TRACKS[trackIndex] return ( - +

{track.title}

diff --git a/app/frontend/components/path/PathNode.tsx b/app/frontend/components/path/PathNode.tsx index f1b12d70..e4982c13 100644 --- a/app/frontend/components/path/PathNode.tsx +++ b/app/frontend/components/path/PathNode.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useContext } from 'react' -import { usePage } from '@inertiajs/react' +import { Link, usePage } from '@inertiajs/react' // @ts-expect-error useModalStack lacks type declarations in this beta package import { ModalLink, useModalStack } from '@inertiaui/modal-react' import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/shared/Tooltip' @@ -82,9 +82,9 @@ export default function PathNode({
{index === 0 ? ( state === 'active' && interactive ? ( - + {starImage} - + ) : state === 'completed' && interactive ? ( - -
- )} + {invite.status === 'pending' && ( +
+ + +
+ )} - {invite.status === 'accepted' && ( -
-

You accepted this invite.

- - Go to project - -
- )} + {invite.status === 'accepted' && ( +
+

You accepted this invite.

+ + Go to project + +
+ )} - {invite.status === 'declined' &&

You declined this invite.

} + {invite.status === 'declined' &&

You declined this invite.

} - {invite.status === 'revoked' &&

This invite was withdrawn.

} + {invite.status === 'revoked' &&

This invite was withdrawn.

} +
) if (is_modal) { return ( - {content} + {content} ) } diff --git a/app/frontend/pages/journal_entries/new.tsx b/app/frontend/pages/journal_entries/new.tsx index 29093027..61443192 100644 --- a/app/frontend/pages/journal_entries/new.tsx +++ b/app/frontend/pages/journal_entries/new.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef, useMemo, type ReactNode } from 'react' import { Deferred as InertiaDeferred, router } from '@inertiajs/react' -import { Deferred as ModalDeferred, Modal } from '@inertiaui/modal-react' +// @ts-expect-error useModal lacks type declarations in this beta package +import { Deferred as ModalDeferred, Modal, useModal } from '@inertiaui/modal-react' import BookLayout from '@/components/shared/BookLayout' import Button from '@/components/shared/Button' import Input from '@/components/shared/Input' @@ -109,6 +110,7 @@ function NewJournal({ ? projects[0] : null + const modal = useModal() const [selectedProject, setSelectedProject] = useState(initialProject) const [rightTab, setRightTab] = useState<'lapse' | 'youtube' | 'lookout'>('lookout') const [selectedTimelapses, setSelectedTimelapses] = useState>(new Set()) @@ -176,6 +178,20 @@ function NewJournal({ const hasRecording = recordingCount > 0 const canSubmit = selectedProject && hasRecording && hasEnoughImages && hasEnoughChars + function handleBack() { + if (modal?.canGoBack) { + modal.goBack() + return + } + + if (modal) { + modal.close() + return + } + + modalRef.current?.close() + } + function toggleTimelapse(id: string) { setSelectedTimelapses((prev) => { const next = new Set(prev) @@ -397,8 +413,8 @@ function NewJournal({
{is_modal && ( @@ -592,12 +608,14 @@ function NewJournal({ return ( - {content} + + {content} + {fullscreenModal} ) @@ -613,8 +631,8 @@ function NewJournal({ function DisabledOverlay() { return ( -
-

Select a project at the top left first!

+
+

Select a project at the top left first!

) } diff --git a/app/frontend/pages/landing/index.tsx b/app/frontend/pages/landing/index.tsx index a3be8fa4..c218609a 100644 --- a/app/frontend/pages/landing/index.tsx +++ b/app/frontend/pages/landing/index.tsx @@ -10,13 +10,41 @@ import { gsap } from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' const PROJECTS = [ - { image: '/landing/projects/cyberpad.webp', description: 'A sleek, cyberpunk inspired macropad', credit: 'By Kai, a 17-year-old from Canada' }, - { image: '/landing/projects/glowpad.webp', description: 'A glowing macropad', credit: 'By Raygen, a 17-year-old from the US' }, - { image: '/landing/projects/angel.webp', description: 'A biblically accurate angel, as a macropad', credit: 'By Alex, a 15-year-old from the US' }, - { image: '/landing/projects/meko.webp', description: 'A high definition music player', credit: 'By Marcell, a 17-year-old from Romania' }, - { image: '/landing/projects/bitlace.webp', description: 'A cool looking retro accessory with an 8x8 monochrome screen', credit: 'By Vladislav, a 17-year-old from Russia' }, - { image: '/landing/projects/3dpmotherboard.webp', description: 'A powerful, yet affordable 3D printer motherboard', credit: 'By Kai, a 17-year-old from Canada' }, - { image: '/landing/projects/twoswap.webp', description: 'An epic wearable TV head', credit: 'By Nick, an 18-year-old from the US' }, + { + image: '/landing/projects/cyberpad.webp', + description: 'A sleek, cyberpunk inspired macropad', + credit: 'By Kai, a 17-year-old from Canada', + }, + { + image: '/landing/projects/glowpad.webp', + description: 'A glowing macropad', + credit: 'By Raygen, a 17-year-old from the US', + }, + { + image: '/landing/projects/angel.webp', + description: 'A biblically accurate angel, as a macropad', + credit: 'By Alex, a 15-year-old from the US', + }, + { + image: '/landing/projects/meko.webp', + description: 'A high definition music player', + credit: 'By Marcell, a 17-year-old from Romania', + }, + { + image: '/landing/projects/bitlace.webp', + description: 'A cool looking retro accessory with an 8x8 monochrome screen', + credit: 'By Vladislav, a 17-year-old from Russia', + }, + { + image: '/landing/projects/3dpmotherboard.webp', + description: 'A powerful, yet affordable 3D printer motherboard', + credit: 'By Kai, a 17-year-old from Canada', + }, + { + image: '/landing/projects/twoswap.webp', + description: 'An epic wearable TV head', + credit: 'By Nick, an 18-year-old from the US', + }, ] export default function LandingIndex() { @@ -327,9 +355,15 @@ export default function LandingIndex() { rafId = requestAnimationFrame(animate) - const onWheel = (e: WheelEvent) => { carouselDirectionRef.current = e.deltaY > 0 ? 1 : -1 } - const onEnter = () => { carouselTargetSpeedRef.current = 0 } - const onLeave = () => { carouselTargetSpeedRef.current = BASE_SPEED } + const onWheel = (e: WheelEvent) => { + carouselDirectionRef.current = e.deltaY > 0 ? 1 : -1 + } + const onEnter = () => { + carouselTargetSpeedRef.current = 0 + } + const onLeave = () => { + carouselTargetSpeedRef.current = BASE_SPEED + } const section = carouselSectionRef.current section?.addEventListener('wheel', onWheel, { passive: true }) @@ -569,7 +603,6 @@ export default function LandingIndex() { {cardCaptions[cardOrder[cardOrder.length - 1]].label} @@ -579,8 +612,18 @@ export default function LandingIndex() {
My parents are worried!
@@ -636,9 +679,15 @@ export default function LandingIndex() {
-