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
# INSTALAÇÃO DE DEPENDENCIAS
FROM base AS deps
RUN apk add --no-cache libc6-compat
# Diretório de trabalho dentro do container
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
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
# Copia os arquivos de dependência
COPY package.json package-lock.json ./
# Instala as dependências
RUN npm ci
# REBUILD CODIGO APENAS QUANDO PRECISA
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
# Copia o restante dos arquivos
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
# Build da aplicação Next.js
RUN npm run build
RUN \
if [ -f yarn.lock ]; then yarn run build; \
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
# Etapa 2: imagem final para produção
FROM node:22-alpine
# IMAGEM PARA PRODUCAO, COPIA TODOS OS ARQUIVOS DO REBUILD E EXECUTA
FROM base AS runner
# Diretório de trabalho
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copia os arquivos necessários do builder
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 ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
#USER nextjs
# Garante que a pasta imgs exista
RUN mkdir -p ./public/imgs
# Expõe a porta padrão
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
\ No newline at end of file
# Comando para iniciar a aplicação
CMD ["npm", "start"]
\ 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
module.exports = {
// ... rest of the configuration.
output: "standalone",
};
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
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.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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 { About } from '@/components/about'
......@@ -6,14 +7,16 @@ import { Banner } from '@/components/banner'
import { CarouselComponent } from '@/components/corousel-component'
import { CourseCategory } from '@/components/course-category'
import { Differences } from '@/components/differences'
import { InputMui } from '@/components/mui/inputs'
import { NavLinkCategory } from '@/components/nav-link-category'
import { SignUp } from '@/components/sign-up'
import { SkeletonSerachParams } from '@/components/skeleton-serach-params'
import { Button } from '@/components/ui/button'
import { Suspense } from 'react'
// import { useForm } from 'react-hook-form'
export default function Home() {
// const { register } = useForm()
return (
<>
<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() {
<Suspense fallback={<SkeletonSerachParams />}>
<NavLinkCategory />
</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
label="O que você quer aprender hoje?"
variant="standard"
type="text"
className="w-full #fafafa"
className="w-full"
themeColor="#fafafa"
name="learned-today"
register={register}
/>
<Search size={24} />
</div>
</div> */}
<CarouselComponent />
<Button
variant="secondary"
className="uppercase mx-auto my-8"
asChild
>
<Link href="#">Ver todos os cursos</Link>
<Link href="/estudantes">Ver todos os cursos</Link>
</Button>
</div>
<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 { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
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 { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
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 (
<section className="container py-10">
<BreadcrumbComponent page="Área" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6">
<h1 className="text-green-700 text-3xl font-bold">Editar Área</h1>
<Button variant="third" className="uppercase">
Salvar como rascunho
</Button>
<Button variant="secondary" className="uppercase">
<h1 className="text-3xl font-bold">Editar Área</h1>
<Button
variant="secondary"
type="submit"
className="uppercase rounded-sm"
disabled={isLoading}
>
{mutationEdit.isPending && <LoadingSpinIcon />}
Salvar e publicar
</Button>
<Button
variant="ghost"
className="uppercase flex items-center gap-2 text-orange-100 hover:text-orange-100"
<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 área</span>
<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 className="w-full">
<div className="w-full mt-16">
<TextField
label="Nome da área"
variant="standard"
type="text"
defaultValue={area?.data.name}
{...register('name')}
variant="standard"
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">
<h6 className="text-xl text-purple-100">Banner Desktop</h6>
<span className="mt-4">Dimensões recomendadas: </span>
<span>Formatos aceitos: .png, .jpg</span>
<Button
variant="third"
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full"
<h6
className={cn(
'text-xl text-purple-100',
errors.desktopBanner && 'text-red-600',
)}
>
Adicionar banner
</Button>
Banner Desktop
</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 className="flex flex-col flex-1 mt-6">
<h6 className="text-xl text-purple-100">Banner Mobile</h6>
<span className="mt-4">Dimensões recomendadas: </span>
<span>Formatos aceitos: .png, .jpg</span>
<Button
variant="third"
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full"
<h6
className={cn(
'text-xl text-purple-100',
errors.mobileBanner && 'text-red-600',
)}
>
Adicionar banner
</Button>
Banner Mobile
</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>
</form>
</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 { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { zodResolver } from '@hookform/resolvers/zod'
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() {
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 (
<section className="container py-10">
<BreadcrumbComponent page="Área" />
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex items-center gap-6">
<h1 className="text-green-700 text-3xl font-bold">Criar Área</h1>
<Button variant="third" className="uppercase">
Salvar como rascunho
</Button>
<Button variant="secondary" className="uppercase">
<h1 className="text-3xl font-bold">Criar Área</h1>
<Button variant="secondary" className="uppercase rounded-sm">
{mutation.isPending && <LoadingSpinIcon />}
Salvar e publicar
</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 className="w-full">
<div className="w-full mt-16">
<TextField
label="Nome da área"
variant="standard"
type="text"
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">
<h6 className="text-xl text-purple-100">Banner Desktop</h6>
<span className="mt-4">Dimensões recomendadas: </span>
<span>Formatos aceitos: .png, .jpg</span>
<Button
variant="third"
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full"
<h6
className={cn(
'text-xl text-purple-100',
errors.desktopBanner && 'text-red-600',
)}
>
Adicionar banner
</Button>
Banner Desktop
</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 className="flex flex-col flex-1 mt-6">
<h6 className="text-xl text-purple-100">Banner Mobile</h6>
<span className="mt-4">Dimensões recomendadas: </span>
<span>Formatos aceitos: .png, .jpg</span>
<Button
variant="third"
className="border border-dotted border-green-400 rounded-sm mt-6 mx-auto w-full"
<h6
className={cn(
'text-xl text-purple-100',
errors.mobileBanner && 'text-red-600',
)}
>
Adicionar banner
</Button>
Banner Mobile
</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>
</form>
</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>
)
}
This diff is collapsed.
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>
)
}
This diff is collapsed.
'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 { InputMui } from '@/components/mui/inputs'
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 Link from 'next/link'
const areas = [
{
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',
},
]
import { useContext, useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
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 (
<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">
<aside className="mt-10 w-[348px]">
<h2 className="text-purple-50 text-2xl">Áreas</h2>
......@@ -55,13 +84,35 @@ export default function Admin() {
<nav className="my-6">
<ul className="space-y-4">
{areas.map((area) => (
<li key={area.title} className="border-b border-green-400">
{areas?.data.map((area) => (
<li key={area.id} className="border-b border-green-400">
<Link
href={`/admin/area/${area.id}`}
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" />
</Link>
</li>
......@@ -69,16 +120,23 @@ export default function Admin() {
</ul>
</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">
<ul className="space-y-4">
{banners.map((banner) => (
<li
key={banner.title}
className="flex justify-between border-b border-green-400 py-2"
{categories?.data.map((category) => (
<li key={category.id} className="border-b border-green-400">
<Link
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" />
</Link>
</li>
))}
</ul>
......@@ -96,19 +154,29 @@ export default function Admin() {
type="text"
variant="standard"
className="w-full"
themeColor="#fafafa"
name="search"
register={register}
/>
<Search size={24} className="-ml-6" />
</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">
{CoursesCard.map((course) => (
{filteredCourses?.map((course) => (
<Link
key={course.id}
className="lowercase scale-100 hover:scale-105 duration-300"
href={`admin/curso/${course.id}`}
>
<div className="border border-gray-100 pb-4 rounded-lg overflow-hidden">
<Card.Image image={course.image.src} width={240} height={320}>
<Card.Title title={course.title} />
<Card.Image
image={course.mobileBanner || ''}
width={240}
height={320}
>
<Card.Title title={course.name} />
</Card.Image>
<Button
variant="secondary"
......@@ -120,7 +188,11 @@ export default function Admin() {
</div>
</Link>
))}
{filteredCourses?.length === 0 && (
<p className="text-yellow-100">Nenhum curso encontrado!</p>
)}
</div>
)}
</div>
</div>
</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 { Button } from '@/components/ui/button'
import { zodResolver } from '@hookform/resolvers/zod'
import { MenuItem } from '@mui/material'
import { Clock4, Mail, Smartphone } from 'lucide-react'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
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() {
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 (
<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">
......@@ -36,15 +92,15 @@ export default function Contact() {
<ul className="flex flex-col gap-4 mt-6">
<li className="flex items-center gap-4">
<Clock4 className="text-green-800" />
<span>De XXh à XXh</span>
<span>De segunda à sexta das 08h às 18h</span>
</li>
<li className="flex items-center gap-4">
<Smartphone className="text-green-800" />
<span>Telefones: (XX) XXXX-XXXX</span>
<span>Telefones: (44) 9725-0427</span>
</li>
<li className="flex items-center gap-4">
<Mail className="text-green-800" />
<span>E-mail: sevenpro@sevenpro.com.br</span>
<span>E-mail: id.sevenpro@gmail.com</span>
</li>
</ul>
</div>
......@@ -52,23 +108,59 @@ export default function Contact() {
<h2 className="text-green-800 text-xl md:text-2xl">
Envie uma mensagem
</h2>
<form action="" className="mt-6 flex flex-col gap-6">
<InputMui type="text" variant="outlined" label="Nome completo" />
<form
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
type="text"
variant="outlined"
label="Mensagem"
multiline
rows={4}
name="message"
register={register}
/>
<InputMui type="text" variant="outlined" label="Assunto" select>
{errors.message?.message && (
<p className="text-red-600">{errors.message?.message}</p>
)}
<InputMui
type="text"
variant="outlined"
label="Assunto"
select
name="subject"
register={register}
>
{matters.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</InputMui>
{errors.subject?.message && (
<p className="text-red-600">{errors.subject?.message}</p>
)}
<div className="flex">
<Button type="submit" className="w-auto">
......@@ -76,6 +168,7 @@ export default function Contact() {
</Button>
</div>
</form>
{status && <p className="mt-4 text-sm text-white">{status}</p>}
</div>
</div>
</main>
......
'use client'
import Image from 'next/image'
import { getCourseId } from '@/api/courses'
import { BannerCategory } from '@/components/banner-category'
import { CarouselComponent } from '@/components/corousel-component'
import { CourseDetails } from '@/components/course-details'
......@@ -11,19 +14,35 @@ import {
} from '@/components/ui/accordion'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { useQuery } from '@tanstack/react-query'
import { useParams } from 'next/navigation'
import imageCapa from '../../../../public/images/banner.png'
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 (
<>
<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 gap-7">
<h1 className="block md:hidden text-green-400 text-3xl font-thin">
História Contemporânea dos Estados Unidos
{course?.data.name}
</h1>
<Image
src={imageCapa}
src={course?.data.mobileBanner || imageCapa}
width={500}
height={500}
className="min-w-[348px] h-[464px] object-cover rounded-lg"
alt="Imagem de capa do curso"
/>
......@@ -33,43 +52,43 @@ export default function CoursePage() {
</div>
<div className="flex flex-col gap-6">
<h1 className="hidden md:block text-green-400 text-4xl font-extrabold">
História Contemporânea dos Estados Unidos
{course?.data.name}
</h1>
<div className="flex flex-col-reverse lg:flex-row gap-6 w-full">
<div className="flex flex-col lg:w-1/2">
<h3 className="font-bold">O que você vai aprender</h3>
<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>
<p>{course?.data.description}</p>
<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 className="lg:w-1/2">
<CourseDetails />
<CourseDetails data={course?.data} />
</div>
</div>
<Button variant="secondary" className="uppercase block md:hidden">
Quero este curso
</Button>
<span className="font-bold mt-4 block md:hidden">Saiba mais:</span>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>Módulos</AccordionTrigger>
{course?.data.modules
.sort((a, b) => a.module.name.localeCompare(b.module.name))
.map((module) => (
<Accordion
type="single"
collapsible
className="w-full"
key={module.module.id}
>
<AccordionItem value={module.module.id}>
<AccordionTrigger>{module.module.name}</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
{module.module.description}
</AccordionContent>
</AccordionItem>
</Accordion>
))}
</div>
</div>
<div className="my-10 px-6">
......
'use client'
import { getCourses } from '@/api/courses'
import { About } from '@/components/about'
import { BannerCategory } from '@/components/banner-category'
import { Card } from '@/components/card'
import { Differences } from '@/components/differences'
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 { 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 { usePathname, useSearchParams } from 'next/navigation'
import { Suspense } from 'react'
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 (
<main>
<Suspense fallback={<SkeletonSerachParams />}>
<BannerCategory />
<NavLinkCategory />
</Suspense>
<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">
{CoursesCard.map((course) => (
{/* <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">
{!category &&
filterCoursesAudience &&
filterCoursesAudience.map((course) => (
<div className="flex justify-center" key={course.id}>
<Card.Root link={`curso/${course.id}`}>
<Card.Image image={course.image.src} width={273} height={365}>
<Card.Title title={course.title} />
<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.hours}>
<Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} />
</Card.Content>
<Card.Content description={course.category}>
<Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content description={course.calender}>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</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 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 className="my-6">
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
</div> */}
<Differences />
<SignUp />
<About.Root>
......
'use client'
import { getCourses } from '@/api/courses'
import { About } from '@/components/about'
import { BannerCategory } from '@/components/banner-category'
import { Card } from '@/components/card'
import { Differences } from '@/components/differences'
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 { 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 { usePathname, useSearchParams } from 'next/navigation'
import { Suspense } from 'react'
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 (
<main>
<Suspense fallback={<SkeletonSerachParams />}>
<BannerCategory />
<NavLinkCategory />
</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">
{CoursesCard.map((course) => (
{!category &&
filterCoursesAudience &&
filterCoursesAudience.map((course) => (
<div className="flex justify-center" key={course.id}>
<Card.Root link={`curso/${course.id}`}>
<Card.Image image={course.image.src} width={273} height={365}>
<Card.Title title={course.title} />
<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.hours}>
<Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} />
</Card.Content>
<Card.Content description={course.category}>
<Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content description={course.calender}>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</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 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 className="my-6">
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
</div> */}
<Differences />
<SignUp />
<About.Root>
......
......@@ -5,6 +5,10 @@ import '@/styles/globals.css'
import { StyledEngineProvider } from '@mui/material'
import type { Metadata } from 'next'
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'] })
......@@ -21,6 +25,7 @@ export default function RootLayout({
return (
<html lang="pt-BR">
<body className={`antialiased ${poppins.className}`}>
<AuthProvider>
<ThemeProvider
attribute="class"
defaultTheme="dark"
......@@ -28,11 +33,15 @@ export default function RootLayout({
disableTransitionOnChange
>
<StyledEngineProvider injectFirst>
<ReactQueryProvider>
<Toaster richColors />
<Header />
{children}
<Footer />
</ReactQueryProvider>
</StyledEngineProvider>
</ThemeProvider>
</AuthProvider>
</body>
</html>
)
......
'use client'
import { InputMui } from '@/components/mui/inputs'
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 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'
type FormLogin = {
email: string
password: string
}
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 (
<main>
<section className=" container flex flex-col justify-center items-center space-y-10 h-[723px]">
......@@ -20,15 +48,27 @@ export default function Login() {
Acesso exclusivo para usuários cadastrados.
</h2>
<form
// onSubmit={handleSubmit(onSubmit)}
onSubmit={handleSubmit(onSubmit)}
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-4">
<InputMui label="E-mail" variant="outlined" type="email" />
<InputMui label="Senha" variant="outlined" type="password" />
<InputMui
label="E-mail"
variant="outlined"
type="email"
name="email"
register={register}
/>
<InputMui
label="Senha"
variant="outlined"
type="password"
name="password"
register={register}
/>
</div>
<div className="flex items-center space-x-2">
{/* <div className="flex items-center space-x-2">
<Checkbox id="remember" />
<label
htmlFor="remember"
......@@ -36,9 +76,10 @@ export default function Login() {
>
Lembrar-me
</label>
</div>
<Button className="rounded-sm" asChild>
<Link href="/admin">Entrar</Link>
</div> */}
<Button className="rounded-sm" type="submit">
{/* <Link href="/admin">Entrar</Link> */}
Entrar
</Button>
<Button
variant={'link'}
......
'use client'
import { getCourses } from '@/api/courses'
import { About } from '@/components/about'
import { BannerCategory } from '@/components/banner-category'
import { Card } from '@/components/card'
import { Differences } from '@/components/differences'
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 { 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 { usePathname, useSearchParams } from 'next/navigation'
import { Suspense } from 'react'
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 (
<main>
<Suspense fallback={<SkeletonSerachParams />}>
<BannerCategory />
<NavLinkCategory />
</Suspense>
<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">
{CoursesCard.map((course) => (
{/* <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">
{!category &&
filterCoursesAudience &&
filterCoursesAudience.map((course) => (
<div className="flex justify-center" key={course.id}>
<Card.Root link={`curso/${course.id}`}>
<Card.Image image={course.image.src} width={273} height={365}>
<Card.Title title={course.title} />
<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.hours}>
<Card.Content
description={`${course.workload} hrs / ${course.category?.name}`}
>
<Card.Icon icon={Clock4} />
</Card.Content>
<Card.Content description={course.category}>
<Card.Content description={course.audience?.name as string}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content description={course.calender}>
<Card.Content
description={formatDate(course.startDate) as string}
>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</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 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 className="my-6">
<PaginationComponent pageIndex={0} totalCount={105} perPage={10} />
</div> */}
<Differences />
<SignUp />
<About.Root>
......
......@@ -2,76 +2,49 @@
import Image from 'next/image'
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 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 bannerEducacaoMobile from '../../public/images/banners/educacao-mobile.jpg'
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'
import { getAreas } from '@/api/areas'
import { useQuery } from '@tanstack/react-query'
export function BannerCategory() {
export function BannerCategory(areaBanner: { data?: string | undefined }) {
const searchParams = useSearchParams()
const category = searchParams.get('categoria')
const category = searchParams.get('area')
const { data: areas } = useQuery({ queryKey: ['areas'], queryFn: getAreas })
function desktopBannerCategory() {
switch (category) {
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
}
}
const bannerImage = areas?.data.find(
(item) => item.name.toLowerCase() === category?.toLowerCase(),
)
function mobileBannerCategory() {
switch (category) {
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
}
}
const bannerImageArea = areas?.data.find(
(item) => item.name.toLowerCase() === areaBanner?.data?.toLowerCase(),
)
return (
<div className="h-[300px] flex items-center justify-end">
<Image
alt="banner"
className="hidden md:flex w-full h-full object-cover"
src={desktopBannerCategory()}
width={100}
height={100}
src={
bannerImage?.desktopBanner ||
bannerImageArea?.desktopBanner ||
bannerStudants
}
unoptimized
/>
<Image
alt="banner"
className="flex md:hidden w-full h-full object-cover"
src={mobileBannerCategory()}
width={100}
height={100}
src={
bannerImage?.mobileBanner ||
bannerImageArea?.mobileBanner ||
bannerStudants
}
unoptimized
/>
</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 = {
export function CardImage({ children, image, width, height }: CardImageProps) {
return (
<div className={`relative w-[${width}px] `}>
<div className={`relative w-[${width}px]`}>
<Image
src={image}
width={width}
......
'use client'
import Link from 'next/link'
import { ReactNode } from 'react'
......@@ -9,10 +11,12 @@ type CardRootProps = {
export function CardRoot({ children, link }: CardRootProps) {
return (
<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}
>
<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}
</div>
</Link>
......
'use client'
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 {
Carousel,
......@@ -11,32 +14,47 @@ import {
} from './ui/carousel'
export function CarouselComponent() {
const { data: courses } = useQuery({
queryKey: ['courses'],
queryFn: getCourses,
})
return (
<section>
<Carousel>
{courses ? (
<CarouselContent>
{CoursesCard.map((course) => (
{courses.data.slice(0, 10).map((course) => (
<CarouselItem
key={course.id}
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.Image image={course.image.src} width={240} height={320}>
<Card.Title title={course.title} />
<Card.Root link={`/curso/${course.id}`}>
<Card.Image
image={course.mobileBanner || ''}
width={240}
height={320}
>
<Card.Title title={course.name} />
</Card.Image>
<Card.Content description={course.hours}>
<Card.Content
description={`${String(course.workload)} horas`}
>
<Card.Icon icon={Clock4} />
</Card.Content>
<Card.Content description={course.category}>
<Card.Content description={course?.audience?.name || ''}>
<Card.Icon icon={User} />
</Card.Content>
<Card.Content description={course.calender}>
<Card.Content description={course?.category?.name || ''}>
<Card.Icon icon={Calendar} />
</Card.Content>
</Card.Root>
</CarouselItem>
))}
</CarouselContent>
) : (
<p>Não há cursos disponíveis</p>
)}
<CarouselPrevious />
<CarouselNext />
</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 (
<ul className="flex flex-col gap-8">
<li className="flex items-center gap-4">
......@@ -11,7 +26,9 @@ export function CourseDetails() {
</div>
<div className="flex flex-col gap-2">
<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>
</li>
<li className="flex items-center gap-4">
......@@ -22,7 +39,7 @@ export function CourseDetails() {
</div>
<div className="flex flex-col gap-2">
<span className="font-thin">Indicado para:</span>
<span className="text-xl">Estudantes</span>
<span className="text-xl">{course.data?.audience.name}</span>
</div>
</li>
<li className="flex items-center gap-4">
......@@ -33,10 +50,10 @@ export function CourseDetails() {
</div>
<div className="flex flex-col gap-2">
<span className="font-thin">Início:</span>
<span className="text-xl">Imediato</span>
<span className="text-xl">{formatDate(course.data?.startDate)}</span>
</div>
</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 flex items-center justify-center w-[57px] h-[57px] dark:bg-gray-900 bg-gray-50">
<DollarSign size={35} />
......@@ -46,7 +63,7 @@ export function CourseDetails() {
<span className="font-thin">Investimento:</span>
<span className="text-xl">R$200,00</span>
</div>
</li>
</li> */}
</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 @@
import { useThemeClient } from '@/hooks/useThemeClient'
import { TextField, TextFieldProps } from '@mui/material'
import { ReactNode } from 'react'
import { FieldValues, Path, UseFormRegister } from 'react-hook-form'
type InputMuiProps = TextFieldProps & {
type InputMuiProps<T extends FieldValues> = TextFieldProps & {
label: string
variant: 'standard' | 'filled' | 'outlined'
themeColor?: string
children?: ReactNode
name: Path<T>
register: UseFormRegister<T>
}
export function InputMui({
export const InputMui = <T extends FieldValues>({
label,
variant,
children,
...props
}: InputMuiProps) {
themeColor = '#fafafa',
register,
name,
...rest
}: InputMuiProps<T>) => {
const themeConfig = useThemeClient()
const color = themeConfig === 'dark' ? '#fafafa' : '#3C3C3C'
const color = themeConfig === 'dark' ? themeColor : '#3C3C3C'
return (
<TextField
label={label}
variant={variant}
{...props}
{...register(name)}
{...rest}
sx={{
'& .MuiOutlinedInput-root': {
'& fieldset': {
......@@ -44,10 +52,10 @@ export function InputMui({
color: `${color}`,
},
'& input': {
color: `${color}`,
color: `#fafafa`,
},
'& .MuiInputBase-input': {
color: `${color}`,
color: `#fafafa`,
},
'& .MuiInput-underline:before': {
borderBottomColor: color,
......
'use client'
import { getAreas } from '@/api/areas'
import { useQuery } from '@tanstack/react-query'
import { NavLinkSearchParams } from './nav-link-search-params'
export function NavLinkCategory() {
const { data: areas } = useQuery({ queryKey: ['areas'], queryFn: getAreas })
return (
<nav className="container my-10">
<ul className="flex flex-wrap justify-center items-center gap-8">
<li>
<NavLinkSearchParams
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>
{areas?.data.map((area) => (
<li key={area.id}>
<NavLinkSearchParams
variant="default"
href="teologia"
href={area.name.toLowerCase()}
className="uppercase"
>
Teologia
{area.name}
</NavLinkSearchParams>
</li>
))}
</ul>
</nav>
)
......
......@@ -2,7 +2,7 @@
import Link, { LinkProps } from 'next/link'
import { useSearchParams } from 'next/navigation'
import { usePathname, useSearchParams } from 'next/navigation'
import { Button } from './ui/button'
interface NavLinkProps extends LinkProps {
......@@ -23,16 +23,25 @@ interface NavLinkProps extends LinkProps {
export function NavLinkSearchParams(props: NavLinkProps) {
const searchParams = useSearchParams()
const category = searchParams.get('categoria')
const pathname = usePathname()
const path = pathname
const area = searchParams.get('area')
return (
<Button
data-current={category === props.href}
data-current={area === props.href}
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
>
<Link href={{ query: { categoria: props.href as string } }}>
<Link
href={{
pathname: `${path === '/' ? '/estudantes' : path}`,
query: { area: props.href as string },
}}
>
{props.children}
</Link>
</Button>
......
'use client'
import { Search } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { InputMui } from './mui/inputs'
import { Label } from './ui/label'
import {
......@@ -11,6 +14,8 @@ import {
} from './ui/select'
export default function SearchFilter() {
const { register } = useForm()
return (
<div className="container grid md:grid-cols-2 gap-8 items-end mb-8">
<div className="flex items-center max-w-[712px] flex-1">
......@@ -19,6 +24,8 @@ export default function SearchFilter() {
variant="standard"
type="text"
className="w-full #fafafa"
name="learned-today"
register={register}
/>
<Search size={24} className="-ml-6" />
</div>
......
......@@ -2,24 +2,69 @@
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { TextField } from '@mui/material'
import { zodResolver } from '@hookform/resolvers/zod'
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 { StyledInputs } from './mui/styled-inputs'
import { InputMui } from './mui/inputs'
type FormSignUpTypes = {
name: string
fullname: string
email: string
whatsapp: string
interest: string
area: 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() {
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 (
<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() {
onSubmit={handleSubmit(onSubmit)}
className="w-full md:w-[600px] flex flex-col gap-8 order-4"
>
<TextField
label="Nome"
variant="standard"
<InputMui
type="text"
sx={StyledInputs({ color: '#fafafa' })}
/>
<TextField
label="Whatsapp"
variant="standard"
type="number"
sx={StyledInputs({ color: '#fafafa' })}
label="Nome completo"
name="fullname"
register={register}
/>
<TextField
label="Área de interesse"
{errors.fullname?.message && (
<p className="text-red-600">{errors.fullname?.message}</p>
)}
<InputMui
type="number"
variant="standard"
type="text"
sx={StyledInputs({ color: '#fafafa' })}
label="Whatsapp"
name="whatsapp"
register={register}
/>
<TextField
label="E-mail"
variant="standard"
{errors.whatsapp?.message && (
<p className="text-red-600">{errors.whatsapp?.message}</p>
)}
<InputMui
type="text"
sx={StyledInputs({ color: '#fafafa' })}
variant="standard"
label="Área de interesse"
name="area"
register={register}
/>
<TextField
label="Curso desejado"
{errors.area?.message && (
<p className="text-red-600">{errors.area?.message}</p>
)}
<InputMui
type="text"
variant="standard"
label="E-mail"
name="email"
register={register}
/>
{errors.email?.message && (
<p className="text-red-600">{errors.email?.message}</p>
)}
<InputMui
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
type="submit"
variant="secondary"
className="uppercase max-w-32 mx-auto md:mb-6"
>
Inscrever
{status || 'Inscrever'}
</Button>
</form>
</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,
}
This diff is collapsed.
import { cva, VariantProps } from 'class-variance-authority'
import * as React from 'react'
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
extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {
label?: string
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
({ className, variant, type, label, ...props }, ref) => {
return (
<label>
<span className="text-sm text-gray-400">{label}</span>
<input
type={type}
className={cn(
'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,
)}
className={cn(inputVariants({ variant, className }))}
ref={ref}
{...props}
/>
</label>
)
},
)
Input.displayName = 'Input'
export { Input }
export { Input, inputVariants }
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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