-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
2,623 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
116
src/app/(app)/me/project/new/components/input-tracker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
186
src/app/(app)/me/project/new/components/sidebar-navigation.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
40 changes: 40 additions & 0 deletions
40
src/app/(app)/me/project/new/contexts/track-selected-step-context.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.