Commit 4c58c646 authored by devteam's avatar devteam

Merge branch 'dev' into 'staging'

fix: add local upload

See merge request !1
parents 92238f07 d5b25004
Pipeline #19012 failed with stage
in 1 minute and 31 seconds
# syntax=docker.io/docker/dockerfile:1 # Etapa 1: build da aplicação
FROM node:22-alpine AS builder
FROM node:22-alpine AS base # Diretório de trabalho dentro do container
# INSTALAÇÃO DE DEPENDENCIAS
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ # Copia os arquivos de dependência
RUN \ COPY package.json package-lock.json ./
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Instala as dependências
RUN npm ci
# REBUILD CODIGO APENAS QUANDO PRECISA # Copia o restante dos arquivos
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
ENV NEXT_TELEMETRY_DISABLED=1 # Build da aplicação Next.js
RUN npm run build
RUN \ # Etapa 2: imagem final para produção
if [ -f yarn.lock ]; then yarn run build; \ FROM node:22-alpine
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Diretório de trabalho
# IMAGEM PARA PRODUCAO, COPIA TODOS OS ARQUIVOS DO REBUILD E EXECUTA
FROM base AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production # Copia os arquivos necessários do builder
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ # Garante que a pasta imgs exista
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static RUN mkdir -p ./public/imgs
#USER nextjs
# Expõe a porta padrão
EXPOSE 3000 EXPOSE 3000
ENV PORT=3000 # Comando para iniciar a aplicação
CMD ["npm", "start"]
ENV HOSTNAME="0.0.0.0" \ No newline at end of file
CMD ["node", "server.js"]
\ No newline at end of file
version: '3.9'
services:
nextjs-app:
build: .
ports:
- "3000:3000"
volumes:
- ./imgs:/app/public/imgs
environment:
- NODE_ENV=production
\ No newline at end of file
// next.config.js /** @type {import('next').NextConfig} */
module.exports = { const nextConfig = {
// ... rest of the configuration. images: {
output: "standalone", remotePatterns: [
}; {
protocol: 'https',
hostname: `${process.env.NEXT_PUBLIC_CLOUDFLARE_URL_PUBLIC_BUCKET}`,
port: '',
pathname: '**',
},
],
},
}
module.exports = nextConfig
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -9,39 +9,50 @@ ...@@ -9,39 +9,50 @@
"lint": "eslint . --fix" "lint": "eslint . --fix"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.758.0",
"@emotion/react": "^11.13.0", "@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.10.0",
"@mui/material": "^5.16.4", "@mui/material": "^5.16.4",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"axios": "^1.7.2", "@tanstack/react-query": "^5.59.3",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"date-fns-tz": "^3.2.0",
"embla-carousel-autoplay": "^8.1.8", "embla-carousel-autoplay": "^8.1.8",
"embla-carousel-react": "^8.1.8", "embla-carousel-react": "^8.1.8",
"lucide-react": "^0.383.0", "lucide-react": "^0.383.0",
"next": "14.2.3", "next": "14.2.3",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"nodemailer": "^6.10.0",
"nookies": "^2.5.2",
"path": "^0.12.7",
"react": "^18", "react": "^18",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.52.2", "react-hook-form": "^7.52.2",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"sonner": "^1.5.0",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@rocketseat/eslint-config": "^2.2.2", "@rocketseat/eslint-config": "^2.2.2",
"@types/cookie": "^0.6.0",
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^6.4.17",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/react-input-mask": "^3.0.5", "@types/react-input-mask": "^3.0.5",
......
import { api } from '@/lib/axios'
export type AccountsProps = {
id: string
name: string
email: string
}
export async function getAccounts() {
const accounts = await api.get<AccountsProps[]>('/accounts')
return accounts
}
export async function getAccountsId(id: string) {
const account = await api.get<AccountsProps>(`/accounts/${id}`)
return account
}
import { api } from '@/lib/axios'
export type AreasProps = {
id?: string
name: string
desktopBanner: string | null
mobileBanner: string | null
}
export async function createAreas({
name,
desktopBanner,
mobileBanner,
}: AreasProps) {
const area = await api.post<AreasProps[]>('/areas', {
name,
desktopBanner,
mobileBanner,
})
return area
}
export async function editArea({
id,
name,
desktopBanner,
mobileBanner,
}: AreasProps) {
const area = await api.put<AreasProps[]>(`/areas/${id}`, {
name,
desktopBanner,
mobileBanner,
})
return area
}
export async function getAreas() {
const areas = await api.get<AreasProps[]>('/areas')
return areas
}
export async function getAreasId(id: string) {
const area = await api.get<AreasProps>(`/areas/${id}`)
return area
}
export async function deleteArea(id: string) {
const area = await api.delete(`/areas/${id}`)
return area
}
import { api } from '@/lib/axios'
export type AudiencesProps = {
id: string
name: string
}
export async function createAudiences({ name }: AudiencesProps) {
const audiences = await api.post<AudiencesProps>('/audiences', {
name,
})
return audiences
}
export async function getAudiences() {
const audiences = await api.get<AudiencesProps[]>('/audiences')
return audiences
}
export async function getAudienceId(id: string) {
const audiences = await api.get<AudiencesProps>(`/audiences/${id}`)
return audiences
}
export async function editAudienceId(data: AudiencesProps) {
const audiences = await api.put<AudiencesProps>(`/audiences/${data.id}`, {
name: data.name,
})
return audiences
}
export async function deleteAudience(id: string) {
const audiences = await api.delete(`/audiences/${id}`)
return audiences
}
import { api } from '@/lib/axios'
export type BannersProps = {
id: string
name: string
desktopBanner?: string
mobileBanner?: string
}
export async function getBanners() {
const banners = await api.get<BannersProps[]>('/banners')
return banners
}
import { api } from '@/lib/axios'
export type CategoriesProps = {
id: string
name: string
}
export async function createCategories({ name }: CategoriesProps) {
const categories = await api.post<CategoriesProps>('/categories', {
name,
})
return categories
}
export async function getCategories() {
const categories = await api.get<CategoriesProps[]>('/categories')
return categories
}
export async function getCategoryId(id: string) {
const categories = await api.get<CategoriesProps>(`/categories/${id}`)
return categories
}
export async function editCategoryId(data: CategoriesProps) {
const categories = await api.put<CategoriesProps>(`/categories/${data.id}`, {
name: data.name,
})
return categories
}
export async function deleteCategory(id: string) {
const categories = await api.delete(`/categories/${id}`)
return categories
}
import { api } from '@/lib/axios'
import { AreasProps } from './areas'
import { AudiencesProps } from './audiences'
import { CategoriesProps } from './categories'
import { ModulesProps } from './modules'
export type CourseIdProps = {
id?: string
name: string
area: AreasProps
audience: AudiencesProps
category: CategoriesProps
description: string
desktopBanner: string
mobileBanner: string
startDate?: string
endDate?: string
professors: string[]
workload: number
modules: {
courseId: string
module: ModulesProps
moduleId: string
}[]
}
export type CoursesProps = {
id?: string
name: string
description: string
desktopBanner: string | null
mobileBanner: string | null
startDate?: string
endDate?: string
professors: string[]
workload: number
areaId: string
audienceId: string
categoryId: string
area?: {
id: string
name: string
}
audience?: {
id: string
name: string
}
category?: {
id: string
name: string
}
moduleIds: string[]
}
export async function createCourses(data: CoursesProps) {
const course = await api.post<CoursesProps[]>('/courses', data)
return course
}
export async function getCourses() {
const courses = await api.get<CoursesProps[]>('/courses')
return courses
}
export async function getCourseId(id: string) {
const course = await api.get<CourseIdProps>(`/courses/${id}`)
return course
}
export async function editCourseId(data: CoursesProps) {
const course = await api.put<CoursesProps>(`/courses/${data.id}`, data)
return course
}
export async function deleteCourse(id: string) {
const course = await api.delete(`/courses/${id}`)
return course
}
import { api } from '@/lib/axios'
export type SignInProps = {
email: string
password: string
}
export type SignInResponse = {
access_token: string
user: {
id: string
name: string
email: string
}
}
export async function login(data: SignInProps) {
const token = await api.post<SignInResponse>('/login', data)
return token
}
import { api } from '@/lib/axios'
export type ModulesProps = {
id: string
name: string
description: string
}
export async function createModules({ name, description }: ModulesProps) {
const modules = await api.post<ModulesProps>('/modules', {
name,
description,
})
return modules
}
export async function editModule({ id, name, description }: ModulesProps) {
const modules = await api.patch<ModulesProps[]>(`/modules/${id}`, {
name,
description,
})
return modules
}
export async function getModules() {
const modules = await api.get<ModulesProps[]>('/modules')
return modules
}
export async function getModulesId(id: string) {
const modules = await api.get<ModulesProps>(`/modules/${id}`)
return modules
}
export async function deleteModule(id: string) {
const modules = await api.delete(`/modules/${id}`)
return modules
}
import { Search } from 'lucide-react' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { About } from '@/components/about' import { About } from '@/components/about'
...@@ -6,14 +7,16 @@ import { Banner } from '@/components/banner' ...@@ -6,14 +7,16 @@ import { Banner } from '@/components/banner'
import { CarouselComponent } from '@/components/corousel-component' import { CarouselComponent } from '@/components/corousel-component'
import { CourseCategory } from '@/components/course-category' import { CourseCategory } from '@/components/course-category'
import { Differences } from '@/components/differences' import { Differences } from '@/components/differences'
import { InputMui } from '@/components/mui/inputs'
import { NavLinkCategory } from '@/components/nav-link-category' import { NavLinkCategory } from '@/components/nav-link-category'
import { SignUp } from '@/components/sign-up' import { SignUp } from '@/components/sign-up'
import { SkeletonSerachParams } from '@/components/skeleton-serach-params' import { SkeletonSerachParams } from '@/components/skeleton-serach-params'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Suspense } from 'react' import { Suspense } from 'react'
// import { useForm } from 'react-hook-form'
export default function Home() { export default function Home() {
// const { register } = useForm()
return ( return (
<> <>
<div className="absolute h-[200px] w-full bg-gradient-to-b from-gray-900 from-5% z-10" /> <div className="absolute h-[200px] w-full bg-gradient-to-b from-gray-900 from-5% z-10" />
...@@ -24,22 +27,25 @@ export default function Home() { ...@@ -24,22 +27,25 @@ export default function Home() {
<Suspense fallback={<SkeletonSerachParams />}> <Suspense fallback={<SkeletonSerachParams />}>
<NavLinkCategory /> <NavLinkCategory />
</Suspense> </Suspense>
<div className="flex justify-center items-center w-full md:w-1/2 mx-auto mb-8"> {/* <div className="flex justify-center items-center w-full md:w-1/2 mx-auto mb-8">
<InputMui <InputMui
label="O que você quer aprender hoje?" label="O que você quer aprender hoje?"
variant="standard" variant="standard"
type="text" type="text"
className="w-full #fafafa" className="w-full"
themeColor="#fafafa"
name="learned-today"
register={register}
/> />
<Search size={24} /> <Search size={24} />
</div> </div> */}
<CarouselComponent /> <CarouselComponent />
<Button <Button
variant="secondary" variant="secondary"
className="uppercase mx-auto my-8" className="uppercase mx-auto my-8"
asChild asChild
> >
<Link href="#">Ver todos os cursos</Link> <Link href="/estudantes">Ver todos os cursos</Link>
</Button> </Button>
</div> </div>
<CourseCategory /> <CourseCategory />
......
'use client'
import { AreasProps, deleteArea, editArea, getAreasId } from '@/api/areas'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import InputFile from '@/components/input-file'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import ModalDialog from '@/components/modal-dialog'
import { StyledInputs } from '@/components/mui/styled-inputs' import { StyledInputs } from '@/components/mui/styled-inputs'
import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { TextField } from '@mui/material' import { TextField } from '@mui/material'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Trash } from 'lucide-react' import { Trash } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
export default function AreaId() { export default function AreaId() {
const [desktopBanner, setDesktopBanner] = useState<File | null>(null)
const [mobileBanner, setMobileBanner] = useState<File | null>(null)
const params = useParams<{ id: string }>()
const {
register,
handleSubmit,
formState: { errors },
} = useForm<AreasProps>()
const router = useRouter()
const queryClient = useQueryClient()
const { data: area, isLoading } = useQuery({
queryKey: ['areas', params.id],
queryFn: () => getAreasId(params.id),
enabled: !!params.id,
})
const mutationEdit = useMutation({
mutationFn: editArea,
onSuccess: () => {
toast.success('Área editada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['areas'] })
router.push('/admin')
},
})
const mutationDelete = useMutation({
mutationFn: deleteArea,
onSuccess: () => {
toast.success('Área deletada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['areas'] })
router.push('/admin')
},
})
async function onSubmit(data: AreasProps) {
const urlDesktop = await handleUpload(desktopBanner)
const urlMobile = await handleUpload(mobileBanner)
const dataArea = {
...data,
id: params.id,
desktopBanner: urlDesktop,
mobileBanner: urlMobile,
}
mutationEdit.mutateAsync(dataArea)
}
const handleUpload = async (file: File | null) => {
if (!file) return
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await res.json()
return data.url
}
function handleDeleteArea() {
mutationDelete.mutate(params.id)
}
if (isLoading) {
return (
<div className="flex w-full h-[500px] items-center justify-center">
<LoadingSpinIcon />
<span>Loading...</span>
</div>
)
}
return ( return (
<section className="container py-10"> <section className="container py-10">
<BreadcrumbComponent page="Área" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<h1 className="text-green-700 text-3xl font-bold">Editar Área</h1> <h1 className="text-3xl font-bold">Editar Área</h1>
<Button variant="third" className="uppercase"> <Button
Salvar como rascunho variant="secondary"
</Button> type="submit"
<Button variant="secondary" className="uppercase"> className="uppercase rounded-sm"
disabled={isLoading}
>
{mutationEdit.isPending && <LoadingSpinIcon />}
Salvar e publicar Salvar e publicar
</Button> </Button>
<Button <AlertDialog>
variant="ghost" <>
className="uppercase flex items-center gap-2 text-orange-100 hover:text-orange-100" <AlertDialogTrigger
className="uppercase flex items-center gap-2 text-orange-100 border-none hover:bg-orange-100/10 hover:text-orange-100 h-10 px-5 rounded-sm"
type="button"
> >
<span>Apagar área</span> <span>Apagar área</span>
<Trash /> <Trash />
</Button> </AlertDialogTrigger>
<ModalDialog
title="Tem certeza que deseja deletar a área?"
description={`Ao clicar em continue você irá deletar a área ${area?.data.name}`}
handleClick={handleDeleteArea}
/>
</>
</AlertDialog>
</div> </div>
<div className="w-full"> <div className="w-full mt-16">
<TextField <TextField
label="Nome da área" label="Nome da área"
variant="standard"
type="text" type="text"
defaultValue={area?.data.name}
{...register('name')}
variant="standard"
className="flex" className="flex"
sx={StyledInputs({ color: '#26AAA7' })} sx={StyledInputs(
errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
)}
/> />
<div className="flex justify-between flex-wrap gap-6"> <div className="flex justify-between flex-wrap gap-6 py-6">
<div className="flex flex-col flex-1 mt-6"> <div className="flex flex-col flex-1 mt-6">
<h6 className="text-xl text-purple-100">Banner Desktop</h6> <h6
<span className="mt-4">Dimensões recomendadas: </span> className={cn(
<span>Formatos aceitos: .png, .jpg</span> 'text-xl text-purple-100',
errors.desktopBanner && 'text-red-600',
<Button )}
variant="third"
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full"
> >
Adicionar banner Banner Desktop
</Button> </h6>
<span className="mt-4">Dimensões recomendadas: 1512 x 300</span>
<span>Formatos aceitos: .png, .jpg, .jpeg</span>
<InputFile
id="desktopBanner"
label={!desktopBanner ? 'Atualizar banner' : desktopBanner.name}
onChange={(e) => setDesktopBanner(e.target.files?.[0] || null)}
/>
</div> </div>
<div className="flex flex-col flex-1 mt-6"> <div className="flex flex-col flex-1 mt-6">
<h6 className="text-xl text-purple-100">Banner Mobile</h6> <h6
<span className="mt-4">Dimensões recomendadas: </span> className={cn(
<span>Formatos aceitos: .png, .jpg</span> 'text-xl text-purple-100',
<Button errors.mobileBanner && 'text-red-600',
variant="third" )}
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full"
> >
Adicionar banner Banner Mobile
</Button> </h6>
<span className="mt-4">Dimensões recomendadas: 390 x 300</span>
<span>Formatos aceitos: .png, .jpg, .jpeg</span>
<InputFile
id="mobileBanner"
label={!mobileBanner ? 'Atualizar banner' : mobileBanner.name}
onChange={(e) => setMobileBanner(e.target.files?.[0] || null)}
/>
</div> </div>
</div> </div>
</div> </div>
</form>
</section> </section>
) )
} }
'use client'
import { AreasProps, createAreas } from '@/api/areas'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import InputFile from '@/components/input-file'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import { StyledInputs } from '@/components/mui/styled-inputs' import { StyledInputs } from '@/components/mui/styled-inputs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { zodResolver } from '@hookform/resolvers/zod'
import { TextField } from '@mui/material' import { TextField } from '@mui/material'
import { Trash } from 'lucide-react' import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
const FormSchema = z.object({
name: z.string().trim().min(3, { message: 'Nome obrigatório' }),
})
export default function Area() { export default function Area() {
const [desktopBanner, setDesktopBanner] = useState<File | null>(null)
const [mobileBanner, setMobileBanner] = useState<File | null>(null)
const {
register,
handleSubmit,
setError,
formState: { errors },
} = useForm<AreasProps>({
resolver: zodResolver(FormSchema),
})
const router = useRouter()
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createAreas,
onSuccess: () => {
toast.success('Área criada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['areas'] })
router.push('/admin')
},
})
async function onSubmit(data: AreasProps) {
if (!desktopBanner) {
setError('desktopBanner', {
type: 'manual',
message: 'Foto desktop obrigatória!',
})
return
}
if (!mobileBanner) {
setError('mobileBanner', {
type: 'manual',
message: 'Foto mobile obrigatória!',
})
return
}
const urlDesktop = await handleUpload(desktopBanner)
const urlMobile = await handleUpload(mobileBanner)
const dataArea = {
...data,
desktopBanner: urlDesktop,
mobileBanner: urlMobile,
}
mutation.mutateAsync(dataArea)
}
const handleUpload = async (file: File | null) => {
if (!file) return
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await res.json()
return data.url
}
return ( return (
<section className="container py-10"> <section className="container py-10">
<BreadcrumbComponent page="Área" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<h1 className="text-green-700 text-3xl font-bold">Criar Área</h1> <h1 className="text-3xl font-bold">Criar Área</h1>
<Button variant="third" className="uppercase"> <Button variant="secondary" className="uppercase rounded-sm">
Salvar como rascunho {mutation.isPending && <LoadingSpinIcon />}
</Button>
<Button variant="secondary" className="uppercase">
Salvar e publicar Salvar e publicar
</Button> </Button>
<Button
variant="ghost"
className="uppercase flex items-center gap-2 text-orange-100 hover:text-orange-100"
>
<span>Apagar área</span>
<Trash />
</Button>
</div> </div>
<div className="w-full"> <div className="w-full mt-16">
<TextField <TextField
label="Nome da área" label="Nome da área"
variant="standard" variant="standard"
type="text" type="text"
className="flex" className="flex"
sx={StyledInputs({ color: '#26AAA7' })} {...register('name')}
sx={StyledInputs(
errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
)}
/> />
<div className="flex justify-between flex-wrap gap-6"> <div className="flex justify-between flex-wrap gap-6 py-6">
<div className="flex flex-col flex-1 mt-6"> <div className="flex flex-col flex-1 mt-6">
<h6 className="text-xl text-purple-100">Banner Desktop</h6> <h6
<span className="mt-4">Dimensões recomendadas: </span> className={cn(
<span>Formatos aceitos: .png, .jpg</span> 'text-xl text-purple-100',
errors.desktopBanner && 'text-red-600',
<Button )}
variant="third"
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full"
> >
Adicionar banner Banner Desktop
</Button> </h6>
<span className="mt-4">Dimensões recomendadas: 1512 x 300</span>
<span>Formatos aceitos: .png, .jpg, .jpeg</span>
<InputFile
id="desktopBanner"
label={!desktopBanner ? 'Adicionar banner' : desktopBanner.name}
onChange={(e) => setDesktopBanner(e.target.files?.[0] || null)}
/>
</div> </div>
<div className="flex flex-col flex-1 mt-6"> <div className="flex flex-col flex-1 mt-6">
<h6 className="text-xl text-purple-100">Banner Mobile</h6> <h6
<span className="mt-4">Dimensões recomendadas: </span> className={cn(
<span>Formatos aceitos: .png, .jpg</span> 'text-xl text-purple-100',
<Button errors.mobileBanner && 'text-red-600',
variant="third" )}
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full"
> >
Adicionar banner Banner Mobile
</Button> </h6>
<span className="mt-4">Dimensões recomendadas: 390 x 300</span>
<span>Formatos aceitos: .png, .jpg, .jpeg</span>
<InputFile
id="mobileBanner"
label={!mobileBanner ? 'Adicionar banner' : mobileBanner.name}
onChange={(e) => setMobileBanner(e.target.files?.[0] || null)}
/>
</div> </div>
</div> </div>
</div> </div>
</form>
</section> </section>
) )
} }
'use client'
import {
AudiencesProps,
deleteAudience,
editAudienceId,
getAudienceId,
} from '@/api/audiences'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import ModalDialog from '@/components/modal-dialog'
import { StyledInputs } from '@/components/mui/styled-inputs'
import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { TextField } from '@mui/material'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Trash } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
export default function AudienceId() {
const params = useParams<{ id: string }>()
const {
register,
handleSubmit,
formState: { errors },
} = useForm<AudiencesProps>()
const router = useRouter()
const queryClient = useQueryClient()
const { data: audience, isLoading } = useQuery({
queryKey: ['audiences', params.id],
queryFn: () => getAudienceId(params.id),
enabled: !!params.id,
})
const mutationEditAudience = useMutation({
mutationFn: editAudienceId,
onSuccess: () => {
toast.success('Audiência auterada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['audiences'] })
router.push('/admin')
},
})
const mutationDeleteAudience = useMutation({
mutationFn: deleteAudience,
onSuccess: () => {
toast.success('Audiência criada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['audiences'] })
router.push('/admin')
},
})
function handleDeleteAudience() {
mutationDeleteAudience.mutate(params.id)
}
async function onSubmit(data: AudiencesProps) {
const body = {
id: params.id,
name: data.name,
}
mutationEditAudience.mutateAsync(body)
}
if (isLoading) {
return (
<div className="flex w-full h-[500px] items-center justify-center">
<LoadingSpinIcon />
<span>Loading...</span>
</div>
)
}
return (
<section className="container py-10">
<BreadcrumbComponent page="Audiência" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6">
<h1 className="text-3xl font-bold">Editar Audiência</h1>
<Button
variant="secondary"
type="submit"
className="uppercase rounded-sm"
disabled={isLoading}
>
{mutationEditAudience.isPending && <LoadingSpinIcon />}
Salvar e publicar
</Button>
<AlertDialog>
<>
<AlertDialogTrigger
className="uppercase flex items-center gap-2 text-orange-100 border-none hover:bg-orange-100/10 hover:text-orange-100 h-10 px-5 rounded-sm"
type="button"
>
<span>Apagar audiência</span>
<Trash />
</AlertDialogTrigger>
<ModalDialog
title="Tem certeza que deseja deletar a audiência?"
description={`Ao clicar em continue você irá deletar a audiência ${audience?.data.name}`}
handleClick={handleDeleteAudience}
/>
</>
</AlertDialog>
</div>
<div className="w-full mt-16">
<TextField
label="Nome da audiência"
type="text"
defaultValue={audience?.data.name}
{...register('name')}
variant="standard"
className="flex"
sx={StyledInputs(
errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
)}
/>
</div>
</form>
</section>
)
}
'use client'
import { AudiencesProps, createAudiences } from '@/api/audiences'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import { StyledInputs } from '@/components/mui/styled-inputs'
import { Button } from '@/components/ui/button'
import { zodResolver } from '@hookform/resolvers/zod'
import { TextField } from '@mui/material'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
const FormSchema = z.object({
name: z.string().trim().min(3, { message: 'Nome obrigatório' }),
})
export default function Audience() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<AudiencesProps>({
resolver: zodResolver(FormSchema),
})
const router = useRouter()
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createAudiences,
onSuccess: () => {
toast.success('Audiência criada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['audiences'] })
router.push('/admin')
},
})
async function onSubmit(data: AudiencesProps) {
mutation.mutateAsync(data)
}
return (
<section className="container py-10">
<BreadcrumbComponent page="Audiência" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6">
<h1 className="text-3xl font-bold">Criar Audiência</h1>
<Button variant="secondary" className="uppercase rounded-sm">
{mutation.isPending && <LoadingSpinIcon />}
Salvar e publicar
</Button>
</div>
<div className="w-full mt-16">
<TextField
label="Nome da audiência"
variant="standard"
type="text"
className="flex"
{...register('name')}
sx={StyledInputs(
errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
)}
/>
</div>
</form>
</section>
)
}
'use client'
import {
CategoriesProps,
deleteCategory,
editCategoryId,
getCategoryId,
} from '@/api/categories'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import ModalDialog from '@/components/modal-dialog'
import { StyledInputs } from '@/components/mui/styled-inputs'
import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { TextField } from '@mui/material'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Trash } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
export default function AudienceId() {
const params = useParams<{ id: string }>()
const {
register,
handleSubmit,
formState: { errors },
} = useForm<CategoriesProps>()
const router = useRouter()
const queryClient = useQueryClient()
const { data: categorie, isLoading } = useQuery({
queryKey: ['categories', params.id],
queryFn: () => getCategoryId(params.id),
enabled: !!params.id,
})
const mutationEditAudience = useMutation({
mutationFn: editCategoryId,
onSuccess: () => {
toast.success('Categoria auterada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['categories'] })
router.push('/admin')
},
})
const mutationDeleteAudience = useMutation({
mutationFn: deleteCategory,
onSuccess: () => {
toast.success('Categoria criada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['categories'] })
router.push('/admin')
},
})
function handleDeleteAudience() {
mutationDeleteAudience.mutate(params.id)
}
async function onSubmit(data: CategoriesProps) {
const body = {
id: params.id,
name: data.name,
}
mutationEditAudience.mutateAsync(body)
}
if (isLoading) {
return (
<div className="flex w-full h-[500px] items-center justify-center">
<LoadingSpinIcon />
<span>Loading...</span>
</div>
)
}
return (
<section className="container py-10">
<BreadcrumbComponent page="Categoria" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6">
<h1 className="text-3xl font-bold">Editar Categoria</h1>
<Button
variant="secondary"
type="submit"
className="uppercase rounded-sm"
disabled={isLoading}
>
{mutationEditAudience.isPending && <LoadingSpinIcon />}
Salvar e publicar
</Button>
<AlertDialog>
<>
<AlertDialogTrigger
className="uppercase flex items-center gap-2 text-orange-100 border-none hover:bg-orange-100/10 hover:text-orange-100 h-10 px-5 rounded-sm"
type="button"
>
<span>Apagar categoria</span>
<Trash />
</AlertDialogTrigger>
<ModalDialog
title="Tem certeza que deseja deletar a categoria?"
description={`Ao clicar em continue você irá deletar a categoria ${categorie?.data.name}`}
handleClick={handleDeleteAudience}
/>
</>
</AlertDialog>
</div>
<div className="w-full mt-16">
<TextField
label="Nome da categoria"
type="text"
defaultValue={categorie?.data.name}
{...register('name')}
variant="standard"
className="flex"
sx={StyledInputs(
errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
)}
/>
</div>
</form>
</section>
)
}
'use client'
import { CategoriesProps, createCategories } from '@/api/categories'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import { StyledInputs } from '@/components/mui/styled-inputs'
import { Button } from '@/components/ui/button'
import { zodResolver } from '@hookform/resolvers/zod'
import { TextField } from '@mui/material'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
const FormSchema = z.object({
name: z.string().trim().min(3, { message: 'Nome obrigatório' }),
})
export default function Category() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<CategoriesProps>({
resolver: zodResolver(FormSchema),
})
const router = useRouter()
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createCategories,
onSuccess: () => {
toast.success('Categoria criada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['categories'] })
router.push('/admin')
},
})
async function onSubmit(data: CategoriesProps) {
mutation.mutateAsync(data)
}
return (
<section className="container py-10">
<BreadcrumbComponent page="Categoria" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6">
<h1 className="text-3xl font-bold">Criar Categoria</h1>
<Button variant="secondary" className="uppercase rounded-sm">
{mutation.isPending && <LoadingSpinIcon />}
Salvar e publicar
</Button>
</div>
<div className="w-full mt-16">
<TextField
label="Nome da categoria"
variant="standard"
type="text"
className="flex"
{...register('name')}
sx={StyledInputs(
errors.name ? { color: '#dc2626' } : { color: '#26AAA7' },
)}
/>
</div>
</form>
</section>
)
}
'use client' 'use client'
import { InputMui } from '@/components/mui/inputs' import { getAreas } from '@/api/areas'
import { getAudiences } from '@/api/audiences'
import { getCategories } from '@/api/categories'
import { editCourseId, getCourseId } from '@/api/courses'
import { createModules, getModules, ModulesProps } from '@/api/modules'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import InputFile from '@/components/input-file'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import { StyledInputs } from '@/components/mui/styled-inputs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar' import { Calendar } from '@/components/ui/calendar'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@/components/ui/form'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/components/ui/popover' } from '@/components/ui/popover'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { MenuItem } from '@mui/material' import { zodResolver } from '@hookform/resolvers/zod'
import { format } from 'date-fns' import { Box, Chip, MenuItem, TextField } from '@mui/material'
import { CalendarIcon, PlusIcon, Trash } from 'lucide-react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react' import { AxiosError } from 'axios'
import { format, parseISO } from 'date-fns'
import { toZonedTime } from 'date-fns-tz'
import { CalendarIcon, Trash2 } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { Controller, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
const FormSchema = z.object({
name: z.string().trim().min(3, { message: 'Título obrigatório' }),
description: z.string().trim().min(3, { message: 'Descrição obrigatória' }),
desktopBanner: z.any(),
mobileBanner: z.any(),
startDate: z.date().optional(),
endDate: z.date().optional(),
professors: z.string().array(),
workload: z.string().trim().min(1, { message: 'Horas do curso necessário' }),
areaId: z.string().trim().min(1, { message: 'Selecione a área' }),
audienceId: z.string(),
categoryId: z.string(),
moduleIds: z.string().array(),
})
export default function Curso() {
const params = useParams<{ id: string }>()
const [checkedStart, setCheckedStart] = useState<boolean>(false)
const [checkedEnd, setCheckedEnd] = useState<boolean>(false)
const [dateStart, setDateStart] = useState<Date>()
const [dateEnd, setDateEnd] = useState<Date>()
const [inputValue, setInputValue] = useState<string>('')
const [desktopBanner, setDesktopBanner] = useState<File | null>(null)
const [mobileBanner, setMobileBanner] = useState<File | null>(null)
const router = useRouter()
const queryClient = useQueryClient()
const { register, handleSubmit } = useForm<ModulesProps>()
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
professors: [],
moduleIds: [],
desktopBanner: '',
mobileBanner: '',
},
})
const { data: course, isLoading } = useQuery({
queryKey: ['course', params.id],
queryFn: () => getCourseId(params.id),
enabled: !!params.id,
})
const formatDate = (dateString: string) => {
const date = parseISO(dateString)
const utcDate = toZonedTime(date, 'UTC')
return utcDate
}
useEffect(() => {
if (course && course.data.startDate) {
setCheckedStart(true)
onCheckedStart(!!course.data.startDate)
}
if (course && course.data.endDate) {
setCheckedEnd(true)
onCheckedEnd(!!course.data.endDate)
}
}, [course])
useEffect(() => {
if (!course?.data) return
const { modules, professors, category, audience, startDate, endDate } =
course.data
if (modules) {
const existingModuleIds = modules.map((mod) => mod.module.id)
form.setValue('moduleIds', existingModuleIds)
}
if (professors) {
const existingTeachers = professors.map((teacher) => teacher)
form.setValue('professors', existingTeachers)
}
if (category) {
form.setValue('categoryId', category.id)
}
if (audience) {
form.setValue('audienceId', audience.id)
}
if (startDate) {
const date = formatDate(startDate)
form.setValue('startDate', date)
setDateStart(date)
}
if (endDate) {
const date = formatDate(endDate)
form.setValue('endDate', date)
setDateEnd(date)
}
}, [course, form])
const values = form.watch()
const { data: areas } = useQuery({
queryKey: ['areas'],
queryFn: getAreas,
})
const { data: modules } = useQuery({
queryKey: ['modules'],
queryFn: getModules,
})
const { data: audiences, isLoading: audiencesLoading } = useQuery({
queryKey: ['audiences'],
queryFn: getAudiences,
})
const { data: categories, isLoading: categoriesLoading } = useQuery({
queryKey: ['categories'],
queryFn: getCategories,
})
const mutationEdit = useMutation({
mutationFn: editCourseId,
onSuccess: () => {
toast.success('Curso editada com sucesso!')
queryClient.invalidateQueries({ queryKey: ['courses'] })
router.push('/admin')
},
onError: (error) => {
if (error instanceof AxiosError) {
const { message } = error.response?.data
toast.error(`${message}`)
}
},
})
function onCheckedStart(checkedStart: boolean) {
setCheckedStart(checkedStart)
}
function onCheckedEnd(checkedEnd: boolean) {
setCheckedEnd(checkedEnd)
}
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' && inputValue.trim()) {
event.preventDefault()
form.setValue('professors', [...values.professors, inputValue.trim()])
setInputValue('')
}
}
const handleDeleteTeacher = (professorToDelete: string) => {
form.setValue(
'professors',
values.professors.filter((str) => str !== professorToDelete),
)
}
const mutation = useMutation({
mutationFn: createModules,
onSuccess: () => {
toast.success('Módulo criado com sucesso!')
queryClient.invalidateQueries({ queryKey: ['modules'] })
},
})
const selectedModuleIds = form.watch('moduleIds') || []
async function addNewModuler(data: ModulesProps) {
const response = await mutation.mutateAsync(data)
const responseModuleId = response.data.id
form.setValue('moduleIds', [...selectedModuleIds, responseModuleId])
}
const selectedModules = modules?.data.filter((module) =>
selectedModuleIds.includes(module.id),
)
const handleSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedId = event.target.value
if (selectedId && !selectedModuleIds.includes(selectedId)) {
form.setValue('moduleIds', [...selectedModuleIds, selectedId])
}
}
const handleDeleteModule = (idToRemove: string) => {
const updatedIds = selectedModuleIds.filter((id) => id !== idToRemove)
const options = [ form.setValue('moduleIds', updatedIds)
{ label: 'The Godfather', value: 1 }, }
{ label: 'Pulp Fiction', value: 2 },
]
export default function EditCourse() { async function onSubmit(data: z.infer<typeof FormSchema>) {
const [date, setDate] = useState<Date>() if (!dateStart) {
form.setError('startDate', {
type: 'manual',
message: 'Data de início obrigatória!',
})
return
}
if (!dateEnd) {
form.setError('endDate', {
type: 'manual',
message: 'Data de fim obrigatória!',
})
return
}
const urlCapaDesktop = await handleUpload(desktopBanner)
const urlCapaMobile = await handleUpload(mobileBanner)
const body = {
...data,
id: params.id,
startDate: dateStart ? format(dateStart, 'yyyy-MM-dd') : '',
endDate: dateEnd ? format(dateEnd, 'yyyy-MM-dd') : '',
desktopBanner: urlCapaDesktop,
mobileBanner: urlCapaMobile,
workload: Number(data.workload),
}
mutationEdit.mutate(body)
}
const handleUpload = async (file: File | null) => {
if (!file) return
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await res.json()
return data.url
}
if (isLoading) {
return <p>Carregando...</p>
}
return ( return (
<section className="container py-10"> <section className="container py-10">
<form> <BreadcrumbComponent page="Curso" />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<h1 className="text-green-700 text-3xl font-bold">Editar curso</h1> <h1 className="text-3xl font-bold">Criar curso</h1>
<Button variant="third" className="uppercase"> <Button type="submit" variant="secondary" className="uppercase">
Salvar como rascunho
</Button>
<Button variant="secondary" className="uppercase">
Salvar e publicar Salvar e publicar
</Button> </Button>
<Button
variant="ghost"
className="uppercase flex items-center gap-2 text-orange-100 hover:text-orange-100"
>
<span>Apagar curso</span>
<Trash />
</Button>
</div> </div>
<div className="flex w-full mt-10"> <div className="flex w-full mt-10">
<div className="flex-1 flex flex-col border-r border-gray-50 pr-6 space-y-4"> <div className="flex-1 flex flex-col border-r border-gray-50 pr-6 space-y-4">
...@@ -50,81 +301,186 @@ export default function EditCourse() { ...@@ -50,81 +301,186 @@ export default function EditCourse() {
Informações sobre o curso Informações sobre o curso
</h6> </h6>
<InputMui label="Nome do curso" variant="standard" type="text" /> <TextField
label="Nome do curso"
variant="standard"
type="text"
{...form.register('name')}
defaultValue={course?.data.name}
sx={StyledInputs(
form.formState.errors.name
? { color: '#dc2626' }
: { color: '#26AAA7' },
)}
/>
<InputMui type="text" variant="standard" label="Área" select> <TextField
{options.map((option) => ( type="text"
<MenuItem key={option.value} value={option.value}> variant="standard"
{option.label} label="Área"
select
defaultValue={course?.data.area.id || ''}
{...form.register('areaId')}
sx={StyledInputs(
form.formState.errors.areaId
? { color: '#dc2626' }
: { color: '#26AAA7' },
)}
>
<MenuItem value="" className="hidden">
Selecione a área
</MenuItem>
{areas?.data.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem> </MenuItem>
))} ))}
</InputMui> </TextField>
<InputMui <TextField
label="Descrição do Curso" label="Descrição do Curso"
variant="standard" variant="standard"
type="text" type="text"
defaultValue={course?.data.description}
multiline multiline
rows={4} rows={4}
{...form.register('description')}
sx={StyledInputs(
form.formState.errors.description
? { color: '#dc2626' }
: { color: '#26AAA7' },
)}
/> />
<InputMui <Controller
label="Nome dos(as) Professores(as):" control={form.control}
name="professors"
render={({ field }) => (
<Box
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
>
<TextField
label="Digite os nomes dos professores e pressione Enter"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
variant="standard" variant="standard"
type="text" sx={StyledInputs({ color: '#26AAA7' })}
/>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{field.value.map((str, index) => (
<Chip
key={index}
label={str}
onDelete={() => handleDeleteTeacher(str)}
variant="outlined"
color="secondary"
sx={{ color: 'white' }}
/>
))}
</Box>
</Box>
)}
/> />
<div className="flex justify-between flex-wrap gap-6"> <div className="flex justify-between flex-wrap gap-6 py-6">
<div className="flex flex-col flex-1 mt-6"> <div className="flex flex-col flex-1 mt-6">
<h6 className="text-xl text-purple-100">Banner Desktop</h6> <h6
<span className="mt-4">Dimensões recomendadas: </span> className={cn(
<span>Formatos aceitos: .png, .jpg</span> 'text-xl text-purple-100',
<Button form.formState.errors.desktopBanner && 'text-red-600',
variant="third" )}
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto md:w-[436px]"
> >
Adicionar banner Capa do curso desktop
</Button> </h6>
<span className="mt-4">
Dimensões recomendadas: 348 x 464
</span>
<span>Formatos aceitos: .png, .jpg</span>
<InputFile
id="desktopBanner"
label={
!desktopBanner ? 'Atualizar capa' : desktopBanner.name
}
onChange={(e) =>
setDesktopBanner(e.target.files?.[0] || null)
}
/>
</div> </div>
<div className="flex flex-col flex-1 mt-6"> <div className="flex flex-col flex-1 mt-6">
<h6 className="text-xl text-purple-100">Banner Mobile</h6> <h6
<span className="mt-4">Dimensões recomendadas: </span> className={cn(
<span>Formatos aceitos: .png, .jpg</span> 'text-xl text-purple-100',
<Button form.formState.errors.mobileBanner && 'text-red-600',
variant="third" )}
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto md:w-[436px]"
> >
Adicionar banner Capa do curso mobile
</Button> </h6>
<span className="mt-4">
Dimensões recomendadas: 348 x 464
</span>
<span>Formatos aceitos: .png, .jpg</span>
<InputFile
id="mobileBanner"
label={!mobileBanner ? 'Atualizar capa' : mobileBanner.name}
onChange={(e) =>
setMobileBanner(e.target.files?.[0] || null)
}
/>
</div> </div>
</div> </div>
<Separator className="!mt-10 !mb-4" />
<h6 className="text-xl text-purple-100 mb-4">Horas</h6>
<TextField
label="Total de horas do curso"
variant="standard"
defaultValue={course?.data.workload}
type="number"
{...form.register('workload')}
sx={StyledInputs(
form.formState.errors.workload
? { color: '#dc2626' }
: { color: '#26AAA7' },
)}
/>
<Separator className="!mt-10 !mb-4" /> <Separator className="!mt-10 !mb-4" />
<h6 className="text-xl text-purple-100 mb-4">Datas</h6> <h6 className="text-xl text-purple-100 mb-4">Datas</h6>
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox id="start" /> <Checkbox
id="start"
onCheckedChange={onCheckedStart}
checked={checkedStart}
/>
<label <label
htmlFor="start" htmlFor="start"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
form.formState.errors.startDate && 'text-red-600',
)}
> >
Definir data de início Definir data de início
</label> </label>
</div> </div>
{checkedStart && (
<div className="flex flex-col"> <div className="flex flex-col">
<Label>Data de início</Label> <Label>Data de início</Label>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant={'ghost'} variant={'ghost'}
type="button"
className={cn( className={cn(
'lg:w-[450px] justify-start text-left font-normal border-b rounded-none pl-0', 'lg:w-[450px] justify-start text-left font-normal border-b rounded-none pl-0 border-green-400',
!date && 'text-muted-foreground', !dateStart && 'text-muted-foreground',
)} )}
> >
{date ? ( {dateStart ? (
format(date, 'dd/MM/yyyy') format(dateStart, 'dd/MM/yyyy')
) : ( ) : (
<span className="text-gray-600">MM/DD/YYYY</span> <span className="text-gray-600">MM/DD/YYYY</span>
)} )}
...@@ -134,37 +490,52 @@ export default function EditCourse() { ...@@ -134,37 +490,52 @@ export default function EditCourse() {
<PopoverContent className="w-auto p-0"> <PopoverContent className="w-auto p-0">
<Calendar <Calendar
mode="single" mode="single"
selected={date} selected={dateStart}
onSelect={setDate} onSelect={(date) => {
if (date) {
setDateStart(date)
form.setValue('startDate', date)
}
}}
initialFocus initialFocus
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
)}
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox id="closure" /> <Checkbox
id="end"
onCheckedChange={onCheckedEnd}
checked={checkedEnd}
/>
<label <label
htmlFor="closure" htmlFor="closure"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
form.formState.errors.endDate && 'text-red-600',
)}
> >
Definir data de encerramento Definir data de encerramento
</label> </label>
</div> </div>
{checkedEnd && (
<div className="flex flex-col"> <div className="flex flex-col">
<Label>Data de encerramento</Label> <Label>Data de encerramento</Label>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant={'ghost'} variant={'ghost'}
type="button"
className={cn( className={cn(
'lg:w-[450px] justify-start text-left font-normal border-b rounded-none pl-0', 'lg:w-[450px] justify-start text-left font-normal border-b rounded-none pl-0 border-green-400',
!date && 'text-muted-foreground', !dateEnd && 'text-muted-foreground',
)} )}
> >
{date ? ( {dateEnd ? (
format(date, 'dd/MM/yyyy') format(dateEnd, 'dd/MM/yyyy')
) : ( ) : (
<span className="text-gray-600">MM/DD/YYYY</span> <span className="text-gray-600">MM/DD/YYYY</span>
)} )}
...@@ -174,13 +545,19 @@ export default function EditCourse() { ...@@ -174,13 +545,19 @@ export default function EditCourse() {
<PopoverContent className="w-auto p-0"> <PopoverContent className="w-auto p-0">
<Calendar <Calendar
mode="single" mode="single"
selected={date} selected={dateEnd}
onSelect={setDate} onSelect={(date) => {
if (date) {
setDateEnd(date)
form.setValue('endDate', date)
}
}}
initialFocus initialFocus
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
)}
</div> </div>
</div> </div>
...@@ -188,89 +565,146 @@ export default function EditCourse() { ...@@ -188,89 +565,146 @@ export default function EditCourse() {
<h6 className="text-xl text-purple-100 mb-4">Módulos</h6> <h6 className="text-xl text-purple-100 mb-4">Módulos</h6>
<TextField
type="text"
variant="standard"
label="Selecione os módulos do curso"
select
defaultValue={''}
value=""
onChange={handleSelect}
// {...form.register('moduleIds')}
sx={StyledInputs(
form.formState.errors.moduleIds
? { color: '#dc2626' }
: { color: '#26AAA7' },
)}
>
<MenuItem value="" className="hidden">
Selecione os módulos
</MenuItem>
{modules?.data.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
{!modules?.data.length && <p>Não há módulo criado!</p>}
</TextField>
{selectedModules?.map((item) => (
<div
key={item.id}
className="w-full flex justify-between bg-green-800/30 py-2 px-4 rounded-sm"
>
<p>{item.name}</p>
<Trash2
className="cursor-pointer text-red-400 hover:text-red-100"
onClick={() => handleDeleteModule(item.id)}
/>
</div>
))}
<div className="flex flex-col p-4 gap-6 border border-gray-100"> <div className="flex flex-col p-4 gap-6 border border-gray-100">
<InputMui label="Título" variant="standard" type="text" /> <TextField
label="Título"
variant="standard"
type="text"
sx={StyledInputs({ color: '#26AAA7' })}
{...register('name')}
/>
<InputMui <TextField
label="Descrição" label="Descrição"
variant="standard" variant="standard"
type="text" type="text"
multiline multiline
rows={4} rows={4}
sx={StyledInputs({ color: '#26AAA7' })}
{...register('description')}
/> />
</div> </div>
<Button <Button
variant="third" variant="third"
type="button"
className="uppercase w-52 flex items-center gap-2 !mt-8" className="uppercase w-52 flex items-center gap-2 !mt-8"
onClick={handleSubmit(addNewModuler)}
> >
<PlusIcon size={20} /> <span>Adicionar Módulo</span> <span>Criar novo Módulo</span>
</Button> </Button>
</div> </div>
<div className="w-[380px] pl-6 flex flex-col gap-4"> <div className="w-[380px] pl-6 flex flex-col gap-4">
<h6 className="text-xl text-purple-100 mb-4"> <h6 className="text-xl text-purple-100 mb-4">
Qual o público alvo? Qual o público alvo?
</h6> </h6>
{audiencesLoading && <LoadingSpinIcon className="ml-10" />}
<div className="flex items-center gap-2 ml-4"> <div className="flex items-center gap-2 ml-4">
<Checkbox id="terms" /> <FormField
<label control={form.control}
htmlFor="terms" name="audienceId"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" render={({ field }) => (
<FormControl>
<RadioGroup
value={field.value}
onValueChange={field.onChange}
className="flex flex-col space-y-4"
> >
Estudante {audiences?.data.map((audience) => (
</label> <FormItem
</div> className="flex items-center space-x-3 space-y-0"
<div className="flex items-center gap-2 ml-4"> key={audience.id}
<Checkbox id="terms" />
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Profissionais
</label>
</div>
<div className="flex items-center gap-2 ml-4">
<Checkbox id="terms" />
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
Empresas <FormControl>
</label> <RadioGroupItem value={audience.id} />
</FormControl>
<FormLabel className="font-normal">
{audience.name}
</FormLabel>
</FormItem>
))}
</RadioGroup>
</FormControl>
)}
/>
</div> </div>
<h6 className="text-xl text-purple-100 my-4"> <h6 className="text-xl text-purple-100 my-4">
Qual o tipo de curso? Qual o tipo de curso?
</h6> </h6>
{categoriesLoading && <LoadingSpinIcon className="ml-10" />}
<div className="flex items-center gap-2 ml-4"> <div className="flex items-center gap-2 ml-4">
<Checkbox id="terms" /> <FormField
<label control={form.control}
htmlFor="terms" name="categoryId"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" render={({ field }) => (
<FormControl>
<RadioGroup
value={field.value}
onValueChange={field.onChange}
className="flex flex-col space-y-4"
> >
Curso rápido {categories?.data.map((category) => (
</label> <FormItem
</div> className="flex items-center space-x-3 space-y-0"
<div className="flex items-center gap-2 ml-4"> key={category.id}
<Checkbox id="terms" />
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
Curso de aprofundamento <FormControl>
</label> <RadioGroupItem value={category.id} />
</div> </FormControl>
<div className="flex items-center gap-2 ml-4"> <FormLabel className="font-normal">
<Checkbox id="terms" /> {category.name}
<label </FormLabel>
htmlFor="terms" </FormItem>
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" ))}
> </RadioGroup>
Curso Corporativo </FormControl>
</label> )}
/>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
</Form>
</section> </section>
) )
} }
import { StyledInputs } from '@/components/mui/styled-inputs'
import { TextField } from '@mui/material'
import { useForm } from 'react-hook-form'
type FormModulesProps = {
index?: number
remove?: (index: number | number[]) => void
}
export function FormNewModules({ index, remove }: FormModulesProps) {
const { register } = useForm()
return (
<div className="flex flex-col p-4 gap-6 border border-gray-100">
<TextField
label="Título"
variant="standard"
type="text"
sx={StyledInputs({ color: '#26AAA7' })}
{...register(`newModule.${index}.name`)}
/>
<TextField
label="Descrição"
variant="standard"
type="text"
multiline
rows={4}
sx={StyledInputs({ color: '#26AAA7' })}
{...register(`newModule.${index}.description`)}
/>
</div>
)
}
'use client' 'use client'
import { InputMui } from '@/components/mui/inputs' import { getAreas } from '@/api/areas'
import { getAudiences } from '@/api/audiences'
import { getCategories } from '@/api/categories'
import { createCourses } from '@/api/courses'
import { createModules, getModules, ModulesProps } from '@/api/modules'
import BreadcrumbComponent from '@/components/breadcrumb-component'
import InputFile from '@/components/input-file'
import { LoadingSpinIcon } from '@/components/loading-spin-icon'
import { StyledInputs } from '@/components/mui/styled-inputs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar' import { Calendar } from '@/components/ui/calendar'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@/components/ui/form'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/components/ui/popover' } from '@/components/ui/popover'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { MenuItem } from '@mui/material' import { zodResolver } from '@hookform/resolvers/zod'
import { Box, Chip, MenuItem, TextField } from '@mui/material'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { AxiosError } from 'axios'
import { format } from 'date-fns' import { format } from 'date-fns'
import { CalendarIcon, PlusIcon, Trash } from 'lucide-react' import { CalendarIcon, Trash2 } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useState } from 'react' import { useState } from 'react'
import { Controller, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
const options = [ const FormSchema = z.object({
{ label: 'The Godfather', value: 1 }, name: z.string().trim().min(3, { message: 'Título obrigatório' }),
{ label: 'Pulp Fiction', value: 2 }, description: z.string().trim().min(3, { message: 'Descrição obrigatória' }),
] desktopBanner: z.string().nullable(),
mobileBanner: z.string().nullable(),
startDate: z.date().optional(),
endDate: z.date().optional(),
professors: z.string().array(),
workload: z.string().trim().min(1, { message: 'Horas do curso necessário' }),
areaId: z.string().trim().min(1, { message: 'Selecione a área' }),
audienceId: z.string(),
categoryId: z.string(),
moduleIds: z.string().array(),
})
export default function Curso() { export default function Curso() {
const [date, setDate] = useState<Date>() const [checkedStart, setCheckedStart] = useState<boolean>(false)
const [checkedEnd, setCheckedEnd] = useState<boolean>(false)
const [dateStart, setDateStart] = useState<Date>()
const [dateEnd, setDateEnd] = useState<Date>()
const [inputValue, setInputValue] = useState<string>('')
const [desktopBanner, setDesktopBanner] = useState<File | null>(null)
const [mobileBanner, setMobileBanner] = useState<File | null>(null)
const router = useRouter()
const queryClient = useQueryClient()
const { register, handleSubmit } = useForm<ModulesProps>()
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
areaId: '',
professors: [],
moduleIds: [],
desktopBanner: null,
mobileBanner: null,
},
})
const values = form.watch()
const { data: areas } = useQuery({
queryKey: ['areas'],
queryFn: getAreas,
})
const { data: modules } = useQuery({
queryKey: ['modules'],
queryFn: getModules,
})
const { data: audiences, isLoading: audiencesLoading } = useQuery({
queryKey: ['audiences'],
queryFn: getAudiences,
})
const { data: categories, isLoading: categoriesLoading } = useQuery({
queryKey: ['categories'],
queryFn: getCategories,
})
function onCheckedStart(checkedStart: boolean) {
setCheckedStart(checkedStart)
}
function onCheckedEnd(checkedEnd: boolean) {
setCheckedEnd(checkedEnd)
}
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' && inputValue.trim()) {
event.preventDefault()
form.setValue('professors', [...values.professors, inputValue.trim()])
setInputValue('')
}
}
const handleDelete = (professorToDelete: string) => {
form.setValue(
'professors',
values.professors.filter((str) => str !== professorToDelete),
)
}
const mutation = useMutation({
mutationFn: createModules,
onSuccess: () => {
toast.success('Módulo criado com sucesso!')
queryClient.invalidateQueries({ queryKey: ['modules'] })
},
})
const selectedModuleIds = form.watch('moduleIds') || []
async function addNewModuler(data: ModulesProps) {
const response = await mutation.mutateAsync(data)
const responseModuleId = response.data.id
form.setValue('moduleIds', [...selectedModuleIds, responseModuleId])
}
const selectedModules = modules?.data.filter((module) =>
selectedModuleIds.includes(module.id),
)
const handleSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedId = event.target.value
if (selectedId && !selectedModuleIds.includes(selectedId)) {
form.setValue('moduleIds', [...selectedModuleIds, selectedId])
}
}
const handleDeleteModule = (idToRemove: string) => {
const updatedIds = selectedModuleIds.filter((id) => id !== idToRemove)
form.setValue('moduleIds', updatedIds)
}
const mutationCreateCourse = useMutation({
mutationFn: createCourses,
onSuccess: () => {
toast.success('Curso criado com sucesso!')
queryClient.invalidateQueries({ queryKey: ['courses'] })
router.push('/admin')
},
onError: (error) => {
if (error instanceof AxiosError) {
const { message } = error.response?.data
toast.error(`${message}`)
}
},
})
async function onSubmit(data: z.infer<typeof FormSchema>) {
if (!desktopBanner) {
form.setError('desktopBanner', {
type: 'manual',
message: 'Foto desktop obrigatória!',
})
return
}
if (!mobileBanner) {
form.setError('mobileBanner', {
type: 'manual',
message: 'Foto mobile obrigatória!',
})
return
}
if (!dateStart) {
form.setError('startDate', {
type: 'manual',
message: 'Data de início obrigatória!',
})
return
}
if (!dateEnd) {
form.setError('endDate', {
type: 'manual',
message: 'Data de fim obrigatória!',
})
return
}
const urlCapaDesktop = await handleUpload(desktopBanner)
const urlCapaMobile = await handleUpload(mobileBanner)
const body = {
...data,
startDate: dateStart ? format(dateStart, 'yyyy-MM-dd') : '',
endDate: dateEnd ? format(dateEnd, 'yyyy-MM-dd') : '',
desktopBanner: urlCapaDesktop,
mobileBanner: urlCapaMobile,
workload: Number(data.workload),
}
mutationCreateCourse.mutateAsync(body)
}
const handleUpload = async (file: File | null) => {
if (!file) return
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await res.json()
return data.url
}
return ( return (
<section className="container py-10"> <section className="container py-10">
<form> <BreadcrumbComponent page="Curso" />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<h1 className="text-green-700 text-3xl font-bold">Criar curso</h1> <h1 className="text-3xl font-bold">Criar curso</h1>
<Button variant="third" className="uppercase"> <Button type="submit" variant="secondary" className="uppercase">
Salvar como rascunho
</Button>
<Button variant="secondary" className="uppercase">
Salvar e publicar Salvar e publicar
</Button> </Button>
<Button
variant="ghost"
className="uppercase flex items-center gap-2 text-orange-100 hover:text-orange-100"
>
<span>Apagar curso</span>
<Trash />
</Button>
</div> </div>
<div className="flex w-full mt-10"> <div className="flex w-full mt-10">
<div className="flex-1 flex flex-col border-r border-gray-50 pr-6 space-y-4"> <div className="flex-1 flex flex-col border-r border-gray-50 pr-6 space-y-4">
...@@ -50,81 +250,179 @@ export default function Curso() { ...@@ -50,81 +250,179 @@ export default function Curso() {
Informações sobre o curso Informações sobre o curso
</h6> </h6>
<InputMui label="Nome do curso" variant="standard" type="text" /> <TextField
label="Nome do curso"
variant="standard"
type="text"
{...form.register('name')}
sx={StyledInputs(
form.formState.errors.name
? { color: '#dc2626' }
: { color: '#26AAA7' },
)}
/>
<InputMui type="text" variant="standard" label="Área" select> <TextField
{options.map((option) => ( type="text"
<MenuItem key={option.value} value={option.value}> variant="standard"
{option.label} label="Área"
select
defaultValue={''}
{...form.register('areaId')}
sx={StyledInputs(
form.formState.errors.areaId
? { color: '#dc2626' }
: { color: '#26AAA7' },
)}
>
<MenuItem value="" className="hidden">
Selecione a área
</MenuItem>
{areas?.data.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem> </MenuItem>
))} ))}
</InputMui> </TextField>
<InputMui <TextField
label="Descrição do Curso" label="Descrição do Curso"
variant="standard" variant="standard"
type="text" type="text"
multiline multiline
rows={4} rows={4}
{...form.register('description')}
sx={StyledInputs(
form.formState.errors.description
? { color: '#dc2626' }
: { color: '#26AAA7' },
)}
/> />
<InputMui <Controller
label="Nome dos(as) Professores(as):" control={form.control}
name="professors"
render={({ field }) => (
<Box
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
>
<TextField
label="Digite os nomes dos professores e pressione Enter"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
variant="standard" variant="standard"
type="text" sx={StyledInputs({ color: '#26AAA7' })}
/>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{field.value.map((str, index) => (
<Chip
key={index}
label={str}
onDelete={() => handleDelete(str)}
variant="outlined"
color="secondary"
sx={{ color: 'white' }}
/>
))}
</Box>
</Box>
)}
/> />
<div className="flex justify-between flex-wrap gap-6"> <div className="flex justify-between flex-wrap gap-6 py-6">
<div className="flex flex-col flex-1 mt-6"> <div className="flex flex-col flex-1 mt-6">
<h6 className="text-xl text-purple-100">Banner Desktop</h6> <h6
<span className="mt-4">Dimensões recomendadas: </span> className={cn(
<span>Formatos aceitos: .png, .jpg</span> 'text-xl text-purple-100',
<Button form.formState.errors.desktopBanner && 'text-red-600',
variant="third" )}
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto md:w-[436px]"
> >
Adicionar banner Capa do curso desktop
</Button> </h6>
<span className="mt-4">
Dimensões recomendadas: 348 x 464
</span>
<span>Formatos aceitos: .png, .jpg</span>
<InputFile
id="desktopBanner"
label={
!desktopBanner ? 'Adicionar capa' : desktopBanner.name
}
onChange={(e) =>
setDesktopBanner(e.target.files?.[0] || null)
}
/>
</div> </div>
<div className="flex flex-col flex-1 mt-6"> <div className="flex flex-col flex-1 mt-6">
<h6 className="text-xl text-purple-100">Banner Mobile</h6> <h6
<span className="mt-4">Dimensões recomendadas: </span> className={cn(
<span>Formatos aceitos: .png, .jpg</span> 'text-xl text-purple-100',
<Button form.formState.errors.mobileBanner && 'text-red-600',
variant="third" )}
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto md:w-[436px]"
> >
Adicionar banner Capa do curso mobile
</Button> </h6>
<span className="mt-4">
Dimensões recomendadas: 348 x 464
</span>
<span>Formatos aceitos: .png, .jpg</span>
<InputFile
id="mobileBanner"
label={!mobileBanner ? 'Adicionar capa' : mobileBanner.name}
onChange={(e) =>
setMobileBanner(e.target.files?.[0] || null)
}
/>
</div> </div>
</div> </div>
<Separator className="!mt-10 !mb-4" />
<h6 className="text-xl text-purple-100 mb-4">Horas</h6>
<TextField
label="Total de horas do curso"
variant="standard"
type="number"
{...form.register('workload')}
sx={StyledInputs(
form.formState.errors.workload
? { color: '#dc2626' }
: { color: '#26AAA7' },
)}
/>
<Separator className="!mt-10 !mb-4" /> <Separator className="!mt-10 !mb-4" />
<h6 className="text-xl text-purple-100 mb-4">Datas</h6> <h6 className="text-xl text-purple-100 mb-4">Datas</h6>
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox id="start" /> <Checkbox id="start" onCheckedChange={onCheckedStart} />
<label <label
htmlFor="start" htmlFor="start"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
form.formState.errors.startDate && 'text-red-600',
)}
> >
Definir data de início Definir data de início
</label> </label>
</div> </div>
{checkedStart && (
<div className="flex flex-col"> <div className="flex flex-col">
<Label>Data de início</Label> <Label>Data de início</Label>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant={'ghost'} variant={'ghost'}
type="button"
className={cn( className={cn(
'lg:w-[450px] justify-start text-left font-normal border-b rounded-none pl-0', 'lg:w-[450px] justify-start text-left font-normal border-b rounded-none pl-0 border-green-400',
!date && 'text-muted-foreground', !dateStart && 'text-muted-foreground',
)} )}
> >
{date ? ( {dateStart ? (
format(date, 'dd/MM/yyyy') format(dateStart, 'dd/MM/yyyy')
) : ( ) : (
<span className="text-gray-600">MM/DD/YYYY</span> <span className="text-gray-600">MM/DD/YYYY</span>
)} )}
...@@ -134,37 +432,44 @@ export default function Curso() { ...@@ -134,37 +432,44 @@ export default function Curso() {
<PopoverContent className="w-auto p-0"> <PopoverContent className="w-auto p-0">
<Calendar <Calendar
mode="single" mode="single"
selected={date} selected={dateStart}
onSelect={setDate} onSelect={setDateStart}
initialFocus initialFocus
{...form.register('startDate')}
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
)}
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox id="closure" /> <Checkbox id="end" onCheckedChange={onCheckedEnd} />
<label <label
htmlFor="closure" htmlFor="closure"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
form.formState.errors.endDate && 'text-red-600',
)}
> >
Definir data de encerramento Definir data de encerramento
</label> </label>
</div> </div>
{checkedEnd && (
<div className="flex flex-col"> <div className="flex flex-col">
<Label>Data de encerramento</Label> <Label>Data de encerramento</Label>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant={'ghost'} variant={'ghost'}
type="button"
className={cn( className={cn(
'lg:w-[450px] justify-start text-left font-normal border-b rounded-none pl-0', 'lg:w-[450px] justify-start text-left font-normal border-b rounded-none pl-0 border-green-400',
!date && 'text-muted-foreground', !dateEnd && 'text-muted-foreground',
)} )}
> >
{date ? ( {dateEnd ? (
format(date, 'dd/MM/yyyy') format(dateEnd, 'dd/MM/yyyy')
) : ( ) : (
<span className="text-gray-600">MM/DD/YYYY</span> <span className="text-gray-600">MM/DD/YYYY</span>
)} )}
...@@ -174,13 +479,15 @@ export default function Curso() { ...@@ -174,13 +479,15 @@ export default function Curso() {
<PopoverContent className="w-auto p-0"> <PopoverContent className="w-auto p-0">
<Calendar <Calendar
mode="single" mode="single"
selected={date} selected={dateEnd}
onSelect={setDate} onSelect={setDateEnd}
initialFocus initialFocus
{...form.register('endDate')}
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
)}
</div> </div>
</div> </div>
...@@ -188,89 +495,146 @@ export default function Curso() { ...@@ -188,89 +495,146 @@ export default function Curso() {
<h6 className="text-xl text-purple-100 mb-4">Módulos</h6> <h6 className="text-xl text-purple-100 mb-4">Módulos</h6>
<TextField
type="text"
variant="standard"
label="Selecione os módulos do curso"
select
defaultValue={''}
value=""
onChange={handleSelect}
// {...form.register('moduleIds')}
sx={StyledInputs(
form.formState.errors.moduleIds
? { color: '#dc2626' }
: { color: '#26AAA7' },
)}
>
<MenuItem value="" className="hidden">
Selecione os módulos
</MenuItem>
{modules?.data.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
{!modules?.data.length && <p>Não há módulo criado!</p>}
</TextField>
{selectedModules?.map((item) => (
<div
key={item.id}
className="w-full flex justify-between bg-green-800/30 py-2 px-4 rounded-sm"
>
<p>{item.name}</p>
<Trash2
className="cursor-pointer text-red-400 hover:text-red-100"
onClick={() => handleDeleteModule(item.id)}
/>
</div>
))}
<div className="flex flex-col p-4 gap-6 border border-gray-100"> <div className="flex flex-col p-4 gap-6 border border-gray-100">
<InputMui label="Título" variant="standard" type="text" /> <TextField
label="Título"
variant="standard"
type="text"
sx={StyledInputs({ color: '#26AAA7' })}
{...register('name')}
/>
<InputMui <TextField
label="Descrição" label="Descrição"
variant="standard" variant="standard"
type="text" type="text"
multiline multiline
rows={4} rows={4}
sx={StyledInputs({ color: '#26AAA7' })}
{...register('description')}
/> />
</div> </div>
<Button <Button
variant="third" variant="third"
type="button"
className="uppercase w-52 flex items-center gap-2 !mt-8" className="uppercase w-52 flex items-center gap-2 !mt-8"
onClick={handleSubmit(addNewModuler)}
> >
<PlusIcon size={20} /> <span>Adicionar Módulo</span> <span>Criar novo Módulo</span>
</Button> </Button>
</div> </div>
<div className="w-[380px] pl-6 flex flex-col gap-4"> <div className="w-[380px] pl-6 flex flex-col gap-4">
<h6 className="text-xl text-purple-100 mb-4"> <h6 className="text-xl text-purple-100 mb-4">
Qual o público alvo? Qual o público alvo?
</h6> </h6>
{audiencesLoading && <LoadingSpinIcon className="ml-10" />}
<div className="flex items-center gap-2 ml-4"> <div className="flex items-center gap-2 ml-4">
<Checkbox id="terms" /> <FormField
<label control={form.control}
htmlFor="terms" name="audienceId"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" render={({ field }) => (
> <FormControl>
Estudante <RadioGroup
</label> onValueChange={field.onChange}
</div> defaultValue={field.value}
<div className="flex items-center gap-2 ml-4"> className="flex flex-col space-y-4"
<Checkbox id="terms" />
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
Profissionais {audiences?.data.map((audience) => (
</label> <FormItem
</div> className="flex items-center space-x-3 space-y-0"
<div className="flex items-center gap-2 ml-4"> key={audience.id}
<Checkbox id="terms" />
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
Empresas <FormControl>
</label> <RadioGroupItem value={audience.id} />
</FormControl>
<FormLabel className="font-normal">
{audience.name}
</FormLabel>
</FormItem>
))}
</RadioGroup>
</FormControl>
)}
/>
</div> </div>
<h6 className="text-xl text-purple-100 my-4"> <h6 className="text-xl text-purple-100 my-4">
Qual o tipo de curso? Qual o tipo de curso?
</h6> </h6>
{categoriesLoading && <LoadingSpinIcon className="ml-10" />}
<div className="flex items-center gap-2 ml-4"> <div className="flex items-center gap-2 ml-4">
<Checkbox id="terms" /> <FormField
<label control={form.control}
htmlFor="terms" name="categoryId"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" render={({ field }) => (
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-4"
> >
Curso rápido {categories?.data.map((category) => (
</label> <FormItem
</div> className="flex items-center space-x-3 space-y-0"
<div className="flex items-center gap-2 ml-4"> key={category.id}
<Checkbox id="terms" />
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
Curso de aprofundamento <FormControl>
</label> <RadioGroupItem value={category.id} />
</div> </FormControl>
<div className="flex items-center gap-2 ml-4"> <FormLabel className="font-normal">
<Checkbox id="terms" /> {category.name}
<label </FormLabel>
htmlFor="terms" </FormItem>
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" ))}
> </RadioGroup>
Curso Corporativo </FormControl>
</label> )}
/>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
</Form>
</section> </section>
) )
} }
'use client'
import { getAreas } from '@/api/areas'
import { getAudiences } from '@/api/audiences'
import { getCategories } from '@/api/categories'
import { getCourses } from '@/api/courses'
import { Card } from '@/components/card' import { Card } from '@/components/card'
import { InputMui } from '@/components/mui/inputs' import { InputMui } from '@/components/mui/inputs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { CoursesCard } from '@/utils/courses-array' import { AuthContext } from '@/contexts/auth-context'
import { useQuery } from '@tanstack/react-query'
import { Pencil, Search } from 'lucide-react' import { Pencil, Search } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import { useContext, useEffect, useState } from 'react'
const areas = [ import { useForm } from 'react-hook-form'
{
id: '1',
title: 'Educação',
},
{
id: '2',
title: 'Negócios',
},
{
id: '3',
title: 'Comunicação',
},
{
id: '4',
title: 'Saúde',
},
{
id: '5',
title: 'Tecnologia',
},
{
id: '6',
title: 'Teologia',
},
]
const banners = [
{
title: 'Banner Home',
},
{
title: 'CTAs',
},
]
export default function Admin() { export default function Admin() {
const { register, watch } = useForm()
const { user } = useContext(AuthContext)
const search = watch('search', '')
const [debouncedSearch, setDebouncedSearch] = useState('')
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearch(search)
}, 500)
return () => clearTimeout(handler)
}, [search])
const {
data: areas,
error,
isLoading: areasLoading,
isError: areasIsError,
} = useQuery({ queryKey: ['areas'], queryFn: getAreas })
const { data: categories } = useQuery({
queryKey: ['categories'],
queryFn: getCategories,
})
const {
data: audiences,
isLoading: audiencesLoading,
isError: audiencesIsError,
} = useQuery({
queryKey: ['audiences'],
queryFn: getAudiences,
})
const {
data: courses,
isLoading: coursesLoading,
isError: coursesIsError,
} = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
const filteredCourses = courses?.data.filter((course) =>
course.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
)
if (areasLoading || coursesLoading || audiencesLoading) {
return <span>Loading...</span>
}
if (areasIsError || audiencesIsError || coursesIsError) {
return <span>Error: {error?.message}</span>
}
return ( return (
<section className="container"> <section className="container">
<h1 className="text-green-400 text-2xl">Bem vindo, João da Silva,</h1> <h1 className="text-green-400 text-2xl">Bem vindo, {user?.name}!</h1>
<div className="flex h-auto mb-20"> <div className="flex h-auto mb-20">
<aside className="mt-10 w-[348px]"> <aside className="mt-10 w-[348px]">
<h2 className="text-purple-50 text-2xl">Áreas</h2> <h2 className="text-purple-50 text-2xl">Áreas</h2>
...@@ -55,13 +84,35 @@ export default function Admin() { ...@@ -55,13 +84,35 @@ export default function Admin() {
<nav className="my-6"> <nav className="my-6">
<ul className="space-y-4"> <ul className="space-y-4">
{areas.map((area) => ( {areas?.data.map((area) => (
<li key={area.title} className="border-b border-green-400"> <li key={area.id} className="border-b border-green-400">
<Link <Link
href={`/admin/area/${area.id}`} href={`/admin/area/${area.id}`}
className="flex justify-between py-2 hover:bg-green-400/10" className="flex justify-between py-2 hover:bg-green-400/10"
> >
<span>{area.title}</span> <span>{area.name}</span>
<Pencil className="text-green-400" />
</Link>
</li>
))}
</ul>
</nav>
<h2 className="text-purple-50 text-2xl mt-10">Audiência</h2>
<Button className="uppercase mt-6" asChild>
<Link href="admin/audience">+ Adicionar novo</Link>
</Button>
<nav className="my-6">
<ul className="space-y-4">
{audiences?.data.map((audience) => (
<li key={audience.id} className="border-b border-green-400">
<Link
href={`/admin/audience/${audience.id}`}
className="flex justify-between py-2 hover:bg-green-400/10"
>
<span>{audience.name}</span>
<Pencil className="text-green-400" /> <Pencil className="text-green-400" />
</Link> </Link>
</li> </li>
...@@ -69,16 +120,23 @@ export default function Admin() { ...@@ -69,16 +120,23 @@ export default function Admin() {
</ul> </ul>
</nav> </nav>
<h2 className="text-purple-50 text-2xl mt-10">Banners</h2> <h2 className="text-purple-50 text-2xl mt-10">Categoria</h2>
<Button className="uppercase mt-6" asChild>
<Link href="admin/category">+ Adicionar novo</Link>
</Button>
<nav className="my-6"> <nav className="my-6">
<ul className="space-y-4"> <ul className="space-y-4">
{banners.map((banner) => ( {categories?.data.map((category) => (
<li <li key={category.id} className="border-b border-green-400">
key={banner.title} <Link
className="flex justify-between border-b border-green-400 py-2" href={`/admin/category/${category.id}`}
className="flex justify-between py-2 hover:bg-green-400/10"
> >
<Link href="#">{banner.title}</Link> <span>{category.name}</span>
<Pencil className="text-green-400" /> <Pencil className="text-green-400" />
</Link>
</li> </li>
))} ))}
</ul> </ul>
...@@ -96,19 +154,29 @@ export default function Admin() { ...@@ -96,19 +154,29 @@ export default function Admin() {
type="text" type="text"
variant="standard" variant="standard"
className="w-full" className="w-full"
themeColor="#fafafa"
name="search"
register={register}
/> />
<Search size={24} className="-ml-6" /> <Search size={24} className="-ml-6" />
</div> </div>
{courses?.data.length === 0 ? (
<p className="text-yellow-100">Não há curso cadastrado!</p>
) : (
<div className="flex flex-wrap justify-around gap-4"> <div className="flex flex-wrap justify-around gap-4">
{CoursesCard.map((course) => ( {filteredCourses?.map((course) => (
<Link <Link
key={course.id} key={course.id}
className="lowercase scale-100 hover:scale-105 duration-300" className="lowercase scale-100 hover:scale-105 duration-300"
href={`admin/curso/${course.id}`} href={`admin/curso/${course.id}`}
> >
<div className="border border-gray-100 pb-4 rounded-lg overflow-hidden"> <div className="border border-gray-100 pb-4 rounded-lg overflow-hidden">
<Card.Image image={course.image.src} width={240} height={320}> <Card.Image
<Card.Title title={course.title} /> image={course.mobileBanner || ''}
width={240}
height={320}
>
<Card.Title title={course.name} />
</Card.Image> </Card.Image>
<Button <Button
variant="secondary" variant="secondary"
...@@ -120,7 +188,11 @@ export default function Admin() { ...@@ -120,7 +188,11 @@ export default function Admin() {
</div> </div>
</Link> </Link>
))} ))}
{filteredCourses?.length === 0 && (
<p className="text-yellow-100">Nenhum curso encontrado!</p>
)}
</div> </div>
)}
</div> </div>
</div> </div>
</section> </section>
......
import { NextResponse } from 'next/server'
import nodemailer from 'nodemailer'
export async function POST(req: Request) {
try {
const { fullname, email, subject, message } = await req.json()
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: Number(process.env.EMAIL_PORT),
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
})
const mailOptions = {
from: email,
to: process.env.EMAIL_SEVENPRO,
subject: `Nova dúvida de ${fullname}: ${subject}`,
text: message,
html: `<p><strong>Nome:</strong> ${fullname}</p><p><strong>Assunto:</strong> ${subject}</p><p><strong>Mensagem:</strong></p><p>${message}</p>`,
}
await transporter.sendMail(mailOptions)
return NextResponse.json(
{ message: 'E-mail enviado com sucesso!' },
{ status: 200 },
)
} catch (error) {
return NextResponse.json(
{ error: 'Erro ao enviar o e-mail.' },
{ status: 500 },
)
}
}
import { writeFile } from 'fs/promises'
import path from 'path'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const formData = await req.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
}
try {
const arrayBuffer = await file.arrayBuffer()
const buffer = new Uint8Array(arrayBuffer)
const fileName = `${Date.now()}-${file.name}`
const filePath = path.join(process.cwd(), 'public', 'imgs', fileName)
await writeFile(filePath, buffer)
const fileUrl = `/imgs/${fileName}` // caminho relativo acessível pela tag <img src=...>
return NextResponse.json({ url: fileUrl })
} catch (error) {
console.error('Upload failed:', error)
return NextResponse.json({ error: 'Upload failed' }, { status: 500 })
}
}
'use client'
import { InputMui } from '@/components/mui/inputs' import { InputMui } from '@/components/mui/inputs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { zodResolver } from '@hookform/resolvers/zod'
import { MenuItem } from '@mui/material' import { MenuItem } from '@mui/material'
import { Clock4, Mail, Smartphone } from 'lucide-react' import { Clock4, Mail, Smartphone } from 'lucide-react'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
const matters = [ const matters = [
{ {
...@@ -22,7 +28,57 @@ const matters = [ ...@@ -22,7 +28,57 @@ const matters = [
}, },
] ]
type DataEmail = {
fullname: string
email: string
message: string
subject: string
}
const schema = z
.object({
fullname: z.string().min(5, { message: 'Digite seu nome completo' }),
email: z.string().email({ message: 'E-mail inválido' }),
message: z
.string()
.min(5, { message: 'Mensagem deve ter no mínimo 5 caracteres' }),
subject: z.string().min(2, { message: 'Selecione uma assunto' }),
})
.required()
export default function Contact() { export default function Contact() {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<DataEmail>({
resolver: zodResolver(schema),
})
const [status, setStatus] = useState('')
async function onSubmit(data: DataEmail) {
setStatus('Enviando...')
try {
const response = await fetch('/api/send-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
if (response.ok) {
setStatus('E-mail enviado com sucesso!')
reset()
} else {
setStatus('Erro ao enviar o e-mail.')
}
} catch (error) {
setStatus('Erro ao enviar o e-mail.')
}
}
return ( return (
<main className="container flex flex-col justify-center items-center py-20"> <main className="container flex flex-col justify-center items-center py-20">
<h1 className="text-center font-normal md:font-extrabold text-green-800 text-2xl md:text-4xl"> <h1 className="text-center font-normal md:font-extrabold text-green-800 text-2xl md:text-4xl">
...@@ -36,15 +92,15 @@ export default function Contact() { ...@@ -36,15 +92,15 @@ export default function Contact() {
<ul className="flex flex-col gap-4 mt-6"> <ul className="flex flex-col gap-4 mt-6">
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
<Clock4 className="text-green-800" /> <Clock4 className="text-green-800" />
<span>De XXh à XXh</span> <span>De segunda à sexta das 08h às 18h</span>
</li> </li>
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
<Smartphone className="text-green-800" /> <Smartphone className="text-green-800" />
<span>Telefones: (XX) XXXX-XXXX</span> <span>Telefones: (44) 9725-0427</span>
</li> </li>
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
<Mail className="text-green-800" /> <Mail className="text-green-800" />
<span>E-mail: sevenpro@sevenpro.com.br</span> <span>E-mail: id.sevenpro@gmail.com</span>
</li> </li>
</ul> </ul>
</div> </div>
...@@ -52,23 +108,59 @@ export default function Contact() { ...@@ -52,23 +108,59 @@ export default function Contact() {
<h2 className="text-green-800 text-xl md:text-2xl"> <h2 className="text-green-800 text-xl md:text-2xl">
Envie uma mensagem Envie uma mensagem
</h2> </h2>
<form action="" className="mt-6 flex flex-col gap-6"> <form
<InputMui type="text" variant="outlined" label="Nome completo" /> onSubmit={handleSubmit(onSubmit)}
className="mt-6 flex flex-col gap-6"
>
<InputMui
type="text"
variant="outlined"
label="Nome completo"
name="fullname"
register={register}
/>
{errors.fullname?.message && (
<p className="text-red-600">{errors.fullname?.message}</p>
)}
<InputMui
type="text"
variant="outlined"
label="E-mail"
name="email"
register={register}
/>
{errors.email?.message && (
<p className="text-red-600">{errors.email?.message}</p>
)}
<InputMui <InputMui
type="text" type="text"
variant="outlined" variant="outlined"
label="Mensagem" label="Mensagem"
multiline multiline
rows={4} rows={4}
name="message"
register={register}
/> />
{errors.message?.message && (
<InputMui type="text" variant="outlined" label="Assunto" select> <p className="text-red-600">{errors.message?.message}</p>
)}
<InputMui
type="text"
variant="outlined"
label="Assunto"
select
name="subject"
register={register}
>
{matters.map((option) => ( {matters.map((option) => (
<MenuItem key={option.value} value={option.value}> <MenuItem key={option.value} value={option.value}>
{option.label} {option.label}
</MenuItem> </MenuItem>
))} ))}
</InputMui> </InputMui>
{errors.subject?.message && (
<p className="text-red-600">{errors.subject?.message}</p>
)}
<div className="flex"> <div className="flex">
<Button type="submit" className="w-auto"> <Button type="submit" className="w-auto">
...@@ -76,6 +168,7 @@ export default function Contact() { ...@@ -76,6 +168,7 @@ export default function Contact() {
</Button> </Button>
</div> </div>
</form> </form>
{status && <p className="mt-4 text-sm text-white">{status}</p>}
</div> </div>
</div> </div>
</main> </main>
......
'use client'
import Image from 'next/image' import Image from 'next/image'
import { getCourseId } from '@/api/courses'
import { BannerCategory } from '@/components/banner-category' import { BannerCategory } from '@/components/banner-category'
import { CarouselComponent } from '@/components/corousel-component' import { CarouselComponent } from '@/components/corousel-component'
import { CourseDetails } from '@/components/course-details' import { CourseDetails } from '@/components/course-details'
...@@ -11,19 +14,35 @@ import { ...@@ -11,19 +14,35 @@ import {
} from '@/components/ui/accordion' } from '@/components/ui/accordion'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { useQuery } from '@tanstack/react-query'
import { useParams } from 'next/navigation'
import imageCapa from '../../../../public/images/banner.png' import imageCapa from '../../../../public/images/banner.png'
export default function CoursePage() { export default function CoursePage() {
const params = useParams<{ courseId: string }>()
const { data: course, isLoading } = useQuery({
queryKey: ['course', params.courseId],
queryFn: () => getCourseId(params.courseId),
enabled: !!params.courseId,
})
if (isLoading) {
return <p>Carregando...</p>
}
return ( return (
<> <>
<BannerCategory /> <BannerCategory data={course?.data.area.name} />
<div className="flex flex-col md:flex-row px-6 pt-20 gap-6"> <div className="flex flex-col md:flex-row px-6 pt-20 gap-6">
<div className="flex flex-col gap-7"> <div className="flex flex-col gap-7">
<h1 className="block md:hidden text-green-400 text-3xl font-thin"> <h1 className="block md:hidden text-green-400 text-3xl font-thin">
História Contemporânea dos Estados Unidos {course?.data.name}
</h1> </h1>
<Image <Image
src={imageCapa} src={course?.data.mobileBanner || imageCapa}
width={500}
height={500}
className="min-w-[348px] h-[464px] object-cover rounded-lg" className="min-w-[348px] h-[464px] object-cover rounded-lg"
alt="Imagem de capa do curso" alt="Imagem de capa do curso"
/> />
...@@ -33,43 +52,43 @@ export default function CoursePage() { ...@@ -33,43 +52,43 @@ export default function CoursePage() {
</div> </div>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<h1 className="hidden md:block text-green-400 text-4xl font-extrabold"> <h1 className="hidden md:block text-green-400 text-4xl font-extrabold">
História Contemporânea dos Estados Unidos {course?.data.name}
</h1> </h1>
<div className="flex flex-col-reverse lg:flex-row gap-6 w-full"> <div className="flex flex-col-reverse lg:flex-row gap-6 w-full">
<div className="flex flex-col lg:w-1/2"> <div className="flex flex-col lg:w-1/2">
<h3 className="font-bold">O que você vai aprender</h3> <h3 className="font-bold">O que você vai aprender</h3>
<p> <p>{course?.data.description}</p>
Este curso levará você por uma jornada desde os tumultuados anos
de 1970 até os dias atuais, explorando os impactos decisivos dos
governos de Bush, Obama, Trump e Biden. Descubra os
momentos-chave que moldaram a história contemporânea dos Estados
Unidos, desde a resiliência diante de crises até as mudanças
geopolíticas e sociais. Vamos desvendar os bastidores dos
eventos que marcaram época, proporcionando uma compreensão
profunda e contextualizada da evolução norte-americana.
Prepare-se para uma experiência de aprendizado envolvente e
reveladora, onde o passado ilumina o presente, moldando o
futuro.
</p>
<h3 className="font-bold mt-6">Com quem você vai aprender</h3> <h3 className="font-bold mt-6">Com quem você vai aprender</h3>
<p>Professor João Silva</p> {course?.data.professors &&
course?.data.professors.length > 0 && (
<p>{course?.data.professors.join(', ')}</p>
)}
</div> </div>
<div className="lg:w-1/2"> <div className="lg:w-1/2">
<CourseDetails /> <CourseDetails data={course?.data} />
</div> </div>
</div> </div>
<Button variant="secondary" className="uppercase block md:hidden"> <Button variant="secondary" className="uppercase block md:hidden">
Quero este curso Quero este curso
</Button> </Button>
<span className="font-bold mt-4 block md:hidden">Saiba mais:</span> <span className="font-bold mt-4 block md:hidden">Saiba mais:</span>
<Accordion type="single" collapsible className="w-full"> {course?.data.modules
<AccordionItem value="item-1"> .sort((a, b) => a.module.name.localeCompare(b.module.name))
<AccordionTrigger>Módulos</AccordionTrigger> .map((module) => (
<Accordion
type="single"
collapsible
className="w-full"
key={module.module.id}
>
<AccordionItem value={module.module.id}>
<AccordionTrigger>{module.module.name}</AccordionTrigger>
<AccordionContent> <AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern. {module.module.description}
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
))}
</div> </div>
</div> </div>
<div className="my-10 px-6"> <div className="my-10 px-6">
......
'use client'
import { getCourses } from '@/api/courses'
import { About } from '@/components/about' import { About } from '@/components/about'
import { BannerCategory } from '@/components/banner-category' import { BannerCategory } from '@/components/banner-category'
import { Card } from '@/components/card' import { Card } from '@/components/card'
import { Differences } from '@/components/differences' import { Differences } from '@/components/differences'
import { NavLinkCategory } from '@/components/nav-link-category' import { NavLinkCategory } from '@/components/nav-link-category'
import { PaginationComponent } from '@/components/pagination-component'
import SearchFilter from '@/components/search-filter'
import { SignUp } from '@/components/sign-up' import { SignUp } from '@/components/sign-up'
import { SkeletonSerachParams } from '@/components/skeleton-serach-params' import { SkeletonSerachParams } from '@/components/skeleton-serach-params'
import { CoursesCard } from '@/utils/courses-array' import { formatDate } from '@/utils/formatDate'
import { useQuery } from '@tanstack/react-query'
import { Calendar, Clock4, User } from 'lucide-react' import { Calendar, Clock4, User } from 'lucide-react'
import { usePathname, useSearchParams } from 'next/navigation'
import { Suspense } from 'react' import { Suspense } from 'react'
export default function Companies() { export default function Companies() {
const path = usePathname()
const searchParams = useSearchParams()
const category = searchParams.get('area')
const { data: courses, isLoading: coursesLoading } = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
const filterCoursesAudience = courses?.data.filter((item) =>
path.toLowerCase().includes(item?.audience?.name.toLowerCase() as string),
)
const filterCoursesAudiencesAndArea = filterCoursesAudience?.filter((area) =>
category?.includes(area?.area?.name.toLowerCase() as string),
)
if (coursesLoading) {
return <p>Carregando...</p>
}
return ( return (
<main> <main>
<Suspense fallback={<SkeletonSerachParams />}> <Suspense fallback={<SkeletonSerachParams />}>
<BannerCategory /> <BannerCategory />
<NavLinkCategory /> <NavLinkCategory />
</Suspense> </Suspense>
<SearchFilter /> {/* <SearchFilter /> */}
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6"> <div className="grid rounded-none grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6">
{CoursesCard.map((course) => ( {!category &&
filterCoursesAudience &&
filterCoursesAudience.map((course) => (
<div className="flex justify-center" key={course.id}> <div className="flex justify-center" key={course.id}>
<Card.Root link={`curso/${course.id}`}> <Card.Root link={`/curso/${course.id}`}>
<Card.Image image={course.image.src} width={273} height={365}> <Card.Image
<Card.Title title={course.title} /> image={course.mobileBanner as string}
width={273}
height={365}
>
<Card.Title title={course.name} />
</Card.Image> </Card.Image>
<Card.Content description={course.hours}> <Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} /> <Card.Icon icon={Clock4} />
</Card.Content> </Card.Content>
<Card.Content description={course.category}> <Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} /> <Card.Icon icon={User} />
</Card.Content> </Card.Content>
<Card.Content description={course.calender}> <Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} /> <Card.Icon icon={Calendar} />
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
</div> </div>
))} ))}
{category &&
filterCoursesAudiencesAndArea &&
filterCoursesAudiencesAndArea.map((course) => (
<div className="flex justify-center" key={course.id}>
<Card.Root link={`/curso/${course.id}`}>
<Card.Image
image={course.mobileBanner as string}
width={273}
height={365}
>
<Card.Title title={course.name} />
</Card.Image>
<Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} />
</Card.Content>
<Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</div> </div>
<div className="my-6"> ))}
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
{category && filterCoursesAudiencesAndArea?.length === 0 && (
<p>
Não há cursos para a área <b>{category}</b> com audiência{' '}
<b>{path.replace('/', '')}</b>
</p>
)}
{!category && filterCoursesAudience?.length === 0 && (
<p>
Não há cursos para a audiência <b>{path.replace('/', '')}</b>
</p>
)}
</div> </div>
{/* <div className="my-6">
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
</div> */}
<Differences /> <Differences />
<SignUp /> <SignUp />
<About.Root> <About.Root>
......
'use client'
import { getCourses } from '@/api/courses'
import { About } from '@/components/about' import { About } from '@/components/about'
import { BannerCategory } from '@/components/banner-category' import { BannerCategory } from '@/components/banner-category'
import { Card } from '@/components/card' import { Card } from '@/components/card'
import { Differences } from '@/components/differences' import { Differences } from '@/components/differences'
import { NavLinkCategory } from '@/components/nav-link-category' import { NavLinkCategory } from '@/components/nav-link-category'
import { PaginationComponent } from '@/components/pagination-component'
import SearchFilter from '@/components/search-filter'
import { SignUp } from '@/components/sign-up' import { SignUp } from '@/components/sign-up'
import { SkeletonSerachParams } from '@/components/skeleton-serach-params' import { SkeletonSerachParams } from '@/components/skeleton-serach-params'
import { CoursesCard } from '@/utils/courses-array' import { formatDate } from '@/utils/formatDate'
import { useQuery } from '@tanstack/react-query'
import { Calendar, Clock4, User } from 'lucide-react' import { Calendar, Clock4, User } from 'lucide-react'
import { usePathname, useSearchParams } from 'next/navigation'
import { Suspense } from 'react' import { Suspense } from 'react'
export default function Students() { export default function Students() {
const path = usePathname()
const searchParams = useSearchParams()
const category = searchParams.get('area')
const { data: courses, isLoading: coursesLoading } = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
const filterCoursesAudience = courses?.data.filter((item) =>
path.toLowerCase().includes(item?.audience?.name.toLowerCase() as string),
)
const filterCoursesAudiencesAndArea = filterCoursesAudience?.filter((area) =>
category?.includes(area?.area?.name.toLowerCase() as string),
)
if (coursesLoading) {
return <p>Carregando...</p>
}
return ( return (
<main> <main>
<Suspense fallback={<SkeletonSerachParams />}> <Suspense fallback={<SkeletonSerachParams />}>
<BannerCategory /> <BannerCategory />
<NavLinkCategory /> <NavLinkCategory />
</Suspense> </Suspense>
<SearchFilter /> {/* <SearchFilter /> */}
<div className="grid rounded-none grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6"> <div className="grid rounded-none grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6">
{CoursesCard.map((course) => ( {!category &&
filterCoursesAudience &&
filterCoursesAudience.map((course) => (
<div className="flex justify-center" key={course.id}> <div className="flex justify-center" key={course.id}>
<Card.Root link={`curso/${course.id}`}> <Card.Root link={`/curso/${course.id}`}>
<Card.Image image={course.image.src} width={273} height={365}> <Card.Image
<Card.Title title={course.title} /> image={course.mobileBanner as string}
width={273}
height={365}
>
<Card.Title title={course.name} />
</Card.Image> </Card.Image>
<Card.Content description={course.hours}> <Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} /> <Card.Icon icon={Clock4} />
</Card.Content> </Card.Content>
<Card.Content description={course.category}> <Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} /> <Card.Icon icon={User} />
</Card.Content> </Card.Content>
<Card.Content description={course.calender}> <Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} /> <Card.Icon icon={Calendar} />
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
</div> </div>
))} ))}
{category &&
filterCoursesAudiencesAndArea &&
filterCoursesAudiencesAndArea.map((course) => (
<div className="flex justify-center" key={course.id}>
<Card.Root link={`/curso/${course.id}`}>
<Card.Image
image={course.mobileBanner as string}
width={273}
height={365}
>
<Card.Title title={course.name} />
</Card.Image>
<Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} />
</Card.Content>
<Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</div> </div>
<div className="my-6"> ))}
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
{category && filterCoursesAudiencesAndArea?.length === 0 && (
<p>
Não há cursos para a área <b>{category}</b> com audiência{' '}
<b>{path.replace('/', '')}</b>
</p>
)}
{!category && filterCoursesAudience?.length === 0 && (
<p>
Não há cursos para a audiência <b>{path.replace('/', '')}</b>
</p>
)}
</div> </div>
{/* <div className="my-6">
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
</div> */}
<Differences /> <Differences />
<SignUp /> <SignUp />
<About.Root> <About.Root>
......
...@@ -5,6 +5,10 @@ import '@/styles/globals.css' ...@@ -5,6 +5,10 @@ import '@/styles/globals.css'
import { StyledEngineProvider } from '@mui/material' import { StyledEngineProvider } from '@mui/material'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import { Poppins } from 'next/font/google' import { Poppins } from 'next/font/google'
import { Toaster } from 'sonner'
import { AuthProvider } from '@/contexts/auth-context'
import ReactQueryProvider from '@/providers/react-query'
const poppins = Poppins({ subsets: ['latin'], weight: ['300', '400', '800'] }) const poppins = Poppins({ subsets: ['latin'], weight: ['300', '400', '800'] })
...@@ -21,6 +25,7 @@ export default function RootLayout({ ...@@ -21,6 +25,7 @@ export default function RootLayout({
return ( return (
<html lang="pt-BR"> <html lang="pt-BR">
<body className={`antialiased ${poppins.className}`}> <body className={`antialiased ${poppins.className}`}>
<AuthProvider>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="dark" defaultTheme="dark"
...@@ -28,11 +33,15 @@ export default function RootLayout({ ...@@ -28,11 +33,15 @@ export default function RootLayout({
disableTransitionOnChange disableTransitionOnChange
> >
<StyledEngineProvider injectFirst> <StyledEngineProvider injectFirst>
<ReactQueryProvider>
<Toaster richColors />
<Header /> <Header />
{children} {children}
<Footer /> <Footer />
</ReactQueryProvider>
</StyledEngineProvider> </StyledEngineProvider>
</ThemeProvider> </ThemeProvider>
</AuthProvider>
</body> </body>
</html> </html>
) )
......
'use client'
import { InputMui } from '@/components/mui/inputs' import { InputMui } from '@/components/mui/inputs'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { AuthContext } from '@/contexts/auth-context'
import { AxiosError } from 'axios'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useContext } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import globo from '../../../public/images/globo.svg' import globo from '../../../public/images/globo.svg'
type FormLogin = {
email: string
password: string
}
export default function Login() { export default function Login() {
const { handleSubmit, register } = useForm<FormLogin>()
const { signIn } = useContext(AuthContext)
const router = useRouter()
const onSubmit = async (data: FormLogin) => {
try {
await signIn(data)
router.push('/admin')
} catch (error) {
if (error instanceof AxiosError) {
const { message } = error.response?.data
toast.error(`${message}`)
}
}
}
return ( return (
<main> <main>
<section className=" container flex flex-col justify-center items-center space-y-10 h-[723px]"> <section className=" container flex flex-col justify-center items-center space-y-10 h-[723px]">
...@@ -20,15 +48,27 @@ export default function Login() { ...@@ -20,15 +48,27 @@ export default function Login() {
Acesso exclusivo para usuários cadastrados. Acesso exclusivo para usuários cadastrados.
</h2> </h2>
<form <form
// onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="w-full md:w-[600px] flex flex-col gap-8 order-4" className="w-full md:w-[600px] flex flex-col gap-8 order-4"
> >
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<InputMui label="E-mail" variant="outlined" type="email" /> <InputMui
<InputMui label="Senha" variant="outlined" type="password" /> label="E-mail"
variant="outlined"
type="email"
name="email"
register={register}
/>
<InputMui
label="Senha"
variant="outlined"
type="password"
name="password"
register={register}
/>
</div> </div>
<div className="flex items-center space-x-2"> {/* <div className="flex items-center space-x-2">
<Checkbox id="remember" /> <Checkbox id="remember" />
<label <label
htmlFor="remember" htmlFor="remember"
...@@ -36,9 +76,10 @@ export default function Login() { ...@@ -36,9 +76,10 @@ export default function Login() {
> >
Lembrar-me Lembrar-me
</label> </label>
</div> </div> */}
<Button className="rounded-sm" asChild> <Button className="rounded-sm" type="submit">
<Link href="/admin">Entrar</Link> {/* <Link href="/admin">Entrar</Link> */}
Entrar
</Button> </Button>
<Button <Button
variant={'link'} variant={'link'}
......
'use client'
import { getCourses } from '@/api/courses'
import { About } from '@/components/about' import { About } from '@/components/about'
import { BannerCategory } from '@/components/banner-category' import { BannerCategory } from '@/components/banner-category'
import { Card } from '@/components/card' import { Card } from '@/components/card'
import { Differences } from '@/components/differences' import { Differences } from '@/components/differences'
import { NavLinkCategory } from '@/components/nav-link-category' import { NavLinkCategory } from '@/components/nav-link-category'
import { PaginationComponent } from '@/components/pagination-component'
import SearchFilter from '@/components/search-filter'
import { SignUp } from '@/components/sign-up' import { SignUp } from '@/components/sign-up'
import { SkeletonSerachParams } from '@/components/skeleton-serach-params' import { SkeletonSerachParams } from '@/components/skeleton-serach-params'
import { CoursesCard } from '@/utils/courses-array' import { formatDate } from '@/utils/formatDate'
import { useQuery } from '@tanstack/react-query'
import { Calendar, Clock4, User } from 'lucide-react' import { Calendar, Clock4, User } from 'lucide-react'
import { usePathname, useSearchParams } from 'next/navigation'
import { Suspense } from 'react' import { Suspense } from 'react'
export default function Professionals() { export default function Professionals() {
const path = usePathname()
const searchParams = useSearchParams()
const category = searchParams.get('area')
const { data: courses, isLoading: coursesLoading } = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
const filterCoursesAudience = courses?.data.filter((item) =>
path.toLowerCase().includes(item?.audience?.name.toLowerCase() as string),
)
const filterCoursesAudiencesAndArea = filterCoursesAudience?.filter((area) =>
category?.includes(area?.area?.name.toLowerCase() as string),
)
if (coursesLoading) {
return <p>Carregando...</p>
}
return ( return (
<main> <main>
<Suspense fallback={<SkeletonSerachParams />}> <Suspense fallback={<SkeletonSerachParams />}>
<BannerCategory /> <BannerCategory />
<NavLinkCategory /> <NavLinkCategory />
</Suspense> </Suspense>
<SearchFilter /> {/* <SearchFilter /> */}
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6"> <div className="grid rounded-none grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 grid-rows-3 lg:grid-rows-2 gap-4 md:gap-6 p-6">
{CoursesCard.map((course) => ( {!category &&
filterCoursesAudience &&
filterCoursesAudience.map((course) => (
<div className="flex justify-center" key={course.id}> <div className="flex justify-center" key={course.id}>
<Card.Root link={`curso/${course.id}`}> <Card.Root link={`/curso/${course.id}`}>
<Card.Image image={course.image.src} width={273} height={365}> <Card.Image
<Card.Title title={course.title} /> image={course.mobileBanner as string}
width={273}
height={365}
>
<Card.Title title={course.name} />
</Card.Image> </Card.Image>
<Card.Content description={course.hours}> <Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} /> <Card.Icon icon={Clock4} />
</Card.Content> </Card.Content>
<Card.Content description={course.category}> <Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} /> <Card.Icon icon={User} />
</Card.Content> </Card.Content>
<Card.Content description={course.calender}> <Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} /> <Card.Icon icon={Calendar} />
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
</div> </div>
))} ))}
{category &&
filterCoursesAudiencesAndArea &&
filterCoursesAudiencesAndArea.map((course) => (
<div className="flex justify-center" key={course.id}>
<Card.Root link={`/curso/${course.id}`}>
<Card.Image
image={course.mobileBanner as string}
width={273}
height={365}
>
<Card.Title title={course.name} />
</Card.Image>
<Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} />
</Card.Content>
<Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</div> </div>
<div className="my-6"> ))}
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
{category && filterCoursesAudiencesAndArea?.length === 0 && (
<p>
Não há cursos para a área <b>{category}</b> com audiência{' '}
<b>{path.replace('/', '')}</b>
</p>
)}
{!category && filterCoursesAudience?.length === 0 && (
<p>
Não há cursos para a audiência <b>{path.replace('/', '')}</b>
</p>
)}
</div> </div>
{/* <div className="my-6">
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
</div> */}
<Differences /> <Differences />
<SignUp /> <SignUp />
<About.Root> <About.Root>
......
...@@ -2,76 +2,49 @@ ...@@ -2,76 +2,49 @@
import Image from 'next/image' import Image from 'next/image'
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
import bannerComunicacao from '../../public/images/banners/comunicacao.jpg'
import bannerEducacao from '../../public/images/banners/educacao.jpg'
import bannerNegocios from '../../public/images/banners/negocios.jpg'
import bannerSaude from '../../public/images/banners/saude.jpg'
import bannerStudants from '../../public/images/banners/students_banner.jpg' import bannerStudants from '../../public/images/banners/students_banner.jpg'
import bannerTecnologia from '../../public/images/banners/tecnologia.jpg'
import bannerTeologia from '../../public/images/banners/teologia.jpg'
import bannerComunicacaoMobile from '../../public/images/banners/comunicacao-mobile.jpg' import { getAreas } from '@/api/areas'
import bannerEducacaoMobile from '../../public/images/banners/educacao-mobile.jpg' import { useQuery } from '@tanstack/react-query'
import bannerNegociosMobile from '../../public/images/banners/negocios-mobile.jpg'
import bannerSaudeMobile from '../../public/images/banners/saude-mobile.jpg'
import bannerTecnologiaMobile from '../../public/images/banners/tecnologia-mobile.jpg'
import bannerTeologiaMobile from '../../public/images/banners/teologia-mobile.jpg'
export function BannerCategory() { export function BannerCategory(areaBanner: { data?: string | undefined }) {
const searchParams = useSearchParams() const searchParams = useSearchParams()
const category = searchParams.get('categoria') const category = searchParams.get('area')
const { data: areas } = useQuery({ queryKey: ['areas'], queryFn: getAreas })
function desktopBannerCategory() { const bannerImage = areas?.data.find(
switch (category) { (item) => item.name.toLowerCase() === category?.toLowerCase(),
case 'tecnologia': )
return bannerTecnologia
case 'negocios':
return bannerNegocios
case 'comunicacao':
return bannerComunicacao
case 'educacao':
return bannerEducacao
case 'saude':
return bannerSaude
case 'teologia':
return bannerTeologia
default:
return bannerStudants
}
}
function mobileBannerCategory() { const bannerImageArea = areas?.data.find(
switch (category) { (item) => item.name.toLowerCase() === areaBanner?.data?.toLowerCase(),
case 'tecnologia': )
return bannerTecnologiaMobile
case 'negocios':
return bannerNegociosMobile
case 'comunicacao':
return bannerComunicacaoMobile
case 'educacao':
return bannerEducacaoMobile
case 'saude':
return bannerSaudeMobile
case 'teologia':
return bannerTeologiaMobile
default:
return bannerStudants
}
}
return ( return (
<div className="h-[300px] flex items-center justify-end"> <div className="h-[300px] flex items-center justify-end">
<Image <Image
alt="banner" alt="banner"
className="hidden md:flex w-full h-full object-cover" className="hidden md:flex w-full h-full object-cover"
src={desktopBannerCategory()} width={100}
height={100}
src={
bannerImage?.desktopBanner ||
bannerImageArea?.desktopBanner ||
bannerStudants
}
unoptimized unoptimized
/> />
<Image <Image
alt="banner" alt="banner"
className="flex md:hidden w-full h-full object-cover" className="flex md:hidden w-full h-full object-cover"
src={mobileBannerCategory()} width={100}
height={100}
src={
bannerImage?.mobileBanner ||
bannerImageArea?.mobileBanner ||
bannerStudants
}
unoptimized unoptimized
/> />
</div> </div>
......
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
import Link from 'next/link'
type BreadcrumbProps = {
page: string
}
export default function BreadcrumbComponent({ page }: BreadcrumbProps) {
return (
<Breadcrumb className="my-4">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/admin">Admin</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{page}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
...@@ -10,7 +10,7 @@ type CardImageProps = { ...@@ -10,7 +10,7 @@ type CardImageProps = {
export function CardImage({ children, image, width, height }: CardImageProps) { export function CardImage({ children, image, width, height }: CardImageProps) {
return ( return (
<div className={`relative w-[${width}px] `}> <div className={`relative w-[${width}px]`}>
<Image <Image
src={image} src={image}
width={width} width={width}
......
'use client'
import Link from 'next/link' import Link from 'next/link'
import { ReactNode } from 'react' import { ReactNode } from 'react'
...@@ -9,10 +11,12 @@ type CardRootProps = { ...@@ -9,10 +11,12 @@ type CardRootProps = {
export function CardRoot({ children, link }: CardRootProps) { export function CardRoot({ children, link }: CardRootProps) {
return ( return (
<Link <Link
className="lowercase transform scale-95 hover:scale-100 transition-transform duration-300 drop-shadow" className="lowercase transform scale-95 hover:scale-100 transition-transform duration-300 drop-shadow "
href={link} href={link}
> >
<div className={`border border-gray-100 pl-0 rounded-lg max-w-[273px] `}> <div
className={`border border-gray-100 pl-0 rounded-lg max-w-[273px] h-full`}
>
{children} {children}
</div> </div>
</Link> </Link>
......
'use client'
import { Calendar, Clock4, User } from 'lucide-react' import { Calendar, Clock4, User } from 'lucide-react'
import { CoursesCard } from '@/utils/courses-array' import { getCourses } from '@/api/courses'
import { useQuery } from '@tanstack/react-query'
import { Card } from './card' import { Card } from './card'
import { import {
Carousel, Carousel,
...@@ -11,32 +14,47 @@ import { ...@@ -11,32 +14,47 @@ import {
} from './ui/carousel' } from './ui/carousel'
export function CarouselComponent() { export function CarouselComponent() {
const { data: courses } = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
return ( return (
<section> <section>
<Carousel> <Carousel>
{courses ? (
<CarouselContent> <CarouselContent>
{CoursesCard.map((course) => ( {courses.data.slice(0, 10).map((course) => (
<CarouselItem <CarouselItem
key={course.id} key={course.id}
className="sm:basis-1/2 md:basis-1/3 lg:basis-1/4 xl:basis-1/5 flex justify-center" className="sm:basis-1/2 md:basis-1/3 lg:basis-1/4 xl:basis-1/5 flex justify-center"
> >
<Card.Root link={`curso/${course.id}`}> <Card.Root link={`/curso/${course.id}`}>
<Card.Image image={course.image.src} width={240} height={320}> <Card.Image
<Card.Title title={course.title} /> image={course.mobileBanner || ''}
width={240}
height={320}
>
<Card.Title title={course.name} />
</Card.Image> </Card.Image>
<Card.Content description={course.hours}> <Card.Content
description={`${String(course.workload)} horas`}
>
<Card.Icon icon={Clock4} /> <Card.Icon icon={Clock4} />
</Card.Content> </Card.Content>
<Card.Content description={course.category}> <Card.Content description={course?.audience?.name || ''}>
<Card.Icon icon={User} /> <Card.Icon icon={User} />
</Card.Content> </Card.Content>
<Card.Content description={course.calender}> <Card.Content description={course?.category?.name || ''}>
<Card.Icon icon={Calendar} /> <Card.Icon icon={Calendar} />
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
</CarouselItem> </CarouselItem>
))} ))}
</CarouselContent> </CarouselContent>
) : (
<p>Não há cursos disponíveis</p>
)}
<CarouselPrevious /> <CarouselPrevious />
<CarouselNext /> <CarouselNext />
</Carousel> </Carousel>
......
import { Calendar, Clock4, DollarSign, UserRound } from 'lucide-react' import { CourseIdProps } from '@/api/courses'
import { format, parseISO } from 'date-fns'
import { toZonedTime } from 'date-fns-tz'
import { Calendar, Clock4, UserRound } from 'lucide-react'
type CourseProps = {
data?: CourseIdProps
}
export function CourseDetails(course: CourseProps) {
const formatDate = (dateString: string | undefined) => {
if (!dateString) return
const date = parseISO(dateString)
const utcDate = toZonedTime(date, 'UTC')
return format(utcDate, 'dd/MM/yyyy')
}
export function CourseDetails() {
return ( return (
<ul className="flex flex-col gap-8"> <ul className="flex flex-col gap-8">
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
...@@ -11,7 +26,9 @@ export function CourseDetails() { ...@@ -11,7 +26,9 @@ export function CourseDetails() {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="font-thin">Duração:</span> <span className="font-thin">Duração:</span>
<span className="text-xl">30 Hrs/Curso Rápido</span> <span className="text-xl">
{course.data?.workload.toString()} Hrs/{course.data?.category.name}
</span>
</div> </div>
</li> </li>
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
...@@ -22,7 +39,7 @@ export function CourseDetails() { ...@@ -22,7 +39,7 @@ export function CourseDetails() {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="font-thin">Indicado para:</span> <span className="font-thin">Indicado para:</span>
<span className="text-xl">Estudantes</span> <span className="text-xl">{course.data?.audience.name}</span>
</div> </div>
</li> </li>
<li className="flex items-center gap-4"> <li className="flex items-center gap-4">
...@@ -33,10 +50,10 @@ export function CourseDetails() { ...@@ -33,10 +50,10 @@ export function CourseDetails() {
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="font-thin">Início:</span> <span className="font-thin">Início:</span>
<span className="text-xl">Imediato</span> <span className="text-xl">{formatDate(course.data?.startDate)}</span>
</div> </div>
</li> </li>
<li className="flex items-center gap-4"> {/* <li className="flex items-center gap-4">
<div className="rounded-full w-[60px] h-[60px] flex items-center justify-center bg-gradient-to-r from-green-700 to-green-50"> <div className="rounded-full w-[60px] h-[60px] flex items-center justify-center bg-gradient-to-r from-green-700 to-green-50">
<div className="rounded-full flex items-center justify-center w-[57px] h-[57px] dark:bg-gray-900 bg-gray-50"> <div className="rounded-full flex items-center justify-center w-[57px] h-[57px] dark:bg-gray-900 bg-gray-50">
<DollarSign size={35} /> <DollarSign size={35} />
...@@ -46,7 +63,7 @@ export function CourseDetails() { ...@@ -46,7 +63,7 @@ export function CourseDetails() {
<span className="font-thin">Investimento:</span> <span className="font-thin">Investimento:</span>
<span className="text-xl">R$200,00</span> <span className="text-xl">R$200,00</span>
</div> </div>
</li> </li> */}
</ul> </ul>
) )
} }
import { forwardRef } from 'react'
interface InputFileProps extends React.InputHTMLAttributes<HTMLInputElement> {
id: string
values?: string
label: string
}
const InputFile = forwardRef<HTMLInputElement, InputFileProps>(
({ id, values, label, ...inputProps }, ref) => {
return (
<label
htmlFor={id}
className="flex justify-center items-center border border-dotted rounded-sm border-green-400 bg-green-400/10 cursor-pointer hover:bg-green-400/5 text-green-400 text-sm h-10 mt-6"
>
{values || label}
<input
id={id}
type="file"
accept="image/png, image/jpeg, image/jpg"
className="hidden"
ref={ref}
{...inputProps}
/>
</label>
)
},
)
InputFile.displayName = 'InputFile'
export default InputFile
import { cn } from '@/lib/utils'
type SpinProps = {
className?: React.HTMLProps<HTMLElement>['className']
}
export function LoadingSpinIcon({ className }: SpinProps) {
return (
<svg
className={cn('animate-spin -ml-1 mr-3 h-5 w-5 text-white', className)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
)
}
import {
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from './ui/alert-dialog'
import { Button } from './ui/button'
type AlertDialogProps = {
title: string
description: string
handleClick: () => void
}
export default function ModalDialog({
title,
description,
handleClick,
}: AlertDialogProps) {
return (
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<Button
onClick={handleClick}
variant="outline"
className="text-gray-50 bg-red-500 hover:bg-red-500/80"
>
Continue
</Button>
</AlertDialogFooter>
</AlertDialogContent>
)
}
'use client'
import { TextField } from '@mui/material'
import { StyledInputs } from './mui/styled-inputs'
import {
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from './ui/alert-dialog'
import { Button } from './ui/button'
type AlertDialogProps = {
title: string
description: string
value?: string
handleClick: () => void
handleClickDelete?: () => void
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
export default function ModalInputs({
title,
description,
value,
handleClick,
handleClickDelete,
onChange,
}: AlertDialogProps) {
return (
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<TextField
label="Nome"
type="text"
value={value}
onChange={onChange}
variant="standard"
className="flex"
sx={StyledInputs({ color: '#26AAA7' })}
/>
<AlertDialogFooter className="mt-4 w-full flex !justify-between">
{handleClickDelete && (
<Button
onClick={handleClickDelete}
variant="outline"
className="text-gray-50 bg-red-500 hover:bg-red-500/80"
>
Deletar
</Button>
)}
<div className='flex gap-4'>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<Button
onClick={handleClick}
variant="outline"
className="text-gray-50 bg-green-500 hover:bg-green-500/80"
>
Salvar
</Button>
</div>
</AlertDialogFooter>
</AlertDialogContent>
)
}
...@@ -3,28 +3,36 @@ ...@@ -3,28 +3,36 @@
import { useThemeClient } from '@/hooks/useThemeClient' import { useThemeClient } from '@/hooks/useThemeClient'
import { TextField, TextFieldProps } from '@mui/material' import { TextField, TextFieldProps } from '@mui/material'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { FieldValues, Path, UseFormRegister } from 'react-hook-form'
type InputMuiProps = TextFieldProps & { type InputMuiProps<T extends FieldValues> = TextFieldProps & {
label: string label: string
variant: 'standard' | 'filled' | 'outlined' variant: 'standard' | 'filled' | 'outlined'
themeColor?: string
children?: ReactNode children?: ReactNode
name: Path<T>
register: UseFormRegister<T>
} }
export function InputMui({ export const InputMui = <T extends FieldValues>({
label, label,
variant, variant,
children, children,
...props themeColor = '#fafafa',
}: InputMuiProps) { register,
name,
...rest
}: InputMuiProps<T>) => {
const themeConfig = useThemeClient() const themeConfig = useThemeClient()
const color = themeConfig === 'dark' ? '#fafafa' : '#3C3C3C' const color = themeConfig === 'dark' ? themeColor : '#3C3C3C'
return ( return (
<TextField <TextField
label={label} label={label}
variant={variant} variant={variant}
{...props} {...register(name)}
{...rest}
sx={{ sx={{
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
'& fieldset': { '& fieldset': {
...@@ -44,10 +52,10 @@ export function InputMui({ ...@@ -44,10 +52,10 @@ export function InputMui({
color: `${color}`, color: `${color}`,
}, },
'& input': { '& input': {
color: `${color}`, color: `#fafafa`,
}, },
'& .MuiInputBase-input': { '& .MuiInputBase-input': {
color: `${color}`, color: `#fafafa`,
}, },
'& .MuiInput-underline:before': { '& .MuiInput-underline:before': {
borderBottomColor: color, borderBottomColor: color,
......
'use client'
import { getAreas } from '@/api/areas'
import { useQuery } from '@tanstack/react-query'
import { NavLinkSearchParams } from './nav-link-search-params' import { NavLinkSearchParams } from './nav-link-search-params'
export function NavLinkCategory() { export function NavLinkCategory() {
const { data: areas } = useQuery({ queryKey: ['areas'], queryFn: getAreas })
return ( return (
<nav className="container my-10"> <nav className="container my-10">
<ul className="flex flex-wrap justify-center items-center gap-8"> <ul className="flex flex-wrap justify-center items-center gap-8">
<li> {areas?.data.map((area) => (
<NavLinkSearchParams <li key={area.id}>
variant="default"
href="tecnologia"
className="uppercase"
>
Tecnologia
</NavLinkSearchParams>
</li>
<li>
<NavLinkSearchParams
variant="default"
href="negocios"
className="uppercase"
>
Negócios
</NavLinkSearchParams>
</li>
<li>
<NavLinkSearchParams
variant="default"
href="saude"
className="uppercase"
>
Saúde
</NavLinkSearchParams>
</li>
<li>
<NavLinkSearchParams
variant="default"
href="educacao"
className="uppercase"
>
Educação
</NavLinkSearchParams>
</li>
<li>
<NavLinkSearchParams
variant="default"
href="comunicacao"
className="uppercase"
>
Comunicação
</NavLinkSearchParams>
</li>
<li>
<NavLinkSearchParams <NavLinkSearchParams
variant="default" variant="default"
href="teologia" href={area.name.toLowerCase()}
className="uppercase" className="uppercase"
> >
Teologia {area.name}
</NavLinkSearchParams> </NavLinkSearchParams>
</li> </li>
))}
</ul> </ul>
</nav> </nav>
) )
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import Link, { LinkProps } from 'next/link' import Link, { LinkProps } from 'next/link'
import { useSearchParams } from 'next/navigation' import { usePathname, useSearchParams } from 'next/navigation'
import { Button } from './ui/button' import { Button } from './ui/button'
interface NavLinkProps extends LinkProps { interface NavLinkProps extends LinkProps {
...@@ -23,16 +23,25 @@ interface NavLinkProps extends LinkProps { ...@@ -23,16 +23,25 @@ interface NavLinkProps extends LinkProps {
export function NavLinkSearchParams(props: NavLinkProps) { export function NavLinkSearchParams(props: NavLinkProps) {
const searchParams = useSearchParams() const searchParams = useSearchParams()
const category = searchParams.get('categoria') const pathname = usePathname()
const path = pathname
const area = searchParams.get('area')
return ( return (
<Button <Button
data-current={category === props.href} data-current={area === props.href}
className={`data-[current=true]:font-semibold rounded-2xl ${props.className}`} className={`data-[current=true]:font-semibold rounded-2xl ${props.className}`}
variant={category === props.href ? 'third' : props.variant} variant={area === props.href ? 'third' : props.variant}
asChild asChild
> >
<Link href={{ query: { categoria: props.href as string } }}> <Link
href={{
pathname: `${path === '/' ? '/estudantes' : path}`,
query: { area: props.href as string },
}}
>
{props.children} {props.children}
</Link> </Link>
</Button> </Button>
......
'use client'
import { Search } from 'lucide-react' import { Search } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { InputMui } from './mui/inputs' import { InputMui } from './mui/inputs'
import { Label } from './ui/label' import { Label } from './ui/label'
import { import {
...@@ -11,6 +14,8 @@ import { ...@@ -11,6 +14,8 @@ import {
} from './ui/select' } from './ui/select'
export default function SearchFilter() { export default function SearchFilter() {
const { register } = useForm()
return ( return (
<div className="container grid md:grid-cols-2 gap-8 items-end mb-8"> <div className="container grid md:grid-cols-2 gap-8 items-end mb-8">
<div className="flex items-center max-w-[712px] flex-1"> <div className="flex items-center max-w-[712px] flex-1">
...@@ -19,6 +24,8 @@ export default function SearchFilter() { ...@@ -19,6 +24,8 @@ export default function SearchFilter() {
variant="standard" variant="standard"
type="text" type="text"
className="w-full #fafafa" className="w-full #fafafa"
name="learned-today"
register={register}
/> />
<Search size={24} className="-ml-6" /> <Search size={24} className="-ml-6" />
</div> </div>
......
...@@ -2,24 +2,69 @@ ...@@ -2,24 +2,69 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { TextField } from '@mui/material' import { zodResolver } from '@hookform/resolvers/zod'
import Image from 'next/image' import Image from 'next/image'
import { SubmitHandler, useForm } from 'react-hook-form' import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
import signUpImage from '../../public/images/sign-up.png' import signUpImage from '../../public/images/sign-up.png'
import { StyledInputs } from './mui/styled-inputs' import { InputMui } from './mui/inputs'
type FormSignUpTypes = { type FormSignUpTypes = {
name: string fullname: string
email: string email: string
whatsapp: string whatsapp: string
interest: string area: string
course: string course: string
} }
const schema = z
.object({
fullname: z.string().min(5, { message: 'Digite seu nome completo' }),
email: z.string().email({ message: 'E-mail inválido' }),
whatsapp: z
.string()
.min(10, { message: 'Digite seu whatsapp com o DDD' })
.max(13, { message: 'Digite apenas o número do seu whatsapp' }),
area: z.string().min(2, { message: 'Informe a área' }),
course: z.string().min(2, { message: 'Informe o curso' }),
})
.required()
export function SignUp() { export function SignUp() {
const { handleSubmit } = useForm<FormSignUpTypes>() const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<FormSignUpTypes>({
resolver: zodResolver(schema),
})
const [status, setStatus] = useState('')
const onSubmit: SubmitHandler<FormSignUpTypes> = (data) => console.log(data) async function onSubmit(data: FormSignUpTypes) {
setStatus('Enviando...')
try {
const response = await fetch('/api/send-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
if (response.ok) {
toast.success('E-mail enviado com sucesso!')
setStatus('Inscrever')
reset()
} else {
toast.error('Falha ao enviar E-mail!')
setStatus('Inscrever')
}
} catch (error) {
setStatus('Inscrever')
}
}
return ( return (
<section className="w-full bg-gradient-to-r from-green-700 to-green-50 pb-6 md:py-6 md:pb-0"> <section className="w-full bg-gradient-to-r from-green-700 to-green-50 pb-6 md:py-6 md:pb-0">
...@@ -39,42 +84,62 @@ export function SignUp() { ...@@ -39,42 +84,62 @@ export function SignUp() {
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="w-full md:w-[600px] flex flex-col gap-8 order-4" className="w-full md:w-[600px] flex flex-col gap-8 order-4"
> >
<TextField <InputMui
label="Nome"
variant="standard"
type="text" type="text"
sx={StyledInputs({ color: '#fafafa' })}
/>
<TextField
label="Whatsapp"
variant="standard" variant="standard"
type="number" label="Nome completo"
sx={StyledInputs({ color: '#fafafa' })} name="fullname"
register={register}
/> />
<TextField {errors.fullname?.message && (
label="Área de interesse" <p className="text-red-600">{errors.fullname?.message}</p>
)}
<InputMui
type="number"
variant="standard" variant="standard"
type="text" label="Whatsapp"
sx={StyledInputs({ color: '#fafafa' })} name="whatsapp"
register={register}
/> />
<TextField {errors.whatsapp?.message && (
label="E-mail" <p className="text-red-600">{errors.whatsapp?.message}</p>
variant="standard" )}
<InputMui
type="text" type="text"
sx={StyledInputs({ color: '#fafafa' })} variant="standard"
label="Área de interesse"
name="area"
register={register}
/> />
<TextField {errors.area?.message && (
label="Curso desejado" <p className="text-red-600">{errors.area?.message}</p>
)}
<InputMui
type="text"
variant="standard" variant="standard"
label="E-mail"
name="email"
register={register}
/>
{errors.email?.message && (
<p className="text-red-600">{errors.email?.message}</p>
)}
<InputMui
type="text" type="text"
sx={StyledInputs({ color: '#fafafa' })} variant="standard"
label="Curso desejado"
name="course"
register={register}
/> />
{errors.course?.message && (
<p className="text-red-600">{errors.course?.message}</p>
)}
<Button <Button
type="submit" type="submit"
variant="secondary" variant="secondary"
className="uppercase max-w-32 mx-auto md:mb-6" className="uppercase max-w-32 mx-auto md:mb-6"
> >
Inscrever {status || 'Inscrever'}
</Button> </Button>
</form> </form>
</div> </div>
......
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
import { cva, VariantProps } from 'class-variance-authority'
import * as React from 'react' import * as React from 'react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
const inputVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-ring focus-visible:border-b-2 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none disabled:cursor-not-allowed',
{
variants: {
variant: {
default:
'flex h-10 w-full border border-t-0 border-x-0 border-gray-300 px-2 py-2 text-md text-white bg-transparent file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-300',
secondary:
'text-gray-50 bg-gradient-to-r from-purple-100 to-purple-200 hover:bg-purple-200 hover:opacity-75',
file: 'border border-dotted rounded-sm border-green-400 bg-green-400/10 cursor-pointer hover:bg-green-400/5 text-green-400 text-sm h-10',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
ghost:
'dark:hover:bg-white/10 dark:hover:text-gray-50 hover:bg-gray-900/10',
link: 'text-primary underline-offset-4 hover:underline',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {} extends React.InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {
label?: string
}
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, variant, type, label, ...props }, ref) => {
return ( return (
<label>
<span className="text-sm text-gray-400">{label}</span>
<input <input
type={type} type={type}
className={cn( className={cn(inputVariants({ variant, className }))}
'flex h-10 w-full border border-t-0 border-x-0 border-gray-300 px-2 py-2 text-md text-white ile:border-0 bg-transparent file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-300 focus-visible:outline-none focus-visible:border-b-white focus-visible:border-b-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref} ref={ref}
{...props} {...props}
/> />
</label>
) )
}, },
) )
Input.displayName = 'Input' Input.displayName = 'Input'
export { Input } export { Input, inputVariants }
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }
'use client'
// import { useRouter } from 'next/router'
import { parseCookies, setCookie } from 'nookies'
import { createContext, useEffect, useState } from 'react'
import { getAccountsId } from '@/api/accounts'
import { login } from '@/api/login'
import { api } from '@/lib/axios'
import { useRouter } from 'next/navigation'
type User = {
id: string
name: string
email: string
} | null
type SignInData = {
email: string
password: string
}
type AuthContextType = {
isAuthenticated: boolean
user: User
signIn: (data: SignInData) => Promise<void>
}
export const AuthContext = createContext({} as AuthContextType)
export function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null)
const router = useRouter()
const isAuthenticated = !!user
useEffect(() => {
const { 'sevenpro-token': token } = parseCookies()
const { 'sevenpro-user': userCookie } = parseCookies()
if (token && userCookie) {
const { id } = JSON.parse(userCookie)
getAccountsId(id).then((response) => {
setUser(response.data)
})
}
}, [])
async function signIn({ email, password }: SignInData) {
const { data } = await login({
email,
password,
})
const token = data.access_token
const user = data.user
setCookie(undefined, 'sevenpro-token', token, {
maxAge: 60 * 60 * 1, // 1 hour
})
setCookie(undefined, 'sevenpro-user', JSON.stringify(user), {
maxAge: 60 * 60 * 1, // 1 hour
})
api.defaults.headers.Authorization = `Bearer ${token}`
setUser(user)
router.push('/admin')
}
return (
<AuthContext.Provider value={{ user, isAuthenticated, signIn }}>
{children}
</AuthContext.Provider>
)
}
// import * as z from 'zod'
// const envSchema = z.object({
// NEXT_PUBLIC_URL_API: z.string().url(),
// NEXT_PUBLIC_CLOUDFLARE_ACCESS_ENDPOINT: z.string().url(),
// NEXT_PUBLIC_CLOUDFLARE_ACCESS_TOKEN_VALUE: z.string(),
// NEXT_PUBLIC_CLOUDFLARE_ACCESS_KEY_ID: z.string(),
// NEXT_PUBLIC_CLOUDFLARE_SECRET_ACCESS_KEY: z.string(),
// NEXT_PUBLIC_CLOUDFLARE_ACCESS_ACCOUNT_ID: z.string(),
// })
// export const env = envSchema.parse(process.env)
import axios from 'axios' import axios from 'axios'
// import Router from 'next/router'
import { destroyCookie, parseCookies } from 'nookies'
const { 'sevenpro-token': token } = parseCookies()
export const api = axios.create({ export const api = axios.create({
baseURL: '/api', baseURL: process.env.NEXT_PUBLIC_URL_API,
}) })
if (token) {
api.defaults.headers.Authorization = `Bearer ${token}`
}
// api.interceptors.request.use(
// (config) => {
// return config
// },
// (error) => {
// return Promise.reject(error)
// },
// )
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response && error.response.status === 401) {
// Remove o token inválido
destroyCookie(null, 'sevenpro-token')
// Redireciona para a página de login
// Router.push('/login')
}
return Promise.reject(error)
},
)
import { S3Client } from '@aws-sdk/client-s3'
export const s3 = new S3Client({
region: 'auto',
endpoint: process.env.NEXT_PUBLIC_CLOUDFLARE_ACCESS_ENDPOINT,
credentials: {
accessKeyId: process.env.NEXT_PUBLIC_CLOUDFLARE_ACCESS_KEY_ID!,
secretAccessKey: process.env.NEXT_PUBLIC_CLOUDFLARE_SECRET_ACCESS_KEY!,
},
})
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
export async function middleware(request: NextRequest) {
const token = request.cookies.get('sevenpro-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/admin/:path*'],
}
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactNode } from 'react'
type ReactQueryProps = {
children: ReactNode
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
})
export default function ReactQueryProvider({ children }: ReactQueryProps) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 0, 0%, 25%; --foreground: 0, 0%, 25%;
...@@ -64,13 +64,13 @@ ...@@ -64,13 +64,13 @@
--input: 240 3.7% 15.9%; --input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%; --ring: 240 4.9% 83.9%;
} }
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
\ No newline at end of file
import { format, parseISO } from "date-fns"
import { toZonedTime } from "date-fns-tz"
export const formatDate = (dateString: string | undefined) => {
if (!dateString) return
const date = parseISO(dateString)
const utcDate = toZonedTime(date, 'UTC')
return format(utcDate, 'dd/MM/yyyy')
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment