Skip to content

Commit

Permalink
feat: add new project form flow
Browse files Browse the repository at this point in the history
  • Loading branch information
brunosllz committed Nov 29, 2023
1 parent 53819e0 commit 94d52b3
Show file tree
Hide file tree
Showing 20 changed files with 2,623 additions and 0 deletions.
37 changes: 37 additions & 0 deletions src/app/(app)/me/project/new/components/back-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client'

import { useBoundStore } from '@/store'

import { Button } from '@/components/ui/button'
import { useRouter } from 'next/navigation'
import { HTMLAttributes } from 'react'

type BackButtonProps = HTMLAttributes<HTMLButtonElement>

export function BackButton({ ...props }: BackButtonProps) {
const { newProjectFormSteps } = useBoundStore(({ newProjectFormSteps }) => ({
newProjectFormSteps,
}))

const router = useRouter()

function handleBack() {
router.back()
}

return (
<Button
onClick={handleBack}
disabled={
newProjectFormSteps.description.submitIsLoading ||
newProjectFormSteps.job.submitIsLoading
}
className="max-w-min"
variant="outline"
size="lg"
{...props}
>
Voltar
</Button>
)
}
116 changes: 116 additions & 0 deletions src/app/(app)/me/project/new/components/input-tracker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'

import { HTMLAttributes, MouseEvent, useEffect, useRef } from 'react'
import { useTrackSelectedStep } from '../contexts/track-selected-step-context'
import { twMerge } from 'tailwind-merge'

type InputTrackerProps = HTMLAttributes<HTMLDivElement>

export function InputTracker({ ...props }: InputTrackerProps) {
const { handleSetCurrentTarget } = useTrackSelectedStep()
const containerRef = useRef<HTMLDivElement | null>(null)

function handleMouseEnter(event: MouseEvent<HTMLDivElement>) {
const currentTarget = event.currentTarget

const firstInputChild = currentTarget.getElementsByTagName('input')[0]
const hasInputChildren = !!firstInputChild

if (!hasInputChildren) {
const hasEditorContainer =
currentTarget.getElementsByClassName('prose')[0]

if (hasEditorContainer) {
return handleSetCurrentTarget(hasEditorContainer.id)
}

const divContainer = currentTarget.getElementsByTagName('div')

return Array.from({ length: divContainer.length }).forEach((_, index) => {
const hasIdIdentifierOnDiv = divContainer.item(index)?.id

if (hasIdIdentifierOnDiv) {
return handleSetCurrentTarget(hasIdIdentifierOnDiv)
}
})
}

const isInputFile = firstInputChild.type === 'file'

if (isInputFile) {
return handleSetCurrentTarget(`${firstInputChild.id}-container`)
}

handleSetCurrentTarget(firstInputChild.id)
}

useEffect(() => {
const container = containerRef.current

if (!container) {
return
}

const selectedInput = container.getElementsByTagName('button')[0]

const hasSelectChildren = !!selectedInput

if (hasSelectChildren) {
selectedInput.addEventListener('focus', () => {
handleSetCurrentTarget(selectedInput.id)
})

return () =>
selectedInput.removeEventListener('focus', () => {
handleSetCurrentTarget(selectedInput.id)
})
}

const firstInputChild = container.getElementsByTagName('input')[0]
const hasInputChildren = !!firstInputChild

if (!hasInputChildren) {
return
}

const isInputFile = firstInputChild.type === 'file'

if (isInputFile) {
const dropzoneContainer = container
.getElementsByTagName('div')
.namedItem(`${firstInputChild.id}-container`)

if (!dropzoneContainer) {
return
}

dropzoneContainer.addEventListener('focus', () => {
handleSetCurrentTarget(`${firstInputChild.id}-container`)
})

return () =>
dropzoneContainer.removeEventListener('focus', () => {
handleSetCurrentTarget(`${firstInputChild.id}-container`)
})
}

firstInputChild.addEventListener('focus', () => {
handleSetCurrentTarget(firstInputChild.id)
})

return () =>
firstInputChild.removeEventListener('focus', () => {
handleSetCurrentTarget(firstInputChild.id)
})
}, [])

return (
<div
ref={containerRef}
className={twMerge('space-y-3.5', props.className)}
onMouseEnter={(event) => handleMouseEnter(event)}
{...props}
/>
)
}
186 changes: 186 additions & 0 deletions src/app/(app)/me/project/new/components/sidebar-navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
'use client'

import { MouseEvent } from 'react'
import { motion } from 'framer-motion'
import { useTrackSelectedStep } from '../contexts/track-selected-step-context'
import { usePathname } from 'next/navigation'
import { parseCookies } from 'nookies'

import Link from 'next/link'
import { Card, CardContent } from '@/components/ui/card'
import { NEW_PROJECT_COOKIES_ID } from '../layout'

const FORM_STEPS = [
{
formHref: 'cover',
formStep: {
title: 'Capa do projeto',
},
steps: [
{
href: 'banner-container',
name: 'Banner',
},
{
href: 'avatar-container',
name: 'Avatar',
},
{
href: 'name',
name: 'Nome do projeto',
},
{
href: 'availability',
name: 'Disponibilidade',
},
],
},
{
formHref: 'description',
formStep: {
title: 'Informações do projeto',
},
steps: [
{
href: 'description',
name: 'Descrição do projeto',
},
{
href: 'skills',
name: 'Habilidades',
},
],
},
{
formHref: 'job',
formStep: {
title: 'Informações das vagas',
},
steps: [
{
href: 'roles',
name: 'Vagas disponíveis',
},
{
href: 'role-description',
name: 'Descrição da função',
},
],
},
]

export function SidebarNavigation() {
const { currentTarget, handleSetCurrentTarget } = useTrackSelectedStep()
const pathname = usePathname()
const cookiesStore = parseCookies()

const pathnameSlip = pathname.split('/')
const currentForm = pathnameSlip[pathnameSlip.length - 1]

const formIdCookiesStore = cookiesStore[NEW_PROJECT_COOKIES_ID]

let formIdCookies: {
[key: string]: string
cover: string
description: string
} | null = null

if (formIdCookiesStore) {
formIdCookies = JSON.parse(formIdCookiesStore)
}

function handleFocusElement(
event: MouseEvent<HTMLAnchorElement>,
elementHref: string,
) {
event.preventDefault()

const inputOrDivElement = window.document.getElementById(elementHref)

if (!inputOrDivElement) {
return
}

if (inputOrDivElement?.tagName.toLowerCase() === 'div') {
const tiptapEditorContainer =
inputOrDivElement?.getElementsByClassName('tiptap')

if (tiptapEditorContainer.length) {
const divContainer = tiptapEditorContainer?.item(0) as HTMLDivElement

divContainer.focus()
}

const divDropzoneContainer = inputOrDivElement

divDropzoneContainer.focus()
return handleSetCurrentTarget(elementHref)
}

handleSetCurrentTarget(elementHref)
const inputElement = inputOrDivElement as HTMLInputElement

inputElement.focus()
}

return (
<aside className="sticky top-[7.75rem] h-min pb-[5.6875rem]">
<Card>
<CardContent className="space-y-6 p-5">
{FORM_STEPS.map((item, index) => {
return (
<div key={item.formHref}>
<Link
data-disabled={
currentForm !== item.formHref &&
!!formIdCookies &&
!formIdCookies[item.formHref]
}
data-is-current-form={currentForm === item.formHref}
data-is-filled={
!!formIdCookies && !!formIdCookies[item.formHref]
}
href={`/me/project/new/${item.formHref}`}
className="flex items-center gap-3 text-muted-foreground opacity-60 transition-opacity data-[disabled=true]:pointer-events-none data-[disabled=true]:select-none data-[is-current-form=true]:text-primary data-[is-current-form=true]:opacity-100 data-[is-filled=true]:opacity-100"
>
<span
data-is-current-form={currentForm === item.formHref}
className="flex h-8 w-8 items-center justify-center rounded-full border text-xs transition-colors data-[is-current-form=true]:border-none data-[is-current-form=true]:bg-zinc-900"
>
{String(index + 1).padStart(2, '0')}
</span>
<span className="font-medium">{item.formStep.title}</span>
</Link>

{currentForm === item.formHref && (
<div className="ml-4 mt-4 flex flex-col gap-3 border-l-2 px-3 transition-transform">
{item.steps.map((step) => (
<div key={step.href} className="relative">
<a
href={`#${step.href}`}
onClick={(event) =>
handleFocusElement(event, step.href)
}
className="text-sm text-muted-foreground transition-colors data-[is-selected=true]:text-primary"
>
{step.name}
</a>

{currentTarget === step.href && (
<motion.div
layoutId="activeTab"
className="absolute -left-3.5 top-0 h-6 w-0.5 bg-primary"
/>
)}
</div>
))}
</div>
)}
</div>
)
})}
</CardContent>
</Card>
</aside>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client'

import { createContext, useContext, useState } from 'react'

type TrackSelectedStepContextType = {
currentTarget: string
handleSetCurrentTarget: (target: string) => void
}

const TrackSelectedStepContext = createContext<TrackSelectedStepContextType>(
{} as TrackSelectedStepContextType,
)

export function TrackSelectedStepContextProvider({
children,
}: {
children: React.ReactNode
}) {
const [currentTarget, setCurrentTarget] = useState('')

function handleSetCurrentTarget(target: string) {
setCurrentTarget((state) => {
if (state === target) {
return state
}

return target
})
}

return (
<TrackSelectedStepContext.Provider
value={{ currentTarget, handleSetCurrentTarget }}
>
{children}
</TrackSelectedStepContext.Provider>
)
}

export const useTrackSelectedStep = () => useContext(TrackSelectedStepContext)
Loading

0 comments on commit 94d52b3

Please sign in to comment.