diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 6a5805e..c0c89a2 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -7,6 +7,8 @@ import { PassThrough } from 'stream' import { getEnv, init } from './utils/env.server.ts' import { NonceProvider } from './utils/nonce-provider.ts' import { makeTimings } from './utils/timing.server.ts' +//import { CronJob } from 'cron' +//import { prisma } from './utils/db.server.ts' const ABORT_DELAY = 5000 @@ -17,6 +19,22 @@ if (ENV.MODE === 'production' && ENV.SENTRY_DSN) { import('~/utils/monitoring.server.ts').then(({ init }) => init()) } +/* new CronJob( + '* * * * *', // cronTime + async function () { + const emailsToSend = await prisma.emailSchedule.findMany() + console.log(`${new Date().toTimeString()} There are now ${emailsToSend.length} emails on the queue...`) + //await sendEmail({ + // to: user.email, + // subject: `André Casal Password Reset`, + // react: , + //}) + }, // onTick + null, // onComplete + true, // start + 'America/Los_Angeles', // timeZone +) */ + type DocRequestArgs = Parameters export default async function handleRequest(...args: DocRequestArgs) { diff --git a/app/routes/_pseo+/subject-data.ts b/app/routes/_marketing+/_pseo+/disciplinas.ts similarity index 56% rename from app/routes/_pseo+/subject-data.ts rename to app/routes/_marketing+/_pseo+/disciplinas.ts index 58401c0..47de3c1 100644 --- a/app/routes/_pseo+/subject-data.ts +++ b/app/routes/_marketing+/_pseo+/disciplinas.ts @@ -1,11 +1,7 @@ -import digitalSistemsFile from './files/sistemas-digitais.zip' -import arquiteturaDeComputadores from './files/arquitetura-de-computadores.zip' -import sistemasOperativos from './files/sistemas-operativos.zip' +import digitalSistemsFile from './materia+/files/sistemas-digitais.zip' +import arquiteturaDeComputadores from './materia+/files/arquitetura-de-computadores.zip' +import sistemasOperativos from './materia+/files/sistemas-operativos.zip' -type Resources = { - name: string - link: string -} type Software = { name: string link: string @@ -19,10 +15,10 @@ type List = { type Subject = { slug: string name: string - prerequisites: string[] - topics: List[] - software: Software[] - resources: Resources[] + prerequisites?: string[] + topics?: List[] + software?: Software[] + resources?: string } export const subjects: Subject[] = [ @@ -64,12 +60,7 @@ export const subjects: Subject[] = [ { name: 'VHDL' }, { name: 'ASICs' }, ], - resources: [ - { - name: 'Matéria de Sistemas Digitais', - link: digitalSistemsFile, - }, - ], + resources: digitalSistemsFile, software: [ { name: 'Logisim', @@ -116,12 +107,7 @@ export const subjects: Subject[] = [ { name: 'Multiprocessadores' }, { name: 'Paralelismo' }, ], - resources: [ - { - name: 'Organização para Arquitetura de Computadores', - link: arquiteturaDeComputadores, - }, - ], + resources: arquiteturaDeComputadores, software: [ { name: 'MARS', @@ -129,6 +115,10 @@ export const subjects: Subject[] = [ }, ], }, + { + slug: 'assembly', + name: 'Assembly', + }, { slug: 'sistemas-operativos', name: 'Sistemas Operativos', @@ -210,12 +200,7 @@ export const subjects: Subject[] = [ ], }, ], - resources: [ - { - name: 'Organização para Sistemas Operativos', - link: sistemasOperativos, - }, - ], + resources: sistemasOperativos, software: [ { name: 'Linux', @@ -235,4 +220,232 @@ export const subjects: Subject[] = [ }, ], }, + { + slug: 'introducao-a-programacao', + name: 'Introdução à Programação', + }, + { + slug: 'programacao', + name: 'Programação', + }, + { + slug: 'programacao-orientada-a-objetos', + name: 'Programação Orientada a Objetos', + }, + { + slug: 'programacao-funcional', + name: 'Programação Funcional', + }, + { + slug: 'algoritmos', + name: 'Algoritmos', + },{ + slug: 'c', + name: 'C', + }, + { + slug: 'cpp', + name: 'C++', + }, + { + slug: 'csharp', + name: 'C#', + }, + { + slug: 'java', + name: 'Java', + }, + { + slug: 'kotlin', + name: 'Kotlin', + }, + { + slug: 'rust', + name: 'Rust', + }, + { + slug: 'php', + name: 'PHP', + }, + { + slug: 'html', + name: 'HTML', + }, + { + slug: 'css', + name: 'CSS', + }, + { + slug: 'javascript', + name: 'JavaScript', + }, + { + slug: 'typescript', + name: 'TypeScript', + }, + { + slug: 'python', + name: 'Python', + }, + { + slug: 'spring-boot', + name: 'Spring Boot', + }, + { + slug: 'react', + name: 'React', + }, + { + slug: 'jpa', + name: 'JPA', + }, + { + slug: 'python', + name: 'Python', + }, + { + slug: 'estruturas-de-dados', + name: 'Estuturas de Dados', + }, + { + slug: 'bases-de-dados', + name: 'Bases de Dados', + prerequisites: [ + 'Aritmética', + 'Álgebra', + 'Sistemas numéricos', + 'Sistema binário', + 'Sistema octal', + 'Sistema hexadecimal', + 'Expressões lógicas', + 'Tipos de dados', + ], + topics: [ + { + name: 'Introdução às Bases de Dados', + children: [ + { name: 'Definição e função de uma base de dados' }, + { name: 'Modelos de dados (relacional, hierárquico, em rede, orientado a objetos, etc.)' }, + { name: 'Sistemas de gestão de bases de dados (SGBD)' }, + { name: 'Normalização de bases de dados' }, + ], + }, + { + name: 'Modelo Relacional', + children: [ + { name: 'Conceitos básicos de tabelas, tuplos, atributos, chaves, etc.' }, + { name: 'Álgebra relacional' }, + { name: 'Cálculo relacional' }, + { name: 'Integridade referencial' }, + { name: 'Normalização de bases de dados' }, + ], + }, + { + name: 'SQL', + children: [ + { name: 'Linguagem de definição de dados (DDL)' }, + { name: 'Linguagem de manipulação de dados (DML)' }, + { name: 'Linguagem de controlo de dados (DCL)' }, + { name: 'Linguagem de transações (DCL)' }, + { name: 'Funções e procedimentos' }, + ], + }, + { + name: 'Modelo de Entidade-Relação (ER)', + children: [ + { name: 'Conceitos básicos de entidades, relações, atributos, chaves, etc.' }, + { name: 'Modelo de dados ER' }, + { name: 'Modelo de dados extendido ER' }, + { name: 'Normalização de bases de dados' }, + ], + }, + { + name: 'Bases de Dados Distribuídas', + children: [ + { name: 'Conceitos básicos de bases de dados distribuídas' }, + { name: 'Arquiteturas de bases de dados distribuídas' }, + { name: 'Transações distribuídas' }, + { name: 'Recuperação de falhas em bases de dados distribuídas' }, + ], + }, + { + name: 'Bases de Dados NoSQL', + children: [ + { name: 'Conceitos básicos de bases de dados NoSQL' }, + { name: 'Tipos de bases de dados NoSQL (documentais, chave-valor, colunares, etc.)' }, + { name: 'Modelos de dados NoSQL' }, + { name: 'Operações básicas em bases de dados NoSQL' }, + ], + }, + { + name: 'Bases de Dados Temporais', + children: [ + { name: 'Conceitos básicos de bases de dados temporais' }, + { name: 'Modelos de dados temporais' }, + { name: 'Operações básicas em bases de dados temporais' }, + { name: 'Recuperação de informação temporal' }, + ], + }, + { + name: 'Bases de Dados Espaciais', + children: [ + { name: 'Conceitos básicos de bases de dados espaciais' }, + { name: 'Modelos de dados espaciais' }, + { name: 'Operações básicas em bases de dados espaciais' }, + { name: 'Recuperação de informação espacial' }, + ], + }, + ], + resources: sistemasOperativos, + software: [ + { + name: 'SQLite', + link: 'https://sqlite.org/', + }, + { + name: 'MySQL', + link: 'https://www.mysql.com/', + }, + { + name: 'PostgreSQL', + link: 'https://www.postgresql.org/', + }, + { + name: 'MongoDB', + link: 'https://www.mongodb.com/', + }, + { + name: 'Cassandra', + link: 'http://cassandra.apache.org/', + }, + ], + }, + { + slug: 'sql', + name: 'SQL', + }, + { + slug: 'redes-de-computadores', + name: 'Redes de Computadores', + }, + { + slug: 'algebra-linear', + name: 'Álgebra Linear', + }, + { + slug: 'calculo', + name: 'Cálculo', + }, + { + slug: 'excel', + name: 'Excel', + }, + { + slug: 'microsoft-office', + name: 'Microsoft Office', + }, + { + slug: 'word', + name: 'Word', + }, ] diff --git a/app/routes/_pseo+/explicacoes.$subjectslug.tsx b/app/routes/_marketing+/_pseo+/explicacoes+/$subjectslug.tsx similarity index 92% rename from app/routes/_pseo+/explicacoes.$subjectslug.tsx rename to app/routes/_marketing+/_pseo+/explicacoes+/$subjectslug.tsx index c7aa4ae..351c277 100644 --- a/app/routes/_pseo+/explicacoes.$subjectslug.tsx +++ b/app/routes/_marketing+/_pseo+/explicacoes+/$subjectslug.tsx @@ -23,7 +23,7 @@ import satisfactionGuarantee from '~/routes/_marketing+/images/satisfaction-guar import signatureBlack from '~/routes/_marketing+/images/signature-black.png' import signatureWhite from '~/routes/_marketing+/images/signature-white.png' import { Link, useLoaderData } from '@remix-run/react' -import { type LoaderArgs, json, type LinksFunction, type V2_MetaFunction } from '@remix-run/node' +import { type LoaderArgs, json, type LinksFunction, type V2_MetaFunction, type ActionArgs } from '@remix-run/node' import { Container } from '~/ui_components/layout/container.tsx' import { H1 } from '~/ui_components/typography/h1.tsx' import { P } from '~/ui_components/typography/p.tsx' @@ -31,10 +31,20 @@ import { H2 } from '~/ui_components/typography/h2.tsx' import { H3 } from '~/ui_components/typography/h3.tsx' import { Span } from '~/ui_components/typography/span.tsx' import { H4 } from '~/ui_components/typography/h4.tsx' -import BackgroundDiagonal from '../_marketing+/components/bg-diagonal.tsx' -import BackgroundBlur from '../_marketing+/components/bg-blur.tsx' -import BackgroundSquareLines from '../_marketing+/components/bg-square-lines.tsx' -import { subjects } from './subject-data.ts' +import BackgroundDiagonal from '../../components/bg-diagonal.tsx' +import BackgroundBlur from '../../components/bg-blur.tsx' +import BackgroundSquareLines from '../../components/bg-square-lines.tsx' +import { subjects } from '../disciplinas.ts' +import { validateCSRF } from '~/utils/csrf.server.ts' +import { checkHoneypot } from '~/utils/honeypot.server.ts' +import { parse } from '@conform-to/zod' +import { emailSchema } from '~/utils/user-validation.ts' +import { z } from 'zod' +import { prisma } from '~/utils/db.server.ts' +import { sendEmail } from '~/utils/email.server.ts' +import { ResourcesEmail } from '../materia+/tutoring.emails.tsx' +import { getDomainUrl } from '~/utils/misc.ts' +import { generateTOTP } from '~/utils/totp.server.ts' export const meta: V2_MetaFunction = ({ params }) => { const { subjectslug } = params @@ -110,7 +120,7 @@ export async function loader({ params }: LoaderArgs) { const Route = () => { const { subject } = useLoaderData() - const { name, prerequisites, topics, resources, software } = subject + const { name } = subject const features = [ { name: 'Aulas online por video-conferência', @@ -407,7 +417,7 @@ const Route = () => { return classes.filter(Boolean).join(' ') } - const navigation = { + const subjectsITeach = { programming: [ { name: 'C, C++, C#, Java, PHP, Python' }, { name: 'Kotlin (Android)' }, @@ -549,7 +559,7 @@ const Route = () => {
  • Passei!
  • - Como é que isto é possível? Porque tiveste aulas de programação comigo! Eu ensinei como estudar {name} e aprendeste um método cientificamente + Como é que isto é possível? Porque tiveste aulas de {name} comigo! Eu ensinei como estudar {name} e aprendeste um método cientificamente provado para memorizar tudo o que for importante. Para além disso, vou ao encontro do teu nível de conhecimento atual e ajudo-te a fazer a ponte entre o conhecimento que tens agora e o conhecimento que precisarás para passar no exames. Podes ter explicações dedicadas só a ti ou em grupo.

    @@ -597,86 +607,6 @@ const Route = () => { - - -
    -
    -

    - {name} -

    -

    - Estas são as matérias que precisas de saber para tirares o máximo proveito das explicações de {name}. -

    -
    -
    -
    -
    -
    -
    -
    -

    Pré-requisitos

    -
      - {prerequisites.map(name => ( -
    • - {name} -
    • - ))} -
    -
    -
    -

    Tópicos

    -
      - {topics.map(({ name, children }) => ( -
    • - {name} - {children ? ( -
        - {children.map(({ name }) => ( -
      • - {name} -
      • - ))} -
      - ) : null} -
    • - ))} -
    -
    -
    -
    -
    -

    Recursos

    -
      - {resources.map(({ name, link }) => ( -
    • - - - {name} - - -
    • - ))} -
    -
    -
    -

    Software

    -
      - {software.map(({ name, link }) => ( -
    • - - - {name} - - -
    • - ))} -
    -
    -
    -
    -
    -
    -
    @@ -735,7 +665,7 @@ const Route = () => {

    Programação

      - {navigation.programming.map(item => ( + {subjectsITeach.programming.map(item => (
    • {item.name}
    • @@ -745,7 +675,7 @@ const Route = () => {

      Desenvolvimento Web

        - {navigation.webdev.map(item => ( + {subjectsITeach.webdev.map(item => (
      • {item.name}
      • @@ -757,7 +687,7 @@ const Route = () => {

        Ciência da Computação

          - {navigation.computerscience.map(item => ( + {subjectsITeach.computerscience.map(item => (
        • {item.name}
        • @@ -767,7 +697,7 @@ const Route = () => {

          Matemática

            - {navigation.math.map(item => ( + {subjectsITeach.math.map(item => (
          • {item.name}
          • @@ -1118,7 +1048,7 @@ const Route = () => {

            Só quero agradecer-te por reservares um tempo para leres sobre meu serviço de tutoria. Continua a ser uma tremenda honra ter tantos alunos que confiam em mim para - ajudá-los a encontrar uma maneira melhor de frequentar a faculdade. Sinceramente espero que tenhas decidido ter tutoria de programação, mesmo que não comigo, + ajudá-los a encontrar uma maneira melhor de frequentar a faculdade. Sinceramente espero que tenhas decidido ter tutoria de {name}, mesmo que não comigo, porque sei que é uma decisão muito boa.

            @@ -1135,3 +1065,97 @@ const Route = () => { ) } export default Route + +const newsletterSchema = z.object({ email: emailSchema }) +export const verificationType = `resources` + +export async function action({ request, params }: ActionArgs) { + const { subjectslug } = params + const subject = subjects.find(s => s.slug === subjectslug)! + const { name } = subject + const formData = await request.formData() + + // Check for bots + await validateCSRF(formData, request.headers) + checkHoneypot(formData, '/newsletter') + + // Parse form + const submission = parse(formData, { schema: newsletterSchema }) + if (submission.intent !== 'submit') { + return json({ status: 'error', submission } as const) + } + if (!submission.value) { + return json({ status: 'error', submission } as const, { status: 400 }) + } + + // Extract data + const { email } = submission.value + + const resourcesURL = new URL(`${getDomainUrl(request)}/explicacoes/${subjectslug}/recursos`) + resourcesURL.searchParams.set('email', email) + + const oneDay = 60 * 60 * 24 // One day in seconds + const target = email + const { otp, secret, algorithm, period, digits } = generateTOTP({ algorithm: 'sha256', period: oneDay }) + // delete old verifications. Users should not have more than one verification + // of a specific type for a specific target at a time. + await prisma.verification.deleteMany({ + where: { type: verificationType, target }, + }) + await prisma.verification.create({ + data: { + type: verificationType, + target, + algorithm, + secret, + period, + digits, + expiresAt: new Date(Date.now() + period * 1000), + }, + }) + + // add the otp to the url we'll email the user. + resourcesURL.searchParams.set('code', otp) + + // Schedule emails + await scheduleEmailSequence(email) + + // Send email with resource link + await sendEmail({ + to: email, + subject: `🚀 Recursos para estudantes de ${name}`, + react: , + }) + + // Everything ok + return json({ status: 'success', submission } as const) +} + +async function scheduleEmailSequence(email: string) { + const now = new Date() + const in1Day = new Date(now.getTime() + 1 * 24 * 3600 * 1000) + const in2Days = new Date(now.getTime() + 2 * 24 * 3600 * 1000) + const in3Days = new Date(now.getTime() + 3 * 24 * 3600 * 1000) + const in4Days = new Date(now.getTime() + 4 * 24 * 3600 * 1000) + const scheduleDates = [in1Day, in2Days, in3Days, in4Days] + for (let i = 0; i < scheduleDates.length; i++) { + const scheduledAt = scheduleDates[i] + const sequence = i + 1 + await prisma.emailSchedule.upsert({ + where: { + to_sequence: { + to: email, + sequence, + }, + }, + update: { + scheduledAt: scheduledAt, + }, + create: { + to: email, + sequence, + scheduledAt: scheduledAt, + }, + }) + } +} diff --git a/app/routes/_marketing+/_pseo+/materia+/$subjectslug.tsx b/app/routes/_marketing+/_pseo+/materia+/$subjectslug.tsx new file mode 100644 index 0000000..910631c --- /dev/null +++ b/app/routes/_marketing+/_pseo+/materia+/$subjectslug.tsx @@ -0,0 +1,1324 @@ +import { Button } from '~/components/ui/button.tsx' +import { Icon } from '~/components/ui/icon.tsx' +import collegeLife from '~/routes/_marketing+/images/college-life.jpg' +import andreOnMacBookPro from '~/routes/_marketing+/images/andre-on-macbook-pro.png' +import goncaloBarreiros from '~/routes/_marketing+/images/goncalo-barreiros.png' +import pauloJorge from '~/routes/_marketing+/images/paulo-jorge.png' +import luciaZiyuan from '~/routes/_marketing+/images/lucia-ziyuan.png' +import catiaSilva from '~/routes/_marketing+/images/catia-silva.png' +import marcoBarreiros from '~/routes/_marketing+/images/marco-barreiros.png' +import dejanMilosevic from '~/routes/_marketing+/images/dejan-milosevic.png' +import yev from '~/routes/_marketing+/images/yev.png' +import alexandreMiguelPinto from '~/routes/_marketing+/images/alexandre-miguel-pinto.png' +import lilianaFerreira from '~/routes/_marketing+/images/liliana-ferreira.png' +import paulaAlexandraSilva from '~/routes/_marketing+/images/paula-alexandra-silva.png' +import joseGuimaraes from '~/routes/_marketing+/images/jose-guimaraes.png' +import kirillLapshev from '~/routes/_marketing+/images/kirill-lapshev.png' +import fabianaMilanez from '~/routes/_marketing+/images/fabiana-milanez.png' +import apoZadeh from '~/routes/_marketing+/images/apo-zadeh.png' +import inesBarreiros from '~/routes/_marketing+/images/ines-barreiros.png' +import leahMeirinhos from '~/routes/_marketing+/images/leah-meirinhos.png' +import miguelFerreira from '~/routes/_marketing+/images/miguel-ferreira.png' +import satisfactionGuarantee from '~/routes/_marketing+/images/satisfaction-guarantee.png' +import signatureBlack from '~/routes/_marketing+/images/signature-black.png' +import signatureWhite from '~/routes/_marketing+/images/signature-white.png' +import * as newsletterAnimation from '~/components/newsletter-animation.json' +import { Form, Link, useActionData, useLoaderData, useNavigation } from '@remix-run/react' +import { type LoaderArgs, json, type LinksFunction, type V2_MetaFunction, type ActionArgs } from '@remix-run/node' +import { Container } from '~/ui_components/layout/container.tsx' +import { H1 } from '~/ui_components/typography/h1.tsx' +import { P } from '~/ui_components/typography/p.tsx' +import { H2 } from '~/ui_components/typography/h2.tsx' +import { H3 } from '~/ui_components/typography/h3.tsx' +import { Span } from '~/ui_components/typography/span.tsx' +import { H4 } from '~/ui_components/typography/h4.tsx' +import BackgroundDiagonal from '../../components/bg-diagonal.tsx' +import BackgroundBlur from '../../components/bg-blur.tsx' +import BackgroundSquareLines from '../../components/bg-square-lines.tsx' +import { subjects } from '../disciplinas.ts' +import { validateCSRF } from '~/utils/csrf.server.ts' +import { checkHoneypot } from '~/utils/honeypot.server.ts' +import { parse } from '@conform-to/zod' +import { emailSchema } from '~/utils/user-validation.ts' +import { z } from 'zod' +import { useForm } from '@conform-to/react' +import { useEffect, useRef, useState } from 'react' +import { Player } from '@lottiefiles/react-lottie-player' +import { AuthenticityTokenInput } from 'remix-utils/csrf/react' +import { HoneypotInputs } from 'remix-utils/honeypot/react' +import { Flex } from '~/ui_components/layout/flex.tsx' +import { VisuallyHidden } from '@radix-ui/react-visually-hidden' +import { Label } from '~/components/ui/label.tsx' +import { Input } from '~/components/ui/input.tsx' +import { prisma } from '~/utils/db.server.ts' +import { sendEmail } from '~/utils/email.server.ts' +import { ResourcesEmail } from '../materia+/tutoring.emails.tsx' +import { getDomainUrl } from '~/utils/misc.ts' +import { generateTOTP } from '~/utils/totp.server.ts' + +export const meta: V2_MetaFunction = ({ params }) => { + const { subjectslug } = params + const subject = subjects.find(s => s.slug === subjectslug)! + const { name } = subject + return [ + { title: `Explicações de ${name}` }, + { + name: 'description', + content: `Procuras explicações de ${name} e queres resultados? Ajudei 650+ alunos a obliterar os exames! Satisfação 100% garantida!`, + }, + { + name: 'keywords', + content: `explicações de ${name}, explicador de ${name}, tutoria de ${name}, tutor de ${name}, professor particular de ${name}, boas notas ${name}, como estudar ${name}`, + }, + ] +} + +export const links: LinksFunction = () => { + return [ + { + rel: 'preload', + href: collegeLife, + as: 'image', + }, + { + rel: 'preload', + href: andreOnMacBookPro, + as: 'image', + }, + { + rel: 'preload', + href: goncaloBarreiros, + as: 'image', + }, + { + rel: 'preload', + href: pauloJorge, + as: 'image', + }, + { + rel: 'preload', + href: miguelFerreira, + as: 'image', + }, + { + rel: 'preload', + href: satisfactionGuarantee, + as: 'image', + }, + { + rel: 'preload', + href: signatureBlack, + as: 'image', + }, + { + rel: 'preload', + href: signatureWhite, + as: 'image', + }, + /* { + rel: 'canonical', + href: 'https://andrecasal.com/explicacoes-de-programacao', + }, */ + ] +} + +export async function loader({ params }: LoaderArgs) { + const { subjectslug } = params + const subject = subjects.find(s => s.slug === subjectslug)! + return json({ subject }) +} + +const Route = () => { + const { subject } = useLoaderData() + const { name, prerequisites, topics, software } = subject + const features = [ + { + name: 'Aulas online por video-conferência', + description: 'Explicações com áudio, vídeo, whiteboard e controlo remoto para te ajudar a organizar a matéria, programar, e explicar conceitos da melhor forma possível.', + icon: 'video', + }, + { + name: 'Gravações privadas gratuitas', + description: 'Acesso vitalício a uma playlist privada com todas as nossas sessões, para puderes rever a matéria sempre que precisares.', + icon: 'play', + }, + { + name: 'Acesso ilimitado a mim', + description: 'Falar comigo está a um clique de distância por WhatsApp. Estás bloqueado? Envia-me uma mensagem e eu respondo-te.', + icon: 'chat', + }, + { + name: 'Compromisso ético', + description: 'Eu comprometo-me a não fazer batota. Ajudo-te a entender a matéria e a tirar dúvidas, mas não farei o teu trabalho por ti.', + icon: 'heart', + }, + ] + + const featuredTestimonial = { + body: 'Antes de começar a ser orientado pelo André, eu odiava Assembly e Microprocessadores. Eu estava a repetir a disciplina pela terceira vez e senti que ia reprovar mais uma vez. Mas depois que comecei a ser orientado pelo André, comecei a gostar do que estava a aprender, porque comecei a entender a matéria e até ficou divertido! Pela primeira vez, senti-me confiante! O André é muito simpático e paciente: ele desenhava diagramas, explicava, e usava outras linguagens de programação para me ajudar a ver a ligação entre linguagens de alto nível e Assembly, e tudo estava ótimo! Nunca tive um professor assim. Quando o resultado do exame chegou, fiquei perplexo ao ver que havia ido de reprovado nessa matéria por dois anos consecutivos para ter um 16! O método de ensino e a dedicação do André são, na minha opinião, imbatíveis.', + author: { + name: 'Gonçalo Barreiros', + imageUrl: goncaloBarreiros, + }, + } + + const testimonials = [ + [ + [ + { + body: 'Os meus professores introduziram a programação do Arduino com pequenos blocos de código. Um pouco de código aqui e ali. Mas eles nunca explicaram as coisas muito bem. Eles presumiam que íamos procurar as peças que faltavam. Tentei pesquisar na internet e conversar com colegas, mas não consegui encontrar bons recursos na internet e meus amigos estavam tão perdidos quanto eu. Quando comecei as aulas de reforço com o André, minhas notas aumentaram exponencialmente. Estou com o André há um ano neste momento, a disciplina atual é a segunda que estamos a fazer juntos. Na primeira aula, se eu sei alguma coisa sobre Arduino, foi porque o André me ensinou. Quero dizer, o André não apenas me ensinou como a fazer projetos com Arduino, mas também me ensinou ao ponto de eu poder concluir os exames sozinho! E em termos de disponibilidade o André é top.', + author: { + name: 'Paulo Jorge', + imageUrl: pauloJorge, + }, + }, + { + body: 'O André é um professor muito amável que coloca o progresso dos seus alunos no centro da sua prática de ensino. Ele preocupa-se pessoalmente e adapta o seu ensino aos interesses e necessidades individuais dos alunos. Ele traz muitos anos de experiência do mundo real na construção de aplicações e negócios de sucesso para o seu ensino. Para além de ser um professor de programação muito conhecedor, também é apaixonado pela aprendizagem - crescimento pessoal, filosofia, saúde e afins. Aprenderá muito com ele para além da programação em si.', + author: { + name: 'Lucia Ziyuan', + imageUrl: luciaZiyuan, + }, + }, + { + body: 'Eu não conseguia entender a matéria da aula que estava a ser dada pelos professores. Eles não estavam a fazer um bom trabalho e não nos davam os recursos necessários. Tentei ir à biblioteca da universidade, tentei conversar com colegas que entendiam um pouco mais da aula mas não adiantou muito. A diferença [entre ter e não ter explicações com o André] é que eu não passaria nas provas. Tenho obtido resultados. Tem sido uma boa experiência, tenho aprendido e melhorado.', + author: { + name: 'Miguel Ferreira', + imageUrl: miguelFerreira, + }, + }, + { + body: 'É maravilhoso trabalhar com André! Ele é experiente e compassivo, e tem ideias inovadoras, paixão e amor por seu trabalho. Altamente recomendado!', + author: { + name: 'Amalia Sirica', + }, + }, + { + body: 'Já conheço o André há algum tempo. Ele é um ótimo programador e se mantém atualizado com as melhores práticas do setor. Altamente recomendado', + author: { + name: 'Zuki G', + }, + }, + { + body: 'Ajudou muito, foi muito paciente e interessado em ensinar e explicar o assunto.', + author: { + name: 'Isabel Bozzato', + }, + }, + { + body: 'Excelente explicador!', + author: { + name: 'José Maria', + }, + }, + { author: { name: 'Lecticia Benchimol' } }, + { author: { name: 'Marisa Oliveira' } }, + ], + [ + { + body: 'André é um tutor e mentor de programação absolutamente estelar. Sempre disponível para fornecer feedback pessoal, conselhos e orientação.', + author: { + name: 'Paula Alexandra Silva', + imageUrl: paulaAlexandraSilva, + }, + }, + { + body: 'André é um excelente tutor, todos os conceitos ficam realmente compreensíveis com suas explicações. Altamente recomendado! 💪', + author: { + name: 'Alexandre Miguel Pinto', + imageUrl: alexandreMiguelPinto, + }, + }, + { + body: 'O André é excelente mesmo!', + author: { + name: 'Inês V. Barreiros', + imageUrl: inesBarreiros, + }, + }, + { + body: 'Incrível!!!!', + author: { + name: 'Léa Meirinhos', + imageUrl: leahMeirinhos, + }, + }, + { + body: 'O professor André explica muito bem e tem imensa paciência. Eu estou extremamente grata!!', + author: { + name: 'Daniela Alexandra', + }, + }, + { + body: 'O André é um profissional que domina o que faz. Nota-se a sua paixão e entrega, mas sobretudo valorizo a sua disponibilidade em validar e perceber as necessidades do meu projeto; sempre com uma visão construtiva e com soluções interessantes que se tornaram numa mais-valia e algo diferenciador no mercado.', + author: { + name: 'Ana Mendes', + }, + }, + { + body: 'Olá André! Espero que esteja tudo bem! Enquanto isso, as notas foram publicadas [...] A de programação foi muito melhor do que eu esperava! [Captura de tela com nota de 17] A programação acabou por ser a melhor ahah!', + author: { + name: 'Guilherme Echeverri', + }, + }, + { + body: 'É excelente ter explicações com o André. Entendo tudo!', + author: { + name: 'Maria Isabel', + }, + }, + { + body: 'Ótimo explicador. Muito dedicado e paciente.', + author: { + name: 'Musslima Ibraimo', + }, + }, + { + body: 'Muito bom!', + author: { + name: 'Wilson Mesquita', + }, + }, + { author: { name: 'Kátia Santos' } }, + { author: { name: 'Gleice Santos' } }, + ], + ], + [ + [ + { + body: 'André sempre foi muito profissional, motivado e apaixonado pelo que faz. E quando está a ensinar, é a pessoa mais dedicada que conheço. Ele traz à tona o melhor que há nas pessoas, em qualquer circunstância. Sempre com um sorriso e uma atitude positiva, é entusiasmante trabalhar com ele!', + author: { + name: 'Dejan Milosevic', + imageUrl: dejanMilosevic, + }, + }, + { + body: 'Tried many courses online with no tangible progress, glad I found Andre, where one size fits all mentality is avoided.', + author: { + name: 'Apo Zadeh', + imageUrl: apoZadeh, + }, + }, + { + body: 'Um dos professores e mentores mais incríveis que conheci! Gentil e gentil, ele é um ótimo ouvinte e uma das pessoas mais calorosas que conheço. É um prazer enorme ser seu aluno 🫶', + author: { + name: 'Yev', + imageUrl: yev, + }, + }, + { + body: 'Fiquei com a impressão de que 2h era pouco tempo, e que o Prof. André poderia ter-nos ajudado mais, não fossem os exercícios da minha faculdade serem supostamente pouco convencionais.', + author: { + name: 'Henrique Silvestre', + }, + }, + { + body: 'É perfeito. Recomendo 100%. Não poderia ter encontrado melhor. Muito grato por tudo.', + author: { + name: 'Isabel Rodrigues', + }, + }, + { + body: 'Profissional: competência e disponibilidade.', + author: { + name: 'Jaime Torrinhas', + }, + }, + { + body: 'Um excelente explicador! Sabe como orientar passo-a-passo para resolver problemas. Experiência muito boa em geral.', + author: { + name: 'Miguel Lomba', + }, + }, + { author: { name: 'João Pedro Araújo' } }, + { author: { name: 'Inês Gouveia' } }, + { author: { name: 'Lúcia Rocha' } }, + { author: { name: 'João Telmo' } }, + { author: { name: 'João Pimentel' } }, + ], + [ + { + body: 'O André é um profissional inteligente super acessível e tem o dom de conseguir tornar simples o que à maioria parece complexo. :) Super Recomendo', + author: { + name: 'Liliana Ferreira', + imageUrl: lilianaFerreira, + }, + }, + { + body: 'O André é um professor dedicado e muito comprometido. Tem uma capacidade de leitura e compreensão das necessidades que superou sempre as minhas expectativas. Tem interesse em ajudar sempre mais! É um conhecedor, gosta de aprender e ler sobre tudo, por isso quem o procurar vai ter explicações nao só de programação mas de outras áreas que se cruzem ou sejam necessárias. Recomendo muito, garantidamente é um contacto enriquecedor!', + author: { + name: 'Cátia Silva', + imageUrl: catiaSilva, + }, + }, + { + body: 'O André é um excelente professor! Sua paciência e clareza ao explicar os conceitos são notáveis. Sinto-me muito grato por suas aulas extras e definitivamente voltarei a procurá-lo se precisar de ajuda novamente. Recomendo totalmente!!!! Muito obrigado, André!', + author: { + name: 'Marco Barreiros', + imageUrl: marcoBarreiros, + }, + }, + { + body: 'Excelente profissional! Deu-me uma grande ajuda a perceber melhor programação!', + author: { + name: 'Kirill Lapshev', + imageUrl: kirillLapshev, + }, + }, + { + body: 'André tem muita paciência, comunicação clara e didática.', + author: { + name: 'Fabiana Milanez', + imageUrl: fabianaMilanez, + }, + }, + { + body: 'O professor André Casal é um excelente explicador, muito dedicado e atento as necessidades dos alunos. Recomendo vivamente!', + author: { + name: 'José Guimarães', + imageUrl: joseGuimaraes, + }, + }, + { + body: 'Muito bom professor. Ajudou-me imenso em introdução à programação em Java. Recomendo a todos os que não entendam bem as aulas na faculdade pois o professor André dá explicações bastante detalhadas e esclarecedoras até entenderem tudo.', + author: { + name: 'Alfredo Soudo', + }, + }, + { + body: 'André Casal é o explicador mais inteligente, trabalhador e atencioso que conheço. Se deseja receber ajuda do mais alto nível, trabalhe com ele!', + author: { + name: 'Mony Chhim', + }, + }, + { + body: 'Estou muito grata por toda a ajuda: conhecimento, disponibilidade, paciência e, acima de tudo, pela gentileza. Obrigado!', + author: { + name: 'Maria Ribeiro', + }, + }, + { + body: 'O André foi muito paciente comigo e explicou tudo muito bem. Se eu precisar de ajuda novamente... é com ele.', + author: { + name: 'Marcos Marcos', + }, + }, + { + body: 'Além das expectativas!', + author: { + name: 'Helena Oliveira', + }, + }, + { author: { name: 'Csongor Csaba Horvath' } }, + { author: { name: 'Ricardo Escudeiro' } }, + { author: { name: 'Peter Delle' } }, + { author: { name: 'Arthur Constantino' } }, + ], + ], + ] + + const stats = [ + { name: 'Número de estudantes', value: '650+' }, + { name: 'Matérias ensinadas', value: '51+' }, + { name: 'Nota média', value: '16.4' }, + { name: 'Taxa de sucesso', value: '99.5%' }, + ] + + function classNames(...classes: string[]) { + return classes.filter(Boolean).join(' ') + } + + const subjectsITeach = { + programming: [ + { name: 'C, C++, C#, Java, PHP, Python' }, + { name: 'Kotlin (Android)' }, + { name: 'Swift (iOS)' }, + { name: 'Clean Code' }, + { name: 'Programação Funcional' }, + { name: 'Programação Orientada a Objetos' }, + { name: 'Programação Imperativa e Declarativa' }, + { name: 'Test-Driven Development (TDD)' }, + ], + webdev: [ + { name: 'HTML, CSS, JS' }, + { name: 'O protocolo HTTP' }, + { name: 'React, Vue, Angular' }, + { name: 'Styled Components' }, + { name: 'TailwindCSS' }, + { name: 'Browser, Node, Deno APIs' }, + { name: 'Express, Next, Remix' }, + { name: 'REST and GraphQL APIs' }, + { name: 'Git & GitHub' }, + { name: 'Databases (SQL, SQLite, Mongo)' }, + ], + computerscience: [ + { name: 'Sistemas Digitais' }, + { name: 'Organização e Arquitetura de Computadores' }, + { name: 'Assembly (x86, x86-64, ARM, MIPS)' }, + { name: 'Sistemas Operativos' }, + { name: 'Redes de Computadores' }, + { name: 'Estruturas de dados' }, + { name: 'Algoritmos' }, + ], + math: [{ name: 'Álgebra' }, { name: 'Analise matemática' }, { name: 'Cálculo' }, { name: 'Análise numérica' }], + } + + const includedFeatures = ['Estudar a par', 'Esclarecer dúvidas', 'Resolver desafios difíceis', 'Ajuda com projetos'] + + const singleSessionFeatures = ['Estudar a par', 'Esclarecer dúvidas', 'Resolver desafios difíceis', 'Ajuda com projetos'] + + const actionData = useActionData() + const navigation = useNavigation() + const [form, { email }] = useForm({ + lastSubmission: actionData?.submission, + shouldValidate: 'onBlur', + onValidate: ({ formData }) => parse(formData, { schema: newsletterSchema }), + }) + const playerRef = useRef(null) + const [state, setState] = useState<'initial' | 'animating' | 'finished'>('initial') + + useEffect(() => { + if (actionData?.status === 'success') { + setState('animating') + playerRef.current?.play() + } + }, [actionData]) + + return ( + <> + +

            +
            +
            +

            + Matéria de {name} +

            +

            + Queres resultados a {name}? Ajudei 650+ alunos a obliterar os exames! Satisfação 100% garantida! +

            +
            + + Quero ter excelentes notas! + +
            +
            +
            +
            +
            + André, o teu tutor de programação +
            André, o teu professor particular de {name}.
            +
            +
            +
            + + + +
            +
            +

            + {name} +

            +

            + Estas são as matérias que precisas de saber para tirares o máximo proveito das explicações de {name}. +

            +
            +
            +
            +
            +
            +
            +

            Pré-requisitos

            +
              + {prerequisites.map(name => ( +
            • + {name} +
            • + ))} +
            +
            +
            +

            Tópicos

            +
              + {topics.map(({ name, children }) => ( +
            • + {name} + {children ? ( +
                + {children.map(({ name }) => ( +
              • + {name} +
              • + ))} +
              + ) : null} +
            • + ))} +
            +
            +
            +

            Software

            +
              + {software.map(({ name, link }) => ( +
            • + + + {name} + + +
            • + ))} +
            +
            +
            +
            +
            +

            Recursos gratuitos

            +

            + Queres receber recursos gratuitos para te ajudar a estudar? Insere o teu email e receberás no teu email imagens, videos, documentos e textos que te ajudarão a + estudar. +

            +
            +
            + + + + + + + + +

            + {email.error}  +

            +
            + +
            + +
            + { + if (event === 'complete') { + setState('finished') + } + }} + /> +
            +
            +

            + Receberás no teu email um link especial para acederes aos recursos gratuitos! +

            + +
            +
            +
            +
            +
            + +
            +

            + A vida universitária +

            +

            + Ok, estás na universidade e não queres ser desrespeitado, envergonhado e humilhado pelos teus colegas. Muito menos pelos professores. Tendo passado pela universidade + e sendo explicador e mentor há mais de {new Date().getFullYear() - 2006} anos, sei como é. Sentes que os professores não se preocupam suficientemente contigo, os teus + colegas estão tão perdidos como tu, e ir à biblioteca não vai fazer as tuas notas magicamente subir. Para além disso, passar mais tempo a ler os livros da + bibliografia das disciplinas não te vai ajudar porque não podes perder ainda mais tempo, e já dormes menos do que seria saudável. +

            +
            +

            + Até já podes ter experimentado centros de explicações. Lembro-me de ir a um centro de explicações para Cálculo quando estava no secundário. Arranjei a minha + mochila, saí de casa e 1h mais tarde cheguei ao centro e estava a estudar. Estávamos a pagar 30€/hora por uma professora numa sala cheia de alunos. Durante as + duas horas que lá estive consegui colocar exatamente duas perguntas à professora, cujas respostas foram curtas e apressadas demais para eu perceber alguma coisa. + A Professora queria distribuir o seu tempo por todos os alunos aparentemente. Lembro-me de me perguntar será que vale a pena perder 2 horas para ir e vir e pagar + 30€/hora para vir para aqui fazer exercícios e apenas obter a atenção da Professora duas vezes em 2 horas? Para isto estudo em casa! +

            +

            + Depois disso experimentei um tutor privado de matemática. Ele sabia a matéria, mas não conseguia fazer a ponte entre o meu conhecimento e o conhecimento dele. Ele + não conseguia perceber que conhecimento de fundo me faltava para compreender os conceitos atuais, então mecanisticamente repetia as mesmas respostas que eu não + seria capaz de perceber enquanto ele não me fornecesse o conhecimento de fundo necessário. Foi frustrante. +

            +

            + Estás à procura de uma solução melhor de subires as tuas notas e sentires-te orgulhoso de ti próprio e daquilo que conseguirás atingir? É por isso que estou aqui. + Para te ajudar a dominar a matéria da universidade e a ganhares notas excelentes com tutoria dedicada a ti! +

            +

            + De iniciante a perito +

            +

            + Imagina-te a comandar o respeito e admiração dos teus pais, professores e colegas. Uau filho (ou filha), estás-te mesmo a safar bem na universidade! Imagina os + professores: malta, se tiveres dúvidas perguntem-lhe a ele (ou ela) que sabe muito disto! Imagina os teus colegas a identificarem o teu conhecimento e a + pedirem-te para guiares sessões de estudo com eles! +

            +
            +
            +

            + Como é trabalhar com um explicador de {name} +

            +
            + André numa chamada com um aluno +
            André numa chamada com um aluno.
            +
            +

            + Imagina isto: o teu professor fazer-te uma pergunta que sabe que nenhum estudante sabe responder. Mas tu tens uma arma secreta - lições com um tutor especializado + em ciência da computação e com skills de comunicação excelentes e que tas ensina. Ao começares a brilhantemente responder à pergunta do professor, os teus colegas + começam a olhar para ti com admiração e forma-se na face do teu professor um sorriso de aprovação. Quando acabas de dar a tua resposta, faz-se silêncio enquanto o + choque da tua resposta subside. Sentes um surto de adrenalina quando de apercebes que toda a gente está a olhar para ti. Sorris e o teu professor diz: Malta, eu + se fosse a vocês fazia amizade com ele, ele vai longe! +

            +

            + Durante o resto da aula todos os teus colegas vão estar a pensar que tu serás um bom amigo para ter. Confia em mim, não vais ter problemas a fazer amigos na + universidade. O teu maior problema vai ser distinguir entre as pessoas que gostam de ti por valorizarem inteligência e as pessoas que se querem aproveitar de ti + para lhes explicares a matéria. +

            +

            + Estás a fazer os exames. Confiante. Concentrado. Os teus colegas lutam para se lembrarem dos conceitos para responderem às questões. Tu passas a parte de + memorização do exame com uma perna às costas. Um excelente começo faz-te sentir ainda mais confiante e dá-te bastante mais tempo para responder às questões que + realmente requerem trabalho. Ainda faltam 20 minutos para o exame acabar mas já acabaste. Com calma e confiança revês o exame e ainda corriges um pequeno erro - + mais meio ponto na classificação. Faltam 10 minutos. Entregas o exame cedo, sentido-te orgulhoso de ti próprio e do que atingiste! Sais da sala, pegas no + telemóvel e ligas aos teus pais. +

            +
              +
            • Estou filho?
            • +
            • Passei!
            • +
            +

            + Como é que isto é possível? Porque tiveste aulas de programação comigo! Eu ensinei como estudar {name} e aprendeste um método cientificamente + provado para memorizar tudo o que for importante. Para além disso, vou ao encontro do teu nível de conhecimento atual e ajudo-te a fazer a ponte entre o + conhecimento que tens agora e o conhecimento que precisarás para passar no exames. Podes ter explicações dedicadas só a ti ou em grupo. +

            +
            +
            +

            Eu dedico-me aos meus alunos

            +

            + Estás a estudar na biblioteca com os teus colegas e encontras um problema que não sabes resolver. Perguntas aos teus colegas mas ninguém sabe a resposta. Estás + bloqueado. Abres o browser e pesquisas no google. Sabes que é uma solução rápida, mas não encontras a resposta online. Mas lembras-te que tens um tutor de + confiança sempre ao teu lado! Pegas no telefone, abres o Whatsapp e escreves uma pequena mensagem. Um minuto depois recebes uma explicação clara que responde + exatamente à dúvida que tinhas. Sentes-te aliviado e agradecido por conseguires continuar a estudar sem solavancos no caminho. +

            +

            + A taxa média de positivas dos meus mais de 650 alunos é de 98,75%. Eu dedico-me aos meus alunos e não descanso enquanto eles não têm resposta a todas as suas + questões! Eu dou-te suporte por Whatsapp para que tenhas a liberdade de colocar todas as questões que quiseres e sentires que podes estudar sem solavancos - desde + que sejam questões que possam ser esclarecidas por mensagem. Caso contrário marcamos uma explicação. Durante as explicações também terás comentários em tempo real + acerca do trabalho que estiveres a fazer. +

            +
            +
            +
            + +
            +

            + Tudo o que precisas para seres um aluno de sucesso +

            +

            + Além de teres acesso a um explicador especializado,aprenderás a organizar teu tempo, materiais e sessões de estudo, para poderes entregar os trabalhos dentro do prazo + e com a qualidade necessária para obteres as melhores notas. +

            +
            +
            +
            + {features.map(feature => ( +
            +
            +
            +
            +

            {feature.description}

            +
            +
            + ))} +
            +
            +
            + + +
            +
            +

            + Sobre o André, o teu explicador de {name} +

            +

            + O André tem vindo a trabalhar como engenheiro de software, há mais de {new Date().getFullYear() - 2006} anos, com empresas como a Fundação Calouste + Gulbenkian, a rede de televisão americana NBC, a marca de bebidas Monster Energy e outras empresas de grande escala. Ele tem fornecido apoio em engenharia a + inúmeras startups e gerido equipas de mais de 20 pessoas. Atualmente dá formação a alunos universitários, engenheiros de software, equipas de desenvolvimento + para melhorarem a qualidade do seu trabalho e o seu curso Mastery for VS Code foi elogiado e destacado pela Microsoft. Ensinar e ajudar pessoas a + transformarem-se sempre foi a sua paixão e é por isso que ao longo da sua carreira obteve comentários notáveis. +

            +
            +
            + André Casal +
            +
            +
            +
            + + +
            +
            +

            + As disciplinas que ensino +

            +

            + Ensino engenharia e ciência da computação há mais de {new Date().getFullYear() - 2006} anos, então dominei algumas matérias. Abaixo podes encontrar uma lista + (não exaustiva) dos assuntos nos quais te posso ajudar. +

            +
            +
            +
            +
            + {stats.map(stat => ( +
            +

            + {stat.name} +

            +

            + + {stat.value} + +

            +
            + ))} +
            +
            +
            + +
            +
            +
            +
            +

            Programação

            +
              + {subjectsITeach.programming.map(item => ( +
            • + {item.name} +
            • + ))} +
            +
            +
            +

            Desenvolvimento Web

            +
              + {subjectsITeach.webdev.map(item => ( +
            • + {item.name} +
            • + ))} +
            +
            +
            +
            +
            +

            Ciência da Computação

            +
              + {subjectsITeach.computerscience.map(item => ( +
            • + {item.name} +
            • + ))} +
            +
            +
            +

            Matemática

            +
              + {subjectsITeach.math.map(item => ( +
            • + {item.name} +
            • + ))} +
            +
            +
            +
            +
            +
            + + +
            +
            +

            + Testemunhos dos alunos incríveis com quem trabalhei

            +
            +
            +
            +
            +

            {`“${featuredTestimonial.body}”`}

            +
            +
            + +
            +

            + {featuredTestimonial.author.name} +

            +
            + {Array(5) + .fill(null) + .map((_value, i) => ( + + ))} +
            +
            +
            +
            + {testimonials.map((columnGroup, columnGroupIdx) => ( +
            + {columnGroup.map((column, columnIdx) => ( +
            + {column.map((testimonial, i) => ( +
            +
            {testimonial.body ?

            {`“${testimonial.body}”`}

            : null}
            +
            + {'imageUrl' in testimonial.author ? ( + {testimonial.author.name} + ) : null} +
            +

            + {testimonial.author.name} +

            +
            + {Array(5) + .fill(null) + .map((_value, i) => ( + + ))} +
            +
            +
            +
            + ))} +
            + ))} +
            + ))} +
            +
            +
            +
            + + +
            +
            +

            + Preços simples +

            +

            + Cobro 40€ por hora. Os pagamentos são feitos por sessão ou através de subscrição mensal. +

            +
            +
            +
            +

            Sessão única

            +

            + Precisa de ajuda com um único projeto ou exame? Agende quantas dessas sessões desejar. +

            +
            +

            + Podemos usar esse tempo para +

            +
            +
            +
              + {singleSessionFeatures.map(feature => ( +
            • +
            • + ))} +
            +
            +
            +
            +
            +

            + Pague por hora +

            +

            + + €40 + + + EUR + +

            + +
            +
            +
            +
            +
            +
            +

            Suporte contínuo 2h/semana

            +

            + Estes são os planos mais populares para estudantes que desejam suporte contínuo. +

            +
            +

            + Podemos usar esse tempo para +

            +
            +
            +
              + {includedFeatures.map(feature => ( +
            • +
            • + ))} +
            +
            +
            +
            +
            +

            + Subscrição mensal +

            +

            + + €320 + + + EUR + +

            + +

            + Assine e cancele a qualquer momento. +

            +
            +
            +
            +
            +
            +
            +

            Suporte contínuo 4h/semana

            +

            + Estes são os planos mais populares para estudantes que desejam ótimas notas. +

            +
            +

            + Podemos usar esse tempo para +

            +
            +
            +
              + {includedFeatures.map(feature => ( +
            • +
            • + ))} +
            +
            +
            +
            +
            +

            + Subscrição mensal +

            +

            + + €640 + + + EUR + +

            + +

            + Assine e cancele a qualquer momento. +

            +
            +
            +
            +
            +
            +
            +

            Suporte contínuo 6h/semana

            +

            + Estes são os planos mais populares para estudantes que desejam notas excelentes. +

            +
            +

            + Podemos usar esse tempo para +

            +
            +
            +
              + {includedFeatures.map(feature => ( +
            • +
            • + ))} +
            +
            +
            +
            +
            +

            + Subscrição mensal +

            +

            + + €960 + + + EUR + +

            + +

            + Assine e cancele a qualquer momento. +

            +
            +
            +
            +
            +
            +
            +

            Suporte contínuo 8h/semana

            +

            + Esses são os planos mais populares para estudantes que desejam as melhores notas. +

            +
            +

            + Podemos usar esse tempo para +

            +
            +
            +
              + {includedFeatures.map(feature => ( +
            • +
            • + ))} +
            +
            +
            +
            +
            +

            + Subscrição mensal +

            +

            + + €1280 + + + EUR + +

            + +

            + Assine e cancele a qualquer momento. +

            +
            +
            +
            +
            +
            + + +
            +
            +

            + Selo de garantia de satisfação +

            +

            + Tenho certeza que vais adorar as sessões de tutoria comigo. Na remota possibilidade de não gostares - e tenho orgulho de dizer que em{' '} + {new Date().getFullYear() - 2006} anos de aulas particulares, ninguém pediu o dinheiro de volta - eu devolverei o teu dinheiro. +

            +
            +
            + Selo de garantia de satisfação +
            +
            +
            + +
            +
            +

            + Uma nota para ti +

            +

            + Só quero agradecer-te por reservares um tempo para leres sobre meu serviço de tutoria. Continua a ser uma tremenda honra ter tantos alunos que confiam em mim para + ajudá-los a encontrar uma maneira melhor de frequentar a faculdade. Sinceramente espero que tenhas decidido ter tutoria de programação, mesmo que não comigo, + porque sei que é uma decisão muito boa. +

            +

            + Ao teu sucesso! 🥂 +

            +
            +
            + Assinatura de André Casal + Assinatura de André Casal +
            +
            +
            + + ) +} +export default Route + +const newsletterSchema = z.object({ email: emailSchema }) +export const verificationType = `resources` + +export async function action({ request, params }: ActionArgs) { + const { subjectslug } = params + const subject = subjects.find(s => s.slug === subjectslug)! + const { name } = subject + const formData = await request.formData() + + // Check for bots + await validateCSRF(formData, request.headers) + checkHoneypot(formData, '/newsletter') + + // Parse form + const submission = parse(formData, { schema: newsletterSchema }) + if (submission.intent !== 'submit') { + return json({ status: 'error', submission } as const) + } + if (!submission.value) { + return json({ status: 'error', submission } as const, { status: 400 }) + } + + // Extract data + const { email } = submission.value + + const resourcesURL = new URL(`${getDomainUrl(request)}/explicacoes/${subjectslug}/recursos`) + resourcesURL.searchParams.set('email', email) + + const oneDay = 60 * 60 * 24 // One day in seconds + const target = email + const { otp, secret, algorithm, period, digits } = generateTOTP({ algorithm: 'sha256', period: oneDay }) + // delete old verifications. Users should not have more than one verification + // of a specific type for a specific target at a time. + await prisma.verification.deleteMany({ + where: { type: verificationType, target }, + }) + await prisma.verification.create({ + data: { + type: verificationType, + target, + algorithm, + secret, + period, + digits, + expiresAt: new Date(Date.now() + period * 1000), + }, + }) + + // add the otp to the url we'll email the user. + resourcesURL.searchParams.set('code', otp) + + // Schedule emails + await scheduleEmailSequence(email) + + // Send email with resource link + await sendEmail({ + to: email, + subject: `🚀 Recursos para estudantes de ${name}`, + react: , + }) + + // Everything ok + return json({ status: 'success', submission } as const) +} + +async function scheduleEmailSequence(email: string) { + const now = new Date() + const in1Day = new Date(now.getTime() + 1 * 24 * 3600 * 1000) + const in2Days = new Date(now.getTime() + 2 * 24 * 3600 * 1000) + const in3Days = new Date(now.getTime() + 3 * 24 * 3600 * 1000) + const in4Days = new Date(now.getTime() + 4 * 24 * 3600 * 1000) + const scheduleDates = [in1Day, in2Days, in3Days, in4Days] + for (let i = 0; i < scheduleDates.length; i++) { + const scheduledAt = scheduleDates[i] + const sequence = i + 1 + await prisma.emailSchedule.upsert({ + where: { + to_sequence: { + to: email, + sequence, + }, + }, + update: { + scheduledAt: scheduledAt, + }, + create: { + to: email, + sequence, + scheduledAt: scheduledAt, + }, + }) + } +} diff --git a/app/routes/_marketing+/_pseo+/materia+/$subjectslug_.recursos.tsx b/app/routes/_marketing+/_pseo+/materia+/$subjectslug_.recursos.tsx new file mode 100644 index 0000000..3cbb5a7 --- /dev/null +++ b/app/routes/_marketing+/_pseo+/materia+/$subjectslug_.recursos.tsx @@ -0,0 +1,110 @@ +import { type LoaderArgs, json, redirect } from '@remix-run/node' +import { subjects } from '../disciplinas.ts' +import { useLoaderData } from '@remix-run/react' +import { parse } from '@conform-to/zod' +import { z } from 'zod' +import { emailSchema } from '~/utils/user-validation.ts' +import { prisma } from '~/utils/db.server.ts' +import { verificationType } from '../explicacoes+/$subjectslug.tsx' +import { verifyTOTP } from '~/utils/totp.server.ts' +import { Container } from '~/ui_components/layout/container.tsx' +import { H1 } from '~/ui_components/typography/h1.tsx' +import { P } from '~/ui_components/typography/p.tsx' + +const verifySchema = z.object({ + email: emailSchema, + code: z.string().min(6).max(6), +}) + +export async function loader({ params, request }: LoaderArgs) { + const searchParams = new URL(request.url).searchParams + if (!searchParams.has('code')) { + return redirect('/') + } + const submission = await parse(searchParams, { + schema: () => + verifySchema.superRefine(async (data, ctx) => { + const verification = await prisma.verification.findFirst({ + where: { + type: verificationType, + target: data.email, + expiresAt: { gt: new Date() }, + }, + select: { + algorithm: true, + secret: true, + period: true, + }, + }) + if (!verification) { + ctx.addIssue({ + path: ['code'], + code: z.ZodIssueCode.custom, + message: `Invalid code`, + }) + return + } + const result = verifyTOTP({ + otp: data.code, + secret: verification.secret, + algorithm: verification.algorithm, + period: verification.period, + window: 0, + }) + if (!result) { + ctx.addIssue({ + path: ['code'], + code: z.ZodIssueCode.custom, + message: `Invalid code`, + }) + return + } + }), + acceptMultipleErrors: () => true, + async: true, + }) + if (!submission.value) { + return redirect('/') + } + const { subjectslug } = params + const subject = subjects.find(s => s.slug === subjectslug)! + return json({ subject }) +} + +const Route = () => { + const { + subject: { name, resources }, + } = useLoaderData() + return ( + +
            +
            +
            +

            + Recursos de {name} +

            +

            + Aqui estão os recursos que te vão ajudar a aprender {name} de forma mais eficaz. +

            + +
            +
            +
            +
            + André, o teu tutor de programação +
            André, o teu professor particular de {name}.
            +
            +
            +
            +
            + ) +} +export default Route diff --git a/app/routes/_pseo+/files/arquitetura-de-computadores.zip b/app/routes/_marketing+/_pseo+/materia+/files/arquitetura-de-computadores.zip similarity index 100% rename from app/routes/_pseo+/files/arquitetura-de-computadores.zip rename to app/routes/_marketing+/_pseo+/materia+/files/arquitetura-de-computadores.zip diff --git a/app/routes/_pseo+/files/sistemas-digitais.zip b/app/routes/_marketing+/_pseo+/materia+/files/sistemas-digitais.zip similarity index 100% rename from app/routes/_pseo+/files/sistemas-digitais.zip rename to app/routes/_marketing+/_pseo+/materia+/files/sistemas-digitais.zip diff --git a/app/routes/_pseo+/files/sistemas-operativos.zip b/app/routes/_marketing+/_pseo+/materia+/files/sistemas-operativos.zip similarity index 100% rename from app/routes/_pseo+/files/sistemas-operativos.zip rename to app/routes/_marketing+/_pseo+/materia+/files/sistemas-operativos.zip diff --git a/app/routes/_marketing+/_pseo+/materia+/tutoring.emails.tsx b/app/routes/_marketing+/_pseo+/materia+/tutoring.emails.tsx new file mode 100644 index 0000000..1464f6c --- /dev/null +++ b/app/routes/_marketing+/_pseo+/materia+/tutoring.emails.tsx @@ -0,0 +1,18 @@ +import { Button, Container, Heading, Html, Tailwind, Text } from '@react-email/components' +import tailwindConfig from '../../../../../tailwind.config.ts' + +export function ResourcesEmail({ subject, resourcesURL }: { subject: string; resourcesURL: string }) { + return ( + + + + Recursos para estudantes de {subject}! + Fico muito feliz por teres decidido melhorar as tuas habilidades de programação! Para te ajudar, envio-te alguns recursos que poderão ser úteis para ti. + + + + + ) +} diff --git a/package-lock.json b/package-lock.json index 365e806..03e9f65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "clsx": "^1.2.1", "compression": "^1.7.4", "confetti-react": "^2.5.0", + "cron": "^3.1.7", "cross-env": "^7.0.3", "crypto-js": "^4.2.0", "dotenv": "^16.3.1", @@ -6751,6 +6752,11 @@ "@types/node": "*" } }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==" + }, "node_modules/@types/mdast": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz", @@ -9525,6 +9531,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cron": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz", + "integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.4.0" + } + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -14993,6 +15008,14 @@ "node": "14 || >=16.14" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/package.json b/package.json index cceb833..e90ca52 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "clsx": "^1.2.1", "compression": "^1.7.4", "confetti-react": "^2.5.0", + "cron": "^3.1.7", "cross-env": "^7.0.3", "crypto-js": "^4.2.0", "dotenv": "^16.3.1", diff --git a/prisma/migrations/20240603142203_add_cron_table_for_email_sequence/migration.sql b/prisma/migrations/20240603142203_add_cron_table_for_email_sequence/migration.sql new file mode 100644 index 0000000..00dec0f --- /dev/null +++ b/prisma/migrations/20240603142203_add_cron_table_for_email_sequence/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - You are about to drop the `Note` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "Note"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "EmailSchedule" ( + "id" TEXT NOT NULL PRIMARY KEY, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "to" TEXT NOT NULL, + "sequence" INTEGER NOT NULL, + "scheduledAt" DATETIME NOT NULL, + "sent" BOOLEAN NOT NULL DEFAULT false +); diff --git a/prisma/migrations/20240603152758_add_unique_constraint_to_email_schedule/migration.sql b/prisma/migrations/20240603152758_add_unique_constraint_to_email_schedule/migration.sql new file mode 100644 index 0000000..d7aaaca --- /dev/null +++ b/prisma/migrations/20240603152758_add_unique_constraint_to_email_schedule/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[to,sequence]` on the table `EmailSchedule` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "EmailSchedule_to_sequence_key" ON "EmailSchedule"("to", "sequence"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f115fa9..6d8e5a7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -114,3 +114,14 @@ model Session { userId String expirationDate DateTime } + +model EmailSchedule { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + to String + sequence Int + scheduledAt DateTime + sent Boolean @default(false) + + @@unique([to, sequence]) +}