From aaf651125b0ce49b4100740eabf7aa111c093717 Mon Sep 17 00:00:00 2001 From: arthur Date: Wed, 12 Nov 2025 15:09:04 -0300 Subject: [PATCH] add typescript --- package-lock.json | 23 +++++++ package.json | 5 +- public/index.html | 27 +++++++- src/api.js | 16 ----- src/api.ts | 52 +++++++++++++++ src/{dom.js => dom.ts} | 17 +++-- src/main.js | 47 ------------- src/main.ts | 148 +++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 16 +++++ 9 files changed, 278 insertions(+), 73 deletions(-) delete mode 100644 src/api.js create mode 100644 src/api.ts rename src/{dom.js => dom.ts} (72%) delete mode 100644 src/main.js create mode 100644 src/main.ts create mode 100644 tsconfig.json diff --git a/package-lock.json b/package-lock.json index 48d513d..757063b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,9 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.1.17", + "@types/toastify-js": "^1.12.4", "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", "vite": "^7.2.2" } }, @@ -1097,6 +1099,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/toastify-js": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/@types/toastify-js/-/toastify-js-1.12.4.tgz", + "integrity": "sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1927,6 +1936,20 @@ "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==", "license": "MIT" }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/vite": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", diff --git a/package.json b/package.json index cf0f520..7f57b4a 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "check": "tsc --pretty" }, "keywords": [], "author": "", @@ -13,7 +14,9 @@ "description": "", "devDependencies": { "@tailwindcss/vite": "^4.1.17", + "@types/toastify-js": "^1.12.4", "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", "vite": "^7.2.2" }, "dependencies": { diff --git a/public/index.html b/public/index.html index ad57969..706f377 100644 --- a/public/index.html +++ b/public/index.html @@ -29,8 +29,31 @@
- +
+

Cadastrar novo Workshop

+
+ + + + + - + + + + +
+
+ + diff --git a/src/api.js b/src/api.js deleted file mode 100644 index 76ecb0e..0000000 --- a/src/api.js +++ /dev/null @@ -1,16 +0,0 @@ -import axios from 'axios' -export const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:5000' -const TOKEN = import.meta.env.VITE_TOKEN ?? '' - -// ajuste o path caso sua API seja /api/workshops -const WORKSHOPS_PATH = '/workshops' - -const getHeaders = () => { - return { - Authorization: `Bearer ${TOKEN}` - } -} -export async function searchWorkshops(q) { - const { data } = await axios.get(`${API_BASE}${WORKSHOPS_PATH}`, { params: { q }, headers: getHeaders() }) - return data // espera-se um array -} diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..575c2d2 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,52 @@ +import axios from 'axios' +export const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:5000' +const TOKEN = import.meta.env.VITE_TOKEN ?? '' + +// ajuste o path caso sua API seja /api/workshops +const WORKSHOPS_PATH = '/workshops' + +// src/api.ts +export type Workshop = { + id: string; + title: string; + startAt: string; // ISO string + endAt: string; // ISO string + isOnline: boolean; + location?: string | null; + capacity?: number; +}; + +export type WorkshopList = Workshop[]; + +export type NewWorkshop = { + title: string; + startAt: string; // ISO + description: string; + endAt: string; // ISO + isOnline: boolean; + location?: string | null; + capacity?: number; // >= 1 (opcional) +}; + +const getHeaders = () => { + console.log(TOKEN) + return { + Authorization: `Bearer ${TOKEN}` + } +} +export async function searchWorkshops(q: string): Promise { + const { data } = await axios.get( + `${API_BASE}${WORKSHOPS_PATH}`, + { params: { q }, headers: getHeaders() } + ); + return data; +} + +export async function createWorkshop(payload: NewWorkshop): Promise { + // POST de criação; backend retorna o Workshop criado (com id) + const { data } = await axios.post( + `${API_BASE}${WORKSHOPS_PATH}`, + payload, { headers: getHeaders() } + ); + return data; +} diff --git a/src/dom.js b/src/dom.ts similarity index 72% rename from src/dom.js rename to src/dom.ts index 8c115cb..bcfadfd 100644 --- a/src/dom.js +++ b/src/dom.ts @@ -1,4 +1,7 @@ -export function renderList(container, items = []) { +import type { Workshop } from './api'; + +export function renderList(container: HTMLElement, items: Workshop[] = []): void { + container.innerHTML = '' if (!Array.isArray(items) || items.length === 0) { container.insertAdjacentHTML('beforeend', `

Nenhum resultado.

`) @@ -8,20 +11,20 @@ export function renderList(container, items = []) { container.insertAdjacentHTML('beforeend', html) } -export function updateMeta(el, text = '') { el.textContent = text } +export function updateMeta(el: HTMLElement, text = ''): void { el.textContent = text } -export function renderLoading(container) { +export function renderLoading(container: HTMLElement) { const skeleton = Array.from({ length: 6 }).map(() => `
` ).join('') container.innerHTML = `
${skeleton}
` } -export function debounce(fn, ms = 350) { - let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms) } +export function debounce(fn: (...args: any[]) => Promise, ms = 350) { + let t: number | undefined; return (...args: any[]) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms) } } -function toCardHtml(w) { +function toCardHtml(w: Workshop) { const title = escapeHtml(w?.title ?? 'Sem título') const where = w?.isOnline ? 'Online' : escapeHtml(w?.location ?? 'Presencial') const dt = formatDateRange(w?.startAt, w?.endAt) @@ -38,7 +41,7 @@ function toCardHtml(w) { ` } -function formatDateRange(a, b) { +function formatDateRange(a: string | number | Date, b: string | number | Date) { try { return `${new Date(a).toLocaleString()} — ${new Date(b).toLocaleString()}` } catch { return '' } } function escapeHtml(s = '') { const d = document.createElement('div'); d.textContent = s; return d.innerHTML } diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 16cf46f..0000000 --- a/src/main.js +++ /dev/null @@ -1,47 +0,0 @@ -import Toastify from 'toastify-js' -import 'toastify-js/src/toastify.css' - -import { searchWorkshops } from './api.js' -import { renderList, updateMeta, renderLoading, debounce } from './dom.js' - -const form = document.getElementById('search-form') -const input = document.getElementById('q') -const results = document.getElementById('results') -const meta = document.getElementById('meta') - -function toast(text, kind = 'info') { - const bg = { info: '#3b82f6', ok: '#10b981', warn: '#f59e0b', err: '#ef4444' }[kind] ?? '#3b82f6' - Toastify({ text, gravity: 'top', position: 'right', backgroundColor: bg, duration: 2400 }).showToast() -} - -async function runSearch(term) { - const q = term.trim() - if (!q) { - updateMeta(meta, 'Digite um termo para buscar.') - renderList(results, []) - toast('Campo de busca vazio.', 'warn') - return - } - - updateMeta(meta, 'Carregando…') - renderLoading(results) - - try { - const items = await searchWorkshops(q) - renderList(results, items) - updateMeta(meta, `Encontrados ${items?.length ?? 0} resultado(s) para “${q}”.`) - toast('Busca concluída.', 'ok') - } catch (err) { - console.error(err) - renderList(results, []) - updateMeta(meta, '') - toast('Erro ao buscar. Confira console/Network.', 'err') - } -} - -// Submit on-demand -form.addEventListener('submit', (e) => { e.preventDefault(); runSearch(input.value) }) - -// Busca reativa com debounce -const debounced = debounce(() => runSearch(input.value), 500) -input.addEventListener('input', debounced) diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..9532e19 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,148 @@ +import Toastify from 'toastify-js'; +import 'toastify-js/src/toastify.css'; + +import { searchWorkshops, createWorkshop } from './api'; +import type { NewWorkshop, Workshop, WorkshopList } from './api'; +import { renderList, updateMeta, renderLoading } from './dom'; + +// ===== Seletores da área de busca +const form = document.getElementById('search-form') as HTMLFormElement; +const input = document.getElementById('q') as HTMLInputElement; +const results = document.getElementById('results') as HTMLElement; +const meta = document.getElementById('meta') as HTMLElement; + +// ===== Seletores da área de inserção +const addForm = document.getElementById('add-form') as HTMLFormElement | null; +const titleEl = document.getElementById('title') as HTMLInputElement | null; +const capacityEl = document.getElementById('capacity') as HTMLInputElement | null; +const isOnlineEl = document.getElementById('isOnline') as HTMLInputElement | null; +const locationEl = document.getElementById('location') as HTMLInputElement | null; +const startAtEl = document.getElementById('startAt') as HTMLInputElement | null; +const endAtEl = document.getElementById('endAt') as HTMLInputElement | null; +const descriptionEl = document.getElementById('description') as HTMLInputElement; + +// ===== Toast helper +function toast(text: string, kind: 'info' | 'ok' | 'warn' | 'err' = 'info') { + const bg: Record = { + info: '#3b82f6', ok: '#10b981', warn: '#f59e0b', err: '#ef4444' + }; + Toastify({ text, gravity: 'top', position: 'right', backgroundColor: bg[kind], duration: 2400 }).showToast(); +} + +// ===== BUSCA +form.addEventListener('submit', async (e) => { + e.preventDefault(); + const q = input.value.trim(); + if (!q) { + updateMeta(meta, 'Digite um termo para buscar.'); + renderList(results, []); + return; + } + + updateMeta(meta, 'Carregando…'); + renderLoading(results); + + try { + const items = await searchWorkshops(q); + renderList(results, items); + updateMeta(meta, `Encontrados ${items?.length ?? 0} resultado(s) para “${q}”.`); + } catch (err) { + console.error(err); + renderList(results, []); + updateMeta(meta, 'Erro ao buscar. Verifique console/Network.'); + } +}); + +// ===== Inserção (TypeScript) +if (addForm) { + addForm.addEventListener('submit', async (e) => { + e.preventDefault(); + try { + const payload = parseNewWorkshopFromForm(); // monta e valida + const created = await createWorkshop(payload); + resetCreateForm(); + toast('Workshop inserido com sucesso!', 'ok'); + + // (Opcional) Atualizar a lista atual, se estiver filtrada: + const term = input.value.trim(); + if (term) { + // reexecuta a busca atual para “enxergar” o item novo + form.requestSubmit(); + } + // Caso contrário, deixamos a UI como está (ou você pode decidir listar tudo aqui) + } catch (err) { + console.error(err); + const msg = err instanceof Error ? err.message : 'Erro ao inserir'; + toast(msg, 'err'); + } + }); +} + +// ===== Helpers: montar payload e validar + +// Converte 'YYYY-MM-DDTHH:mm' local para ISO (UTC) +// IMPORTANTE: isso converte assumindo horário local do navegador +function toIsoFromLocal(input: string): string { + if (!input) throw new Error('Data/hora inválida.'); + const d = new Date(input); + if (isNaN(d.getTime())) throw new Error('Formato de data/hora inválido.'); + return d.toISOString(); +} + +// Lê e valida o formulário; retorna NewWorkshop ou lança Error +function parseNewWorkshopFromForm(): NewWorkshop { + // elementos obrigatórios + if (!titleEl || !isOnlineEl || !startAtEl || !endAtEl) { + throw new Error('Formulário não inicializado corretamente.'); + } + const title = (titleEl.value ?? '').trim(); + if (title.length < 5) throw new Error('Título deve ter pelo menos 5 caracteres.'); + + const isOnline = !!isOnlineEl.checked; + + const startAtISO = toIsoFromLocal(startAtEl.value); + const endAtISO = toIsoFromLocal(endAtEl.value); + + const startMs = new Date(startAtISO).getTime(); + const endMs = new Date(endAtISO).getTime(); + if (endMs <= startMs) throw new Error('EndAt deve ser depois de StartAt.'); + + let location: string | null | undefined = null; + if (!isOnline) { + if (!locationEl) throw new Error('Campo de local não encontrado.'); + const loc = (locationEl.value ?? '').trim(); + if (!loc) throw new Error('Local é obrigatório para workshops presenciais.'); + location = loc; + } + + let capacity: number | undefined; + if (capacityEl) { + const raw = capacityEl.value?.trim(); + if (raw) { + const n = Number(raw); + if (!Number.isFinite(n) || n < 1) throw new Error('Capacidade deve ser um número >= 1.'); + capacity = n; + } + } + + const description = (descriptionEl.value ?? '').trim(); + if (title.length < 5) throw new Error('Descrição deve ter pelo menos 5 caracteres.'); + + + const payload: NewWorkshop = { + title, + startAt: startAtISO, + endAt: endAtISO, + description, + isOnline, + location, + capacity + }; + return payload; +} + +function resetCreateForm(): void { + if (!addForm) return; + addForm.reset(); + titleEl?.focus(); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..06655d1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "noEmit": true, + "allowJs": true, + "checkJs": false, + "types": ["vite/client"], + "isolatedModules": true, + "skipLibCheck": true + }, + "include": ["src"] +}