Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions app/controllers/onboarding_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 43 additions & 8 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
79 changes: 63 additions & 16 deletions app/frontend/components/onboarding/SpeechBubble.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<div className={`relative bg-${bg} h-auto w-auto p-3 sm:px-6 sm:py-4 rounded-2xl border-2 border-dark-brown`}>
<span className="relative z-1 text-base lg:text-lg text-dark-brown text-center font-bold">{children ?? text}</span>
{dir === '' ? (
<svg className="absolute -bottom-4 left-1/2 -translate-x-1/2" width="24" height="16" viewBox="0 0 24 16">
<polygon points="0,0 24,0 12,16" className="fill-white stroke-dark-brown" strokeWidth="2" />
<polygon points="1,0 23,0 12,14" className="fill-white" />
</svg>
) : (
<svg className="absolute -left-4 top-1/2 -translate-y-1/2" width="16" height="24" viewBox="0 0 16 24">
<polygon points="16,0 16,24 0,12" className="fill-white stroke-dark-brown" strokeWidth="2" />
<polygon points="16,1 16,23 2,12" className="fill-white" />
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 (
<div className={`relative bg-${bg} h-auto w-auto p-3 sm:px-6 sm:py-4 rounded-2xl border-2 border-dark-brown`}>
<span className="relative z-1 text-base lg:text-lg text-dark-brown text-center font-bold">
{children ?? text}
</span>

<svg className={tail.className} width={tail.width} height={tail.height} viewBox={tail.viewBox}>
<polygon points={tail.outerPoints} className={`fill-${bg} stroke-dark-brown`} strokeWidth="2" />
<polygon points={tail.innerPoints} className={`fill-${bg}`} />
</svg>
)}
</div>
)
</div>
)
}

export default SpeechBubble
2 changes: 1 addition & 1 deletion app/frontend/components/path/BgmPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export default function BgmPlayer({ hasProjects = false }: { hasProjects?: boole
const track = TRACKS[trackIndex]

return (
<Frame>
<Frame showBorderOnMobile>
<div className="flex flex-col w-60 gap-1 p-4">
<div>
<p className="text-lg font-medium text-dark-brown truncate text-center">{track.title}</p>
Expand Down
6 changes: 3 additions & 3 deletions app/frontend/components/path/PathNode.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -82,9 +82,9 @@ export default function PathNode({
<div style={{ pointerEvents: 'auto' }} className="cursor-pointer">
{index === 0 ? (
state === 'active' && interactive ? (
<ModalLink href="/projects/onboarding" className="outline-0">
<Link href="/projects/onboarding" className="outline-0">
{starImage}
</ModalLink>
</Link>
) : state === 'completed' && interactive ? (
<button
onClick={() =>
Expand Down
51 changes: 49 additions & 2 deletions app/frontend/components/shared/BookLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ const BookLayout = ({
children,
className,
showJoint = true,
showBorderOnMobile = false,
}: {
children: ReactNode
className?: string
showJoint?: boolean
showBorderOnMobile?: boolean
}) => (
<div className="h-full xl:p-12 flex">
<div
className={twMerge(
'h-full flex',
showBorderOnMobile ? 'relative pl-4.25 pt-3.75 pr-6.25 pb-5.5 xl:p-12' : 'relative xl:p-12',
)}
>
<div className={twMerge('relative flex-1 h-full my-auto', className)}>
<div className="inset-0 bg-light-brown h-full w-full">{children}</div>
<div className="inset-0 bg-light-brown h-full w-full max-xl:p-3 md:max-xl:p-4">{children}</div>
<div className="hidden xl:block absolute pointer-events-none -left-5 -right-5 top-5 -bottom-5">
<div className="absolute left-0 bottom-0 top-0 w-5 bg-[#d4bb9d]"></div>
<div className="absolute left-0 bottom-0 right-0 h-5 bg-[#d4bb9d]"></div>
Expand All @@ -32,6 +39,46 @@ const BookLayout = ({
<div className="hidden xl:block absolute pointer-events-none left-1/2 top-0 -translate-x-1/2 -bottom-15 w-px bg-dark-brown"></div>
)}
</div>
{showBorderOnMobile && (
<>
<img
className="absolute top-0 left-0 w-22.5 h-20 pointer-events-none xl:hidden z-10"
src="/border/top_left.webp"
alt=""
/>
<img
className="absolute top-0 right-0 w-22.5 h-20 pointer-events-none xl:hidden z-10"
src="/border/top_right.webp"
alt=""
/>
<img
className="absolute bottom-0 left-0 w-22.5 h-20 pointer-events-none xl:hidden z-10"
src="/border/bottom_left.webp"
alt=""
/>
<img
className="absolute bottom-0 right-0 w-22.5 h-20 pointer-events-none xl:hidden z-10"
src="/border/bottom_right.webp"
alt=""
/>
<div
className="absolute top-20 left-0 bottom-20 w-22.5 pointer-events-none xl:hidden z-10"
style={{ backgroundImage: 'url(/border/left.webp)', backgroundSize: '100% 100%' }}
/>
<div
className="absolute top-20 right-0 bottom-20 w-22.5 pointer-events-none xl:hidden z-10"
style={{ backgroundImage: 'url(/border/right.webp)', backgroundSize: '100% 100%' }}
/>
<div
className="absolute top-0 left-22.5 right-22.5 h-20 pointer-events-none xl:hidden z-10"
style={{ backgroundImage: 'url(/border/top.webp)', backgroundSize: '100% 100%' }}
/>
<div
className="absolute bottom-0 left-22.5 right-22.5 h-20 pointer-events-none xl:hidden z-10"
style={{ backgroundImage: 'url(/border/bottom.webp)', backgroundSize: '100% 100%' }}
/>
</>
)}
</div>
)

Expand Down
95 changes: 54 additions & 41 deletions app/frontend/components/shared/Frame.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,59 @@
import type { ReactNode } from 'react'
import { twMerge } from 'tailwind-merge'

const Frame = ({ children, className }: { children: ReactNode; className?: string }) => (
<div className={twMerge('relative md:pl-4.25 md:pt-3.75 md:pr-6.25 md:pb-5.5', className)}>
<div className="bg-light-brown h-full w-full p-4 md:p-3 overflow-y-auto">{children}</div>
<img
className="hidden md:block absolute top-0 left-0 w-22.5 h-20 pointer-events-none"
src="/border/top_left.webp"
alt=""
/>
<img
className="hidden md:block absolute top-0 right-0 w-22.5 h-20 pointer-events-none"
src="/border/top_right.webp"
alt=""
/>
<img
className="hidden md:block absolute bottom-0 left-0 w-22.5 h-20 pointer-events-none"
src="/border/bottom_left.webp"
alt=""
/>
<img
className="hidden md:block absolute bottom-0 right-0 w-22.5 h-20 pointer-events-none"
src="/border/bottom_right.webp"
alt=""
/>
<div
className="hidden md:block absolute top-20 left-0 bottom-20 w-22.5 pointer-events-none"
style={{ backgroundImage: 'url(/border/left.webp)', backgroundSize: '100% 100%' }}
/>
<div
className="hidden md:block absolute top-20 right-0 bottom-20 w-22.5 pointer-events-none"
style={{ backgroundImage: 'url(/border/right.webp)', backgroundSize: '100% 100%' }}
/>
<div
className="hidden md:block absolute top-0 left-22.5 right-22.5 h-20 pointer-events-none"
style={{ backgroundImage: 'url(/border/top.webp)', backgroundSize: '100% 100%' }}
/>
<div
className="hidden md:block absolute bottom-0 left-22.5 right-22.5 h-20 pointer-events-none"
style={{ backgroundImage: 'url(/border/bottom.webp)', backgroundSize: '100% 100%' }}
/>
</div>
)
const Frame = ({
children,
className,
showBorderOnMobile = false,
}: {
children: ReactNode
className?: string
showBorderOnMobile?: boolean
}) => {
const bp = showBorderOnMobile ? 'block' : 'hidden md:block'
const pad = showBorderOnMobile ? 'pl-4.25 pt-3.75 pr-6.25 pb-5.5' : 'md:pl-4.25 md:pt-3.75 md:pr-6.25 md:pb-5.5'

return (
<div className={twMerge(`relative ${pad}`, className)}>
<div className="bg-light-brown h-full w-full p-4 md:p-3 flex flex-col">{children}</div>
<img
className={`${bp} absolute top-0 left-0 w-22.5 h-20 pointer-events-none`}
src="/border/top_left.webp"
alt=""
/>
<img
className={`${bp} absolute top-0 right-0 w-22.5 h-20 pointer-events-none`}
src="/border/top_right.webp"
alt=""
/>
<img
className={`${bp} absolute bottom-0 left-0 w-22.5 h-20 pointer-events-none`}
src="/border/bottom_left.webp"
alt=""
/>
<img
className={`${bp} absolute bottom-0 right-0 w-22.5 h-20 pointer-events-none`}
src="/border/bottom_right.webp"
alt=""
/>
<div
className={`${bp} absolute top-20 left-0 bottom-20 w-22.5 pointer-events-none`}
style={{ backgroundImage: 'url(/border/left.webp)', backgroundSize: '100% 100%' }}
/>
<div
className={`${bp} absolute top-20 right-0 bottom-20 w-22.5 pointer-events-none`}
style={{ backgroundImage: 'url(/border/right.webp)', backgroundSize: '100% 100%' }}
/>
<div
className={`${bp} absolute top-0 left-22.5 right-22.5 h-20 pointer-events-none`}
style={{ backgroundImage: 'url(/border/top.webp)', backgroundSize: '100% 100%' }}
/>
<div
className={`${bp} absolute bottom-0 left-22.5 right-22.5 h-20 pointer-events-none`}
style={{ backgroundImage: 'url(/border/bottom.webp)', backgroundSize: '100% 100%' }}
/>
</div>
)
}

export default Frame
Loading
Loading