From b900df2f949d8b77f3f8018350d4da07db21c493 Mon Sep 17 00:00:00 2001 From: Marcelo Santos <117441129+marcelo-m7@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:50:34 +0100 Subject: [PATCH] Replace Next.js scaffolding with Hugo + Supabase setup --- .github/workflows/sync-md-to-supabase.yml | 31 ++ .github/workflows/validate-md.yml | 25 ++ README.md | 169 +++---- config/_default/hugo.toml | 46 +- config/_default/languages.toml | 37 +- config/_default/menus/menus.en.toml | 89 +--- config/_default/menus/menus.nl.toml | 39 -- config/_default/menus/menus.pt.toml | 9 + config/_default/params.toml | 185 +++----- content/_index.md | 18 +- content/blog/_index.md | 19 - content/blog/example/index.md | 25 -- content/courses/LESTI/2024-2025/index.md | 23 + .../uc/LESTI-ALG1/estruturas-de-dados.md | 18 + .../LESTI/2024-2025/uc/LESTI-ALG1/index.md | 35 ++ content/docs/_index.md | 15 - content/docs/guides/_index.md | 15 - content/docs/guides/example.md | 21 - content/docs/reference/_index.md | 17 - content/docs/reference/example.md | 21 - content/docs/resources.md | 17 - content/en/_index.md | 7 + content/privacy.md | 14 - hugo_stats.json | 181 +++----- layouts/_default/baseof.html | 22 + .../footer/script-footer-custom.html | 23 +- layouts/course/single.html | 55 +++ layouts/home.html | 97 ++--- layouts/topic/single.html | 38 ++ layouts/uc/single.html | 63 +++ package-lock.json | 202 ++++++++- package.json | 25 +- scripts/sync-content.mjs | 253 +++++++++++ scripts/validate-frontmatter.js | 169 +++++++ static/js/loaders.js | 411 ++++++++++++++++++ static/js/supabaseClient.js | 27 ++ .../migrations/20240101000000_init_facodi.sql | 261 +++++++++++ supabase/seed.sql | 122 ++++++ 38 files changed, 2068 insertions(+), 776 deletions(-) create mode 100644 .github/workflows/sync-md-to-supabase.yml create mode 100644 .github/workflows/validate-md.yml delete mode 100644 config/_default/menus/menus.nl.toml create mode 100644 config/_default/menus/menus.pt.toml delete mode 100644 content/blog/_index.md delete mode 100644 content/blog/example/index.md create mode 100644 content/courses/LESTI/2024-2025/index.md create mode 100644 content/courses/LESTI/2024-2025/uc/LESTI-ALG1/estruturas-de-dados.md create mode 100644 content/courses/LESTI/2024-2025/uc/LESTI-ALG1/index.md delete mode 100644 content/docs/_index.md delete mode 100644 content/docs/guides/_index.md delete mode 100644 content/docs/guides/example.md delete mode 100644 content/docs/reference/_index.md delete mode 100644 content/docs/reference/example.md delete mode 100644 content/docs/resources.md create mode 100644 content/en/_index.md delete mode 100644 content/privacy.md create mode 100644 layouts/_default/baseof.html create mode 100644 layouts/course/single.html create mode 100644 layouts/topic/single.html create mode 100644 layouts/uc/single.html create mode 100755 scripts/sync-content.mjs create mode 100755 scripts/validate-frontmatter.js create mode 100644 static/js/loaders.js create mode 100644 static/js/supabaseClient.js create mode 100644 supabase/migrations/20240101000000_init_facodi.sql create mode 100644 supabase/seed.sql diff --git a/.github/workflows/sync-md-to-supabase.yml b/.github/workflows/sync-md-to-supabase.yml new file mode 100644 index 00000000..e987f79e --- /dev/null +++ b/.github/workflows/sync-md-to-supabase.yml @@ -0,0 +1,31 @@ +name: Sync Markdown to Supabase + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Validate frontmatter + run: npm run lint:content + + - name: Sync Markdown with Supabase + run: npm run sync:content + env: + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }} diff --git a/.github/workflows/validate-md.yml b/.github/workflows/validate-md.yml new file mode 100644 index 00000000..92914d3e --- /dev/null +++ b/.github/workflows/validate-md.yml @@ -0,0 +1,25 @@ +name: Validate Markdown frontmatter + +on: + pull_request: + push: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Validate frontmatter + run: npm run lint:content diff --git a/README.md b/README.md index 4c7882f1..61ec4ed4 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,135 @@ # FACODI — Faculdade Comunitária Digital -**FACODI** é uma plataforma EAD gratuita e open-source inspirada nos planos curriculares da Universidade do Algarve (UALG). -Nosso objetivo é **democratizar o acesso ao ensino superior** por meio de trilhas de estudo organizadas em cursos, unidades curriculares e playlists do YouTube. +A **FACODI** é uma plataforma de aprendizagem aberta que replica planos curriculares da Universidade do Algarve (UAlg) em formato digital. Todo o conteúdo é escrito em Markdown, sincronizado com o Supabase e disponibilizado como um site estático construído com [Hugo](https://gohugo.io/) (tema Doks). -🚀 Projeto mantido pela [Monynha Softwares](https://monynha.com). +> 🎯 Objetivo: democratizar o acesso ao ensino superior com cursos, unidades curriculares, playlists públicas e conteúdo versionado pela comunidade. --- -## ✨ Funcionalidades +## ✨ Principais funcionalidades -- 📚 Catálogo de cursos e currículos completos -- 🎥 Aulas organizadas em **playlists do YouTube** -- 📝 Conteúdo textual em **Markdown versionado** -- 🔑 Autenticação via [Supabase Auth](https://supabase.com) -- 📊 Acompanhamento de progresso por vídeo -- 🌍 Multi-idioma: PT / EN / ES / FR +- 📚 Catálogo de cursos com planos curriculares completos +- 🎓 Unidades curriculares com ementa, resultados de aprendizagem e tópicos +- 🎥 Playlists abertas do YouTube organizadas por prioridade +- 📝 Conteúdo em Markdown sincronizado automaticamente com o Supabase +- 🌐 Multilíngue (PT como padrão, EN preparado como fallback) --- - +--- -## 📂 Estrutura do Repositório +## 📂 Estrutura do repositório ```bash -facodi-docs/ +facodi.pt/ ├─ README.md -├─ .github/ -│ └─ workflows/ -│ ├─ validate-md.yml -│ └─ sync-md-to-supabase.yml ├─ config/ -│ ├─ _default/ -├─ scripts/ -├─ package.json -├─ package-lock.json +│ └─ _default/ ├─ content/ -│ ├─ _index.md -│ └─ courses/ -│ └─ LESTI/ -│ └─ 2024-2025/ -│ ├─ index.md -│ └─ uc/ -│ ├─ LESTI-ALG1/ -│ │ ├─ index.md -│ │ └─ estruturas-de-dados.md -│ └─ LESTI-BD1/ -│ └─ index.md -├─ static/ (opcional: imagens anexas ao conteúdo) -│ └─ courses/ -│ └─ ... -└─ schemas/ (opcional: documentação de esquema e seeds) -├─ README.md -├─ mapping.md -└─ examples/ -└─ frontmatter-samples.md -```` +│ ├─ _index.md +│ └─ courses/ +│ └─ LESTI/ +│ └─ 2024-2025/ +│ ├─ index.md # Curso +│ └─ uc/ +│ └─ LESTI-ALG1/ +│ ├─ index.md # Unidade curricular +│ └─ estruturas-de-dados.md # Tópico +├─ layouts/ +│ ├─ _default/baseof.html +│ ├─ course/single.html +│ ├─ uc/single.html +│ └─ topic/single.html +├─ static/ +│ └─ js/ +│ ├─ loaders.js +│ └─ supabaseClient.js +├─ supabase/ +│ ├─ migrations/ +│ │ └─ ... sql +│ └─ seed.sql +└─ .github/ + └─ workflows/ + ├─ sync-md-to-supabase.yml + └─ validate-md.yml +``` --- -## ⚙️ Como rodar localmente +## 🛠️ Scripts disponíveis -```bash -# Clonar o repositório -git clone https://github.com/Monynha-Softwares/facodi.pt.git -cd facodi.pt +| Script | Descrição | +| ------ | --------- | +| `npm run dev` | Inicia `hugo server -D` com *hot reload* | +| `npm run build` | Gera os artefatos estáticos com `hugo` | +| `npm run lint:content` | Valida o frontmatter dos arquivos Markdown | +| `npm run sync:content` | Sincroniza Markdown → Supabase (usa `SUPABASE_SERVICE_KEY`) | -# Instalar dependências -pnpm install +--- -# Iniciar Supabase local -pnpm supabase start +## 🚀 Desenvolvimento local -# Rodar o frontend -pnpm dev --filter=web -``` +1. **Instale as dependências:** + ```bash + npm install + ``` ---- +2. **Configure as variáveis de ambiente** (exemplo usando Linux/macOS): + ```bash + export SUPABASE_URL="https://.supabase.co" + export SUPABASE_ANON_KEY="" + export SUPABASE_SERVICE_KEY="" + ``` + +3. **Suba o servidor Hugo:** + ```bash + npm run dev + ``` -## 🤝 Contribuindo +4. **(Opcional) Sincronize conteúdo com o banco:** + ```bash + npm run sync:content + ``` -FACODI é open-source! Você pode contribuir de várias formas: +> As chaves **anon** e **service** também são usadas nos workflows do GitHub (definidas como *repository secrets*). -1. Fork o projeto e abra um Pull Request -2. Relate bugs ou sugira features em [Issues](../../issues) -3. Traduza conteúdos (PT → EN/ES/FR) -4. Ajude a revisar planos curriculares e trilhas de estudo +--- + +## 🧪 Qualidade e CI -Consulte nosso guia em [`CONTRIBUTING.md`](./CONTRIBUTING.md). +- `validate-md.yml`: valida campos obrigatórios do frontmatter em cada PR. +- `sync-md-to-supabase.yml`: executa o script de sincronização (`npm run sync:content`) sempre que a branch principal recebe alterações. + +Ambos os workflows exigem as variáveis `SUPABASE_URL`, `SUPABASE_ANON_KEY` e `SUPABASE_SERVICE_KEY` configuradas como *secrets*. --- -## 👩‍💻 Autores & Créditos +## 🤝 Como contribuir + +1. Faça um fork do repositório +2. Crie uma branch com a sua contribuição +3. Garanta que os testes/scripts passam +4. Abra um Pull Request descrevendo as mudanças -* [Marcelo Santos](https://github.com/marcelosantos) — fundador do projeto -* Comunidade Monynha Softwares -* Base acadêmica: planos curriculares da [UALG](https://www.ualg.pt) +Consulte também [`CONTRIBUTING.md`](./CONTRIBUTING.md) para orientações gerais. --- -## 📜 Licença +## 👥 Créditos -Este projeto é distribuído sob a licença **MIT**. -Veja o arquivo [`LICENSE`](./LICENSE) para mais detalhes. +- Projeto mantido pela [Monynha Softwares](https://monynha.com) +- Conteúdo inspirado nos planos curriculares da [Universidade do Algarve](https://www.ualg.pt) +- Comunidade FACODI ❤️ --- + +## 📜 Licença + +Distribuído sob a licença [MIT](./LICENSE). diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml index 274c6a3f..72679145 100644 --- a/config/_default/hugo.toml +++ b/config/_default/hugo.toml @@ -1,43 +1,33 @@ -title = "Monynha Docs" -baseurl = "https://docs.monynha.com/" +title = "FACODI — Faculdade Comunitária Digital" +baseurl = "https://facodi.pt/" disableAliases = true disableHugoGeneratorInject = true -# disableKinds = ["taxonomy", "term"] enableEmoji = true enableGitInfo = false enableRobotsTXT = true -languageCode = "en-US" +languageCode = "pt-PT" rssLimit = 10 -summarylength = 20 # 70 (default) +summarylength = 40 -# Multilingual -defaultContentLanguage = "en" -disableLanguages = ["de", "nl"] +defaultContentLanguage = "pt" defaultContentLanguageInSubdir = false -copyRight = "Copyright (c) 2020-2024 Thulite" +copyRight = "FACODI — Conteúdo aberto para aprendizagem comunitária" [build.buildStats] enable = true [outputs] - home = ["HTML", "RSS", "searchIndex"] + home = ["HTML", "RSS"] section = ["HTML", "RSS", "SITEMAP"] -[outputFormats.searchIndex] - mediaType = "application/json" - baseName = "search-index" - isPlainText = true - notAlternative = true - -# Add output format for section sitemap.xml [outputFormats.SITEMAP] mediaType = "application/xml" baseName = "sitemap" isHTML = false isPlainText = true noUgly = true - rel = "sitemap" + rel = "sitemap" [sitemap] changefreq = "monthly" @@ -47,31 +37,21 @@ copyRight = "Copyright (c) 2020-2024 Thulite" [caches] [caches.getjson] dir = ":cacheDir/:project" - maxAge = -1 # "30m" + maxAge = -1 [taxonomies] - contributor = "contributors" - category = "categories" tag = "tags" [permalinks] - blog = "/blog/:slug/" - docs = "/docs/:sections[1:]/:slug/" -# docs = "/docs/1.0/:sections[1:]/:slug/" - -[minify.tdewolff.html] - keepWhitespace = false + courses = "/courses/:sections[1:]/" [pagination] - pagerSize = 10 + pagerSize = 12 [related] threshold = 80 includeNewer = true toLower = false - [[related.indices]] - name = "categories" - weight = 100 [[related.indices]] name = "tags" weight = 80 @@ -85,3 +65,7 @@ copyRight = "Copyright (c) 2020-2024 Thulite" hint = "photo" quality = 85 resampleFilter = "Lanczos" + +[security] + [security.funcs] + getenv = ['^HUGO_', '^CI$', '^SUPABASE_'] diff --git a/config/_default/languages.toml b/config/_default/languages.toml index 23667cdd..8846aa2e 100644 --- a/config/_default/languages.toml +++ b/config/_default/languages.toml @@ -1,32 +1,17 @@ +[pt] + languageName = "Português" + contentDir = "content" + weight = 1 + [pt.params] + languageISO = "PT" + languageTag = "pt-PT" + footer = 'Construído pela comunidade FACODI com Hugo e Supabase.' + [en] languageName = "English" contentDir = "content/en" - weight = 10 + weight = 2 [en.params] languageISO = "EN" languageTag = "en-US" - footer = 'Brought to you by Thulite' - alertText = 'Doks version 1.0 just shipped!' - -[de] - languageName = "German" - contentDir = "content/de" - weight = 15 - [de.params] - languageISO = "DE" - languageTag = "de-DE" - footer = 'Gebaut mit Thulite' - alertText = 'Neue Version ist da! Doks v0.5' - -[nl] - languageName = "Nederlands" - contentDir = "content/nl" - weight = 20 - [nl.params] - languageISO = "NL" - languageTag = "nl-NL" - titleAddition = "Modern documentatie-thema" - description = "Doks is een Hugo-thema waarmee je moderne documentatie-websites kunt bouwen die veilig, snel en klaar voor SEO zijn — standaard." - titleHome = "Doks thema" - footer = 'Mogelijk gemaakt door Netlify, Hugo, en Doks' - alertText = 'Introductie van het Doks-kinderthema, verschillende DX + UX-updates en meer! Bekijk Doks v0.2' + footer = 'Built by the FACODI community. English content coming soon.' diff --git a/config/_default/menus/menus.en.toml b/config/_default/menus/menus.en.toml index 26ca94e3..91a65ca0 100644 --- a/config/_default/menus/menus.en.toml +++ b/config/_default/menus/menus.en.toml @@ -1,90 +1,9 @@ -# [[docs]] -# name = "Prologue" -# weight = 10 -# identifier = "prologue" -# url = "/docs/prologue/" - -# [[docs]] -# name = "Help" -# weight = 60 -# identifier = "help" -# url = "/docs/help/" - -# [[docs]] -# name = "Lorem" -# weight = 70 -# identifier = "lorem" -# url = "/docs/lorem/" - -[[guide]] - name = "Lorem" - weight = 10 - identifier = "lorem" - url = "/guide/lorem/" - -[[tutorial]] - name = "Lorem" - weight = 10 - identifier = "lorem" - url = "/tutorial/lorem/" - [[main]] - name = "Docs" - url = "/docs/guides/example-guide/" -# url = "/docs/1.0/prologue/introduction/" + name = "Courses" + url = "/" weight = 10 -[[main]] - name = "Blog" - url = "/blog/" - weight = 30 - -# [[social]] -# name = "Twitter" -# pre = '' -# url = "https://twitter.com/getdoks" -# weight = 10 -# -# [[social]] -# name = "Discord" -# pre = '' -# url = "https://getdoks.org/chat" -# weight = 20 - -[[social]] - name = "GitHub" - pre = '' - url = "https://github.com/thuliteio/doks" - post = "v0.1.0" - weight = 30 - -# [[social]] -# name = "LinkedIn" -# pre = '' -# url = "https://www.linkedin.com/" -# weight = 40 - -# [[sidebar_docs]] -# name = "Guides" -# pageRef = "/docs/guides" -# weight = 10 -# -# [[sidebar_docs]] -# name = "Reference" -# pageRef = "/docs/reference" -# weight = 20 -# -# [[sidebar_docs]] -# name = "Resources" -# pageRef = "/docs/resources" -# weight = 30 - [[footer]] - name = "Privacy Policy" - url = "/privacy/" + name = "Open Source" + url = "https://github.com/Monynha-Softwares/facodi.pt" weight = 10 - -# [[footer]] -# name = "Terms of Service" -# url = "/terms/" -# weight = 20 diff --git a/config/_default/menus/menus.nl.toml b/config/_default/menus/menus.nl.toml deleted file mode 100644 index e530352a..00000000 --- a/config/_default/menus/menus.nl.toml +++ /dev/null @@ -1,39 +0,0 @@ -[[docs]] - name = "Prologue" - weight = 10 - identifier = "prologue" - url = "/docs/prologue/" - -[[docs]] - name = "Help" - weight = 60 - identifier = "help" - url = "/docs/help/" - -[[main]] - name = "Docs" - url = "/docs/prologue/introduction/" - weight = 10 - -# [[main]] -# name = "Blog" -# url = "/blog/" -# weight = 20 - -[[social]] - name = "GitHub" - pre = "" - url = "https://github.com/h-enk/doks" - post = "v0.1.0" - weight = 10 - -[[social]] - name = "Twitter" - pre = "" - url = "https://twitter.com/getdoks" - weight = 20 - -# [[footer]] -# name = "Privacy" -# url = "/privacy-policy/" -# weight = 10 diff --git a/config/_default/menus/menus.pt.toml b/config/_default/menus/menus.pt.toml new file mode 100644 index 00000000..b3e88ccb --- /dev/null +++ b/config/_default/menus/menus.pt.toml @@ -0,0 +1,9 @@ +[[main]] + name = "Cursos" + url = "/" + weight = 10 + +[[footer]] + name = "Comunidade" + url = "https://github.com/Monynha-Softwares/facodi.pt" + weight = 10 diff --git a/config/_default/params.toml b/config/_default/params.toml index ddfffddb..1ce8f688 100644 --- a/config/_default/params.toml +++ b/config/_default/params.toml @@ -1,140 +1,75 @@ # Hugo -title = "Monynha Docs" -description = "Get to know more about our awesome projects and awesome open source resources!" +title = "FACODI — Faculdade Comunitária Digital" +description = "Catálogo aberto de cursos inspirados nos planos curriculares da Universidade do Algarve." images = ["cover.png"] -# mainSections -mainSections = ["docs"] +mainSections = ["courses"] [social] - twitter = "getdoks" + github = "Monynha-Softwares/facodi.pt" # Doks (@thulite/doks-core) [doks] - # Color mode - colorMode = "auto" # auto (default), light or dark - colorModeToggler = true # true (default) or false (this setting is only relevant when colorMode = auto) - - # Navbar - navbarSticky = true # true (default) or false - containerBreakpoint = "lg" # "", "sm", "md", "lg" (default), "xl", "xxl", or "fluid" - - ## Button - navBarButton = false # false (default) or true - navBarButtonUrl = "/docs/prologue/introduction/" - navBarButtonText = "Get started" - - # FlexSearch - flexSearch = true # true (default) or false - searchExclKinds = [] # list of page kinds to exclude from search indexing (e.g. ["home", "taxonomy", "term"] ) - searchExclTypes = [] # list of content types to exclude from search indexing (e.g. ["blog", "docs", "legal", "contributors", "categories"]) - showSearch = [] # [] (all pages, default) or homepage (optionally) and list of sections (e.g. ["homepage", "blog", "guides"]) - indexSummary = false # true or false (default); whether to index only the `.Summary` instead of the full `.Content`; limits the respective JSON field size and thus increases loading time - - ## Search results - showDate = false # false (default) or true - showSummary = true # true (default) or false - searchLimit = 99 # 0 (no limit, default) or natural number - - # Global alert - alert = false # false (default) or true - alertDismissable = true # true (default) or false - - # Bootstrap - bootstrapJavascript = false # false (default) or true - - # Nav - sectionNav = ["docs"] # ["docs"] (default) or list of sections (e.g. ["docs", "guides"]) - toTopButton = false # false (default) or true - breadcrumbTrail = false # false (default) or true - headlineHash = true # true (default) or false - scrollSpy = true # true (default) or false - - # Multilingual - multilingualMode = false # false (default) or true - showMissingLanguages = true # whether or not to show untranslated languages in the language menu; true (default) or false - - # Versioning - docsVersioning = false # false (default) or true + colorMode = "auto" + colorModeToggler = true + navbarSticky = true + containerBreakpoint = "lg" + + navBarButton = false + navBarButtonUrl = "/" + navBarButtonText = "" + + flexSearch = false + searchExclKinds = [] + searchExclTypes = [] + showSearch = [] + indexSummary = false + + showDate = false + showSummary = true + searchLimit = 50 + + alert = false + alertDismissable = true + + bootstrapJavascript = true + + sectionNav = ["courses"] + toTopButton = true + breadcrumbTrail = false + headlineHash = true + scrollSpy = false + + multilingualMode = true + showMissingLanguages = false + + docsVersioning = false docsVersion = "1.0" - # UX - headerBar = false # true (default) or false - backgroundDots = true # true (default) or false - - # Homepage - sectionFooter = false # false (default) or true - - # Blog - relatedPosts = false # false (default) or true - imageList = true # true (default) or false - imageSingle = true # true (default) or false - - # Repository - editPage = false # false (default) or true - lastMod = false # false (default) or true - repoHost = "GitHub" # GitHub (default), Gitea, GitLab, Bitbucket, or BitbucketServer - docsRepo = "https://github.com/h-enk/doks" - docsRepoBranch = "main" # main (default), master, or - docsRepoSubPath = "" # "" (none, default) or - - krokiURL = "https://kroki.io" # "https://kroki.io" (default) or custom URL like http://localhost:8000. - - # SCSS colors - # backGround = "yellowgreen" - ## Dark theme - # textDark = "#dee2e6" # "#dee2e6" (default), "#dee2e6" (orignal), or custom color - # accentDark = "#5d2f86" # "#5d2f86" (default), "#5d2f86" (original), or custom color - ## Light theme - # textLight = "#1d2d35" # "#1d2d35" (default), "#1d2d35" (orignal), or custom color - # accentLight = "#8ed6fb" # "#8ed6fb" (default), "#8ed6fb" (orignal), or custom color - - # [doks.menu] - # [doks.menu.section] - # auto = true # true (default) or false - # collapsibleSidebar = true # true (default) or false - -# Debug -[render_hooks.image] - errorLevel = 'ignore' # ignore (default), warning, or error (fails the build) - -[render_hooks.link] - errorLevel = 'ignore' # ignore (default), warning, or error (fails the build) - highlightBroken = false # true or false (default) - -# Images (@thulite/images) -[thulite_images] - [thulite_images.defaults] - decoding = "async" # sync, async, or auto (default) - fetchpriority = "auto" # high, low, or auto (default) - loading = "lazy" # eager or lazy (default) - widths = [480, 576, 768, 1025, 1200, 1440] # [640, 768, 1024, 1366, 1600, 1920] for example - sizes = "auto" # 100vw (default), 75vw, or auto for example - process = "" # "fill 1600x900" or "fill 2100x900" for example - lqip = "16x webp q20" # "16x webp q20" or "21x webp q20" for example - -# Inline SVG (@thulite/inline-svg) -[inline_svg] - iconSetDir = "tabler-icons" # "tabler-icons" (default) - -# SEO (@thulite/seo) + headerBar = false + backgroundDots = false + + sectionFooter = false + relatedPosts = false + imageList = false + imageSingle = true + + editPage = false + lastMod = false + repoHost = "GitHub" + docsRepo = "https://github.com/Monynha-Softwares/facodi.pt" + docsRepoBranch = "main" + docsRepoSubPath = "" + + krokiURL = "https://kroki.io" + +[supabase] + url = "" + anon_key = "" + [seo] [seo.title] separator = " | " suffix = "" - [seo.favicons] - sizes = [] - icon = "favicon.png" # favicon.png (default) - svgIcon = "favicon.svg" # favicon.svg (default) - maskIcon = "mask-icon.svg" # mask-icon.svg (default) - maskIconColor = "white" # white (default) [seo.schemas] - type = "Organization" # Organization (default) or Person - logo = "favicon-512x512.png" # Logo of Organization — favicon-512x512.png (default) - name = "Thulite" # Name of Organization or Person - sameAs = [] # E.g. ["https://github.com/thuliteio/thulite", "https://fosstodon.org/@thulite"] - images = ["cover.png"] # ["cover.png"] (default) - article = [] # Article sections - newsArticle = [] # NewsArticle sections - blogPosting = ["blog"] # BlogPosting sections - product = [] # Product sections + type = "Organization" diff --git a/content/_index.md b/content/_index.md index 6e64711d..437dbc9c 100644 --- a/content/_index.md +++ b/content/_index.md @@ -1,13 +1,9 @@ --- -title: "Welcome to Doks" -description: "" -lead: "Congrats on setting up a new Doks project!" -date: 2023-09-07T16:33:54+02:00 -lastmod: 2023-09-07T16:33:54+02:00 -draft: false -seo: - title: "Welcome to Doks" # custom title (optional) - description: "" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - noindex: false # false (default) or true +title: "Catálogo FACODI" +lead: "Cursos gratuitos inspirados nos planos curriculares da Universidade do Algarve." +layout: home --- + +A FACODI é uma iniciativa comunitária que organiza cursos abertos com base nos planos oficiais da UAlg. + +Explore os cursos disponíveis e mergulhe nas unidades curriculares para acessar ementas, resultados de aprendizagem e playlists recomendadas. diff --git a/content/blog/_index.md b/content/blog/_index.md deleted file mode 100644 index 4cd6946e..00000000 --- a/content/blog/_index.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: "Blog" -description: "" -summary: "" -date: 2023-09-07T16:21:44+02:00 -lastmod: 2023-09-07T16:21:44+02:00 -draft: false -weight: 50 -categories: [] -tags: [] -contributors: [] -pinned: false -homepage: false -seo: - title: "" # custom title (optional) - description: "" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - noindex: false # false (default) or true ---- diff --git a/content/blog/example/index.md b/content/blog/example/index.md deleted file mode 100644 index c3ccae4a..00000000 --- a/content/blog/example/index.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: "Example Post" -description: "Just an example post." -summary: "You can use blog posts for announcing product updates and features." -date: 2023-09-07T16:27:22+02:00 -lastmod: 2023-09-07T16:27:22+02:00 -draft: false -weight: 50 -categories: [] -tags: [] -contributors: [] -pinned: false -homepage: false -seo: - title: "" # custom title (optional) - description: "" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - noindex: false # false (default) or true ---- - -Well-thought-through product announcements will help increase feature awareness and engage users with new functionality. Just like [sharing your public roadmap](https://canny.io/blog/should-you-have-a-public-roadmap/), it's also a great way to let potential customers see that you're constantly improving. - -## Further reading - -- Read [How to announce product updates and features](https://canny.io/blog/announce-product-updates-features/) diff --git a/content/courses/LESTI/2024-2025/index.md b/content/courses/LESTI/2024-2025/index.md new file mode 100644 index 00000000..70ecbeda --- /dev/null +++ b/content/courses/LESTI/2024-2025/index.md @@ -0,0 +1,23 @@ +--- +title: "Licenciatura em Engenharia de Sistemas e Tecnologias da Informação" +code: "LESTI" +degree: "bachelor" +ects_total: 180 +duration_semesters: 6 +plan_version: "2024/2025" +institution: "Universidade do Algarve" +school: "Escola Superior de Tecnologia" +language: "pt" +summary: "Curso voltado para o desenvolvimento de soluções digitais, integrando fundamentos de engenharia, programação e sistemas de informação." +seo: + title: "LESTI — Plano 2024/2025" + description: "Plano curricular, unidades curriculares e recursos multimédia." +type: course +layout: course +--- + +## Visão Geral + +A licenciatura em Engenharia de Sistemas e Tecnologias da Informação (LESTI) combina uma base sólida em matemática e programação com disciplinas voltadas para a análise, conceção e implementação de sistemas de informação. + +O plano 2024/2025 reflete as competências necessárias para atuar em equipas multidisciplinares, com forte ênfase na resolução de problemas reais através de tecnologia aberta e acessível. diff --git a/content/courses/LESTI/2024-2025/uc/LESTI-ALG1/estruturas-de-dados.md b/content/courses/LESTI/2024-2025/uc/LESTI-ALG1/estruturas-de-dados.md new file mode 100644 index 00000000..14ee9896 --- /dev/null +++ b/content/courses/LESTI/2024-2025/uc/LESTI-ALG1/estruturas-de-dados.md @@ -0,0 +1,18 @@ +--- +slug: "estruturas-de-dados" +title: "Estruturas de Dados" +summary: "Conceitos fundamentais de armazenamento e manipulação eficiente de dados." +youtube_playlists: + - id: "PLtopicosEstruturas" + priority: 1 +tags: + - "estruturas" + - "listas" + - "pilhas" +type: topic +layout: topic +--- + +As estruturas de dados definem como a informação é organizada na memória e determinam o desempenho dos algoritmos que as manipulam. + +Neste tópico abordamos listas ligadas, pilhas, filas, árvores e mapas, destacando cenários de aplicação e implicações na complexidade temporal. diff --git a/content/courses/LESTI/2024-2025/uc/LESTI-ALG1/index.md b/content/courses/LESTI/2024-2025/uc/LESTI-ALG1/index.md new file mode 100644 index 00000000..943258b0 --- /dev/null +++ b/content/courses/LESTI/2024-2025/uc/LESTI-ALG1/index.md @@ -0,0 +1,35 @@ +--- +title: "Algoritmos e Estruturas de Dados I" +code: "LESTI-ALG1" +course_code: "LESTI" +description: "Introdução aos fundamentos de algoritmos, análise de complexidade e estruturas de dados lineares." +ects: 6 +semester: 1 +language: "pt" +prerequisites: + - "LESTI-MAT1" +learning_outcomes: + - "Compreender a análise de complexidade de algoritmos." + - "Implementar estruturas de dados lineares em pseudocódigo e numa linguagem de programação." + - "Selecionar a estrutura de dados adequada para resolver um problema." +youtube_playlists: + - id: "PLlestiALG1Aulas" + priority: 1 + - id: "PLlestiALG1Exercicios" + priority: 2 +topics: + - slug: "estruturas-de-dados" +type: uc +layout: uc +--- + +## Ementa + +* Conceitos básicos de algoritmos e notação assintótica. +* Estruturas de dados lineares: arrays, listas ligadas, pilhas e filas. +* Introdução a árvores e tabelas de dispersão. +* Abordagens iterativas e recursivas para resolução de problemas. + +## Metodologia + +A unidade curricular combina aulas teóricas com laboratórios práticos apoiados por playlists abertas, priorizando materiais em português. Trabalhos orientados e desafios semanais reforçam a aprendizagem contínua. diff --git a/content/docs/_index.md b/content/docs/_index.md deleted file mode 100644 index 5ec013c8..00000000 --- a/content/docs/_index.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Docs" -description: "" -summary: "" -date: 2023-09-07T16:12:03+02:00 -lastmod: 2023-09-07T16:12:03+02:00 -draft: false -weight: 999 -toc: true -seo: - title: "" # custom title (optional) - description: "" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - noindex: false # false (default) or true ---- diff --git a/content/docs/guides/_index.md b/content/docs/guides/_index.md deleted file mode 100644 index 2c26cb64..00000000 --- a/content/docs/guides/_index.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Guides" -description: "" -summary: "" -date: 2023-09-07T16:06:50+02:00 -lastmod: 2023-09-07T16:06:50+02:00 -draft: false -weight: 800 -toc: true -seo: - title: "" # custom title (optional) - description: "" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - noindex: false # false (default) or true ---- diff --git a/content/docs/guides/example.md b/content/docs/guides/example.md deleted file mode 100644 index 3258d103..00000000 --- a/content/docs/guides/example.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: "Example Guide" -description: "Guides lead a user through a specific task they want to accomplish, often with a sequence of steps." -summary: "" -date: 2023-09-07T16:04:48+02:00 -lastmod: 2023-09-07T16:04:48+02:00 -draft: false -weight: 810 -toc: true -seo: - title: "" # custom title (optional) - description: "" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - noindex: false # false (default) or true ---- - -Guides lead a user through a specific task they want to accomplish, often with a sequence of steps. Writing a good guide requires thinking about what your users are trying to do. - -## Further reading - -- Read [about how-to guides](https://diataxis.fr/how-to-guides/) in the Diátaxis framework diff --git a/content/docs/reference/_index.md b/content/docs/reference/_index.md deleted file mode 100644 index 15e9941e..00000000 --- a/content/docs/reference/_index.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: "Reference" -description: "" -summary: "" -date: 2023-09-07T16:12:37+02:00 -lastmod: 2023-09-07T16:12:37+02:00 -draft: false -weight: 900 -toc: true -sidebar: - collapsed: true -seo: - title: "" # custom title (optional) - description: "" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - noindex: false # false (default) or true ---- diff --git a/content/docs/reference/example.md b/content/docs/reference/example.md deleted file mode 100644 index fe5d9b69..00000000 --- a/content/docs/reference/example.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: "Example Reference" -description: "Reference pages are ideal for outlining how things work in terse and clear terms." -summary: "" -date: 2023-09-07T16:13:18+02:00 -lastmod: 2023-09-07T16:13:18+02:00 -draft: false -weight: 910 -toc: true -seo: - title: "" # custom title (optional) - description: "" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - noindex: false # false (default) or true ---- - -Reference pages are ideal for outlining how things work in terse and clear terms. Less concerned with telling a story or addressing a specific use case, they should give a comprehensive outline of what your documenting. - -## Further reading - -- Read [about reference](https://diataxis.fr/reference/) in the Diátaxis framework diff --git a/content/docs/resources.md b/content/docs/resources.md deleted file mode 100644 index a5dac925..00000000 --- a/content/docs/resources.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: "Resources" -description: "" -summary: "" -date: 2024-02-27T09:30:56+01:00 -lastmod: 2024-02-27T09:30:56+01:00 -draft: false -weight: 999 -toc: true -seo: - title: "" # custom title (optional) - description: "" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - noindex: false # false (default) or true ---- - -Link to valuable, relevant resources. diff --git a/content/en/_index.md b/content/en/_index.md new file mode 100644 index 00000000..41521c1f --- /dev/null +++ b/content/en/_index.md @@ -0,0 +1,7 @@ +--- +title: "FACODI Catalog" +layout: home +draft: true +--- + +FACODI content is primarily authored in Portuguese. English translations will be published as they become available. diff --git a/content/privacy.md b/content/privacy.md deleted file mode 100644 index 178d9c15..00000000 --- a/content/privacy.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: "Privacy Policy" -description: "" -summary: "" -date: 2023-09-07T17:19:07+02:00 -lastmod: 2023-09-07T17:19:07+02:00 -draft: false -type: "legal" -seo: - title: "" # custom title (optional) - description: "" # custom description (recommended) - canonical: "" # custom canonical URL (optional) - noindex: false # false (default) or true ---- diff --git a/hugo_stats.json b/hugo_stats.json index 4b9a90e3..5176c09e 100644 --- a/hugo_stats.json +++ b/hugo_stats.json @@ -7,27 +7,21 @@ "base", "body", "button", - "circle", - "code", - "details", + "dd", "div", + "dl", + "dt", "footer", - "form", - "g", "h1", "h2", - "h3", "h5", "head", "header", + "hr", "html", - "input", - "kbd", - "label", "li", "line", "link", - "main", "meta", "nav", "noscript", @@ -35,131 +29,99 @@ "path", "script", "section", - "small", "span", - "strong", "style", - "summary", "svg", - "template", - "time", "title", "ul" ], "classes": [ - "DocSearch-Label", - "active", - "anchor", - "bg-dots", - "blog", - "blog-header", + "bg-light-subtle", + "border", + "border-0", "btn", - "btn-close", - "btn-cta", - "btn-lg", + "btn-dropdown", "btn-link", "btn-primary", "card", "card-body", "card-list", - "categories", + "col-12", + "col-5", + "col-7", "col-lg-10", - "col-lg-11", - "col-lg-12", - "col-lg-5", + "col-lg-4", "col-lg-8", - "col-lg-9", "col-md-12", - "col-xl-3", - "col-xl-4", + "col-md-6", "col-xl-8", - "col-xl-9", "container", - "container-fluid", "container-lg", "content", - "contributors", - "created-date", + "courses", + "current", "d-flex", - "d-lg-block", + "d-inline-flex", "d-lg-none", - "d-md-block", - "d-md-none", - "d-none", - "d-xl-block", - "d-xl-none", - "docs", - "docs-content", - "docs-links", - "docs-sidebar", - "docs-toc", + "display-5", + "display-6", "doks-sidebar", + "dropdown", + "dropdown-caret", + "dropdown-divider", + "dropdown-item", + "dropdown-menu", + "dropdown-menu-lg-end", + "dropdown-toggle", "error404", + "fixed-bottom-right", "flex-column", "flex-grow-1", "flex-lg-row", - "flex-md-row", - "flex-row", - "flex-sm-row", - "flex-xl-nowrap", "footer", - "form-control", - "form-control-lg", - "fs-5", + "fw-semibold", + "g-3", + "g-4", + "g-5", + "h-100", "h-auto", - "h3", "h4", "h5", "home", "icon", "icon-tabler", - "icon-tabler-arrow-left", - "icon-tabler-arrow-right", - "icon-tabler-brand-github", - "icon-tabler-clock", + "icon-tabler-chevron-down", + "icon-tabler-chevron-up", "icon-tabler-dots-vertical", + "icon-tabler-language", "icon-tabler-menu", "icon-tabler-moon", - "icon-tabler-search", "icon-tabler-sun", "icon-tabler-x", "justify-content-between", "justify-content-center", - "justify-content-end", - "justify-content-start", "lead", "list", "list-inline", "list-inline-item", - "list-nested", - "list-unstyled", - "list-view", - "m-2", "mb-0", + "mb-2", + "mb-3", + "mb-4", + "mb-5", "me-2", "me-auto", "me-lg-1", + "me-lg-2", "me-lg-3", - "message", - "modal", - "modal-body", - "modal-content", - "modal-dialog", - "modal-dialog-scrollable", - "modal-footer", - "modal-fullscreen-md-down", - "modal-header", - "modal-title", - "ms-2", - "ms-3", "ms-auto", "ms-lg-2", + "mt-1", "mt-3", "mt-n3", "mx-2", "mx-auto", "my-3", - "nav", "nav-item", "nav-link", "navbar", @@ -173,75 +135,48 @@ "offcanvas-start", "offcanvas-title", "order-3", - "order-lg-4", + "order-lg-2", "p-0", "p-2", - "page-footer-meta", - "page-links", - "page-nav", - "pb-3", - "position-relative", - "privacy", - "px-0", - "query-no-results", - "reading-time", - "rounded-pill", + "p-4", + "pb-4", + "pe-4", + "py-5", + "rounded", + "rounded-3", + "rounded-circle", "row", - "search-form", - "search-input", - "search-loading", - "search-no-recent", - "search-no-results", - "search-result", - "search-results", - "search-text", "section", - "section-features", - "section-md", - "section-nav", + "shadow", + "shadow-sm", "single", - "social-link", - "status", + "small", "sticky-top", "stretched-link", - "submitted", "tags", "taxonomy", - "text-body", - "text-body-secondary", "text-center", "text-decoration-none", - "text-end", "text-lg-end", "text-lg-start", "text-muted", - "text-nowrap", - "text-reset", - "title", - "title-submitted", - "toc-mobile", + "text-start", + "text-uppercase", "visually-hidden", - "w-100", "wrap" ], "ids": [ - "TableOfContents", "buttonColorMode", + "course-content-md", "doks-docs-nav", - "further-reading", + "doks-language-current", + "doks-languages", "offcanvasNavMain", "offcanvasNavMainLabel", "offcanvasNavSection", "offcanvasNavSectionLabel", - "query", - "search-form", - "searchModal", - "searchModalLabel", - "searchResults", - "searchToggleDesktop", - "searchToggleMobile", - "socialMenu", - "toc" + "toTop", + "visão-geral" ] } } diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100644 index 00000000..a98637f9 --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,22 @@ + + + {{ partial "head/head" . }} + {{ partial "head/body-class" . }} + + {{ partial "header/header" . }} +
+
+ {{ if and (eq site.Params.doks.containerBreakpoint "fluid") (or (not (in .Site.Params.mainSections .Type)) (.IsNode)) }}
{{ end }} + {{ block "main" . }}{{ end }} + {{ if and (eq site.Params.doks.containerBreakpoint "fluid") (or (not (in .Site.Params.mainSections .Type)) (.IsNode)) }}
{{ end }} +
+
+ {{ block "sidebar-prefooter" . }}{{ end }} + {{ block "sidebar-footer" . }}{{ end }} + {{ partial "footer/footer" . }} + {{ partial "footer/script-footer" . }} + {{ if eq site.Params.doks.toTopButton true -}} + {{ partial "footer/to-top" . }} + {{ end }} + + diff --git a/layouts/_partials/footer/script-footer-custom.html b/layouts/_partials/footer/script-footer-custom.html index d570e83e..9a1de999 100644 --- a/layouts/_partials/footer/script-footer-custom.html +++ b/layouts/_partials/footer/script-footer-custom.html @@ -1,13 +1,10 @@ -{{/* Put your custom tags here */}} - -{{/* EXAMPLE - only load script for production -{{ if eq (hugo.Environment) "production" -}} - {{ partial "footer/esbuild" (dict "src" "js/instantpage.js" "load" "async" "transpile" false) -}} -{{ end -}} -*/}} - -{{/* EXAMPLE - only load script for a page type e.g. contact or gallery -{{ if eq .Type "gallery" -}} - {{ partial "footer/esbuild" (dict "src" "js/gallery.js" "load" "async" "transpile" false) -}} -{{ end -}} -*/}} +{{- $supabaseUrl := site.Params.supabase.url | default (getenv "SUPABASE_URL") -}} +{{- $supabaseAnon := site.Params.supabase.anon_key | default (getenv "SUPABASE_ANON_KEY") -}} + + + + + diff --git a/layouts/course/single.html b/layouts/course/single.html new file mode 100644 index 00000000..b2b9844f --- /dev/null +++ b/layouts/course/single.html @@ -0,0 +1,55 @@ +{{ define "main" }} +
+
+
+
+

Curso {{ .Params.code }}

+

{{ .Title }}

+ {{ with .Params.plan_version }} +

Plano curricular {{ . }}

+ {{ end }} +
+
+ {{ .Content }} +
+
+

Conteúdo sincronizado

+
+

Carregando descrição detalhada a partir do Supabase…

+
+
+
+

Unidades curriculares

+
+
Carregando unidades curriculares…
+
+
+
+
+ +
+
+
+{{ end }} + +{{ define "body-attrs" -}} +{{- $code := .Params.code | default "" -}} +{{- $plan := .Params.plan_version | default "" -}} +{{- printf " data-page=\"course\" data-course=\"%s\" data-plan=\"%s\"" $code $plan | safeHTMLAttr -}} +{{- end }} diff --git a/layouts/home.html b/layouts/home.html index d95a4a74..f7e26d28 100644 --- a/layouts/home.html +++ b/layouts/home.html @@ -1,59 +1,52 @@ {{ define "main" }} -
-
-
-

{{ .Title }}

-
-
-

{{ .Params.lead | safeHTML }}

- {{ i18n "get_started" }} - {{ .Content }} +
+
+
+

{{ .Title }}

+ {{ with .Params.lead }} +

{{ . }}

+ {{ end }} + {{ with .Content }} +
+ {{ . }} +
+ {{ end }}
-
-{{ end }} - -{{ define "sidebar-prefooter" }} - {{ if site.Params.doks.backgroundDots -}} -
-
-
- {{ end -}} - {{ if eq $.Site.Language.LanguageName "English" }} -
-
-
-
-

Update content

-

Edit content/_index.md to see this page change.

-
-
-

Add new content

-

Add Markdown files to content to create new pages.

-
-
-

Configure your site

-

Edit your config in config/_default/params.toml.

-
-
-

Read the docs

-

Learn more in the Docs.

-
+
+ {{ $courses := where site.RegularPages "Type" "course" }} + {{ if gt (len $courses) 0 }} + {{ range $courses }} +
+
+
+

+ + {{ .Params.code }} · {{ .Title }} + +

+

+ {{ with .Params.plan_version }}Plano {{ . }}{{ end }} + {{ with .Params.ects_total }}{{ . }} ECTS{{ end }} + {{ with .Params.duration_semesters }}{{ . }} semestres{{ end }} +

+ {{ with .Params.summary }} +

{{ . }}

+ {{ else }} +

Documentação em preparação.

+ {{ end }} +
+
-
-
- {{ end }} -{{ end }} - -{{ define "sidebar-footer" }} -{{ if site.Params.doks.sectionFooter -}} -
-
-
-

Start building with Doks today

- {{ i18n "get-started" }} -
+ {{ end }} + {{ else }} +
+

Nenhum curso publicado ainda. Volte em breve!

+
+ {{ end }}
-{{ end -}} {{ end }} + +{{ define "sidebar-prefooter" }}{{ end }} +{{ define "sidebar-footer" }}{{ end }} diff --git a/layouts/topic/single.html b/layouts/topic/single.html new file mode 100644 index 00000000..dabb4d43 --- /dev/null +++ b/layouts/topic/single.html @@ -0,0 +1,38 @@ +{{ define "main" }} +
+
+
+
+

Tópico {{ .Params.slug }}

+

{{ .Title }}

+
+
+ {{ .Content }} +
+
+

Conteúdo sincronizado

+
+

Carregando conteúdo Markdown…

+
+
+
+

Playlists relacionadas

+
+
Carregando playlists…
+
+
+
+

Etiquetas

+
+ Carregando tags… +
+
+
+
+
+{{ end }} + +{{ define "body-attrs" -}} +{{- $slug := .Params.slug | default "" -}} +{{- printf " data-page=\"topic\" data-topic=\"%s\"" $slug | safeHTMLAttr -}} +{{- end }} diff --git a/layouts/uc/single.html b/layouts/uc/single.html new file mode 100644 index 00000000..4036e83c --- /dev/null +++ b/layouts/uc/single.html @@ -0,0 +1,63 @@ +{{ define "main" }} +
+
+
+
+

Unidade curricular {{ .Params.code }}

+

{{ .Title }}

+
+
+ {{ .Content }} +
+
+

Conteúdo oficial

+
+

Sincronizando ementa e materiais…

+
+
+
+

Resultados de aprendizagem

+
    +
  1. Carregando resultados…
  2. +
+
+
+

Playlists recomendadas

+
+
Carregando playlists…
+
+
+
+

Tópicos associados

+
+
Carregando tópicos…
+
+
+
+
+ +
+
+
+{{ end }} + +{{ define "body-attrs" -}} +{{- $code := .Params.code | default "" -}} +{{- printf " data-page=\"uc\" data-uc=\"%s\"" $code | safeHTMLAttr -}} +{{- end }} diff --git a/package-lock.json b/package-lock.json index 35f14326..f4569d00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,22 @@ { - "name": "monadocs", - "version": "1.8.0", + "name": "facodi-site", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "monadocs", - "version": "1.8.0", + "name": "facodi-site", + "version": "1.0.0", "hasInstallScript": true, "license": "MIT", "dependencies": { + "@supabase/supabase-js": "^2.48.0", "@tabler/icons": "^3.34.1", "@thulite/doks-core": "^1.8.3", "@thulite/images": "^3.3.1", "@thulite/inline-svg": "^1.2.0", "@thulite/seo": "^2.4.1", + "gray-matter": "^4.0.3", "thulite": "^2.6.3" }, "devDependencies": { @@ -2902,6 +2904,80 @@ "win32" ] }, + "node_modules/@supabase/auth-js": { + "version": "2.71.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz", + "integrity": "sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.6.tgz", + "integrity": "sha512-bhjZ7rmxAibjgmzTmQBxJU6ZIBCCJTc3Uwgvdi4FewueUTAGO5hxZT1Sj6tiD+0dSXf9XI87BDdJrg12z8Uaew==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.21.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.4.tgz", + "integrity": "sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.15.5", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.5.tgz", + "integrity": "sha512-/Rs5Vqu9jejRD8ZeuaWXebdkH+J7V6VySbCZ/zQM93Ta5y3mAmocjioa/nzlB6qvFmyylUgKVS1KpE212t30OA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.13", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.12.1.tgz", + "integrity": "sha512-QWg3HV6Db2J81VQx0PqLq0JDBn4Q8B1FYn1kYcbla8+d5WDmTdwwMr+EJAxNOSs9W4mhKMv+EYCpCrTFlTj4VQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.57.4", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.57.4.tgz", + "integrity": "sha512-LcbTzFhHYdwfQ7TRPfol0z04rLEyHabpGYANME6wkQ/kLtKNmI+Vy+WEM8HxeOZAtByUFxoUTTLwhXmrh+CcVw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.71.1", + "@supabase/functions-js": "2.4.6", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.21.4", + "@supabase/realtime-js": "2.15.5", + "@supabase/storage-js": "2.12.1" + } + }, "node_modules/@tabler/icons": { "version": "3.34.1", "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.34.1.tgz", @@ -2999,14 +3075,26 @@ "version": "24.3.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "undici-types": "~7.10.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -3058,7 +3146,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" @@ -3610,7 +3697,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -3629,6 +3715,18 @@ "node": ">=0.10.0" } }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/extendable-error": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", @@ -3976,6 +4074,21 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4074,6 +4187,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4167,7 +4289,6 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -4211,6 +4332,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/lazysizes": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/lazysizes/-/lazysizes-5.3.2.tgz", @@ -5246,6 +5376,19 @@ "node": ">=6.0.0" } }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/select": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", @@ -5338,7 +5481,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/string-width": { @@ -5405,6 +5547,15 @@ "node": ">=4" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -5524,17 +5675,13 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, "license": "MIT" }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -5732,14 +5879,12 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -5808,6 +5953,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 134e4fc8..0d9544b8 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,27 @@ { - "name": "monadocs", - "version": "1.8.0", - "description": "Doks theme", - "author": "Thulite", + "name": "facodi-site", + "version": "1.0.0", + "description": "FACODI static site powered by Hugo and Supabase", "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/thuliteio/doks.git" + "url": "https://github.com/Monynha-Softwares/facodi.pt" }, "scripts": { - "create": "hugo new", - "dev": "hugo server --disableFastRender --noHTTPCache", - "start": "hugo server --disableFastRender --noHTTPCache --port 3000", - "format": "prettier **/** -w -c", - "build": "hugo --minify --gc", - "preview": "vite preview --outDir public", + "dev": "hugo server -D", + "build": "hugo", + "lint:content": "node scripts/validate-frontmatter.js", + "sync:content": "node scripts/sync-content.mjs", "postinstall": "bash scripts/install-hugo.sh" }, "dependencies": { + "@supabase/supabase-js": "^2.48.0", "@thulite/doks-core": "^1.8.3", "@thulite/images": "^3.3.1", "@thulite/inline-svg": "^1.2.0", "@thulite/seo": "^2.4.1", "@tabler/icons": "^3.34.1", + "gray-matter": "^4.0.3", "thulite": "^2.6.3" }, "devDependencies": { @@ -33,9 +32,5 @@ }, "engines": { "node": ">=20.11.0" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" } } diff --git a/scripts/sync-content.mjs b/scripts/sync-content.mjs new file mode 100755 index 00000000..93c6dcd8 --- /dev/null +++ b/scripts/sync-content.mjs @@ -0,0 +1,253 @@ +#!/usr/bin/env node + +import { promises as fs } from 'fs'; +import path from 'path'; +import matter from 'gray-matter'; +import { createClient } from '@supabase/supabase-js'; + +const CONTENT_ROOT = path.resolve('content'); +const SUPABASE_URL = process.env.SUPABASE_URL; +const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY; + +if (!SUPABASE_URL || !SUPABASE_SERVICE_KEY) { + console.error('❌ Defina SUPABASE_URL e SUPABASE_SERVICE_KEY para sincronizar o conteúdo.'); + process.exit(1); +} + +const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY, { + auth: { + persistSession: false, + }, +}); + +const courseRecords = []; +const courseContents = []; +const ucRecords = []; +const ucContents = []; +const ucOutcomes = []; +const ucPlaylists = []; +const ucTopics = []; +const topicRecords = []; +const topicContents = []; +const topicTags = []; +const topicPlaylists = []; + +function toPosix(p) { + return p.split(path.sep).join('/'); +} + +async function walk(dir) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + await walk(fullPath); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + await parseMarkdown(fullPath); + } + } +} + +function normalizePlanFromDir(dirName) { + if (!dirName) return dirName; + if (dirName.includes('/')) return dirName; + return dirName.replace('-', '/'); +} + +async function parseMarkdown(fullPath) { + const relPath = toPosix(path.relative(CONTENT_ROOT, fullPath)); + if (!relPath || relPath.startsWith('en/')) { + return; + } + + const raw = await fs.readFile(fullPath, 'utf8'); + const parsed = matter(raw); + const data = parsed.data || {}; + const content = parsed.content.trim(); + + if (/^courses\/[^/]+\/[^/]+\/index\.md$/i.test(relPath)) { + const segments = relPath.split('/'); + const codeFromPath = segments[1]; + const planDir = segments[2]; + const record = { + code: data.code || codeFromPath, + plan_version: data.plan_version || normalizePlanFromDir(planDir), + name: data.title || '', + degree: data.degree || '', + ects_total: data.ects_total ?? null, + duration_semesters: data.duration_semesters ?? null, + institution: data.institution || null, + school: data.school || null, + language: data.language || null, + summary: data.summary || null, + }; + courseRecords.push(record); + courseContents.push({ + course_code: record.code, + plan_version: record.plan_version, + content_md: content, + }); + } else if (/^courses\/[^/]+\/[^/]+\/uc\/[^/]+\/index\.md$/i.test(relPath)) { + const segments = relPath.split('/'); + const courseCodeFromPath = segments[1]; + const planDir = segments[2]; + const ucCodeFromPath = segments[4]; + const planVersion = normalizePlanFromDir(data.plan_version || planDir); + const courseCode = data.course_code || courseCodeFromPath; + + ucRecords.push({ + code: data.code || ucCodeFromPath, + course_code: courseCode, + course_plan_version: planVersion, + name: data.title || '', + description: data.description || null, + ects: data.ects ?? null, + semester: data.semester ?? null, + language: data.language || null, + prerequisites: Array.isArray(data.prerequisites) ? data.prerequisites : [], + }); + + ucContents.push({ + uc_code: data.code || ucCodeFromPath, + content_md: content, + }); + + if (Array.isArray(data.learning_outcomes)) { + data.learning_outcomes.forEach((outcome, index) => { + if (typeof outcome === 'string' && outcome.trim().length > 0) { + ucOutcomes.push({ + uc_code: data.code || ucCodeFromPath, + outcome, + order: index + 1, + }); + } + }); + } + + if (Array.isArray(data.youtube_playlists)) { + data.youtube_playlists.forEach((playlist) => { + if (playlist && playlist.id) { + ucPlaylists.push({ + uc_code: data.code || ucCodeFromPath, + playlist_id: playlist.id, + priority: playlist.priority ?? null, + }); + } + }); + } + + if (Array.isArray(data.topics)) { + data.topics.forEach((topic) => { + const slug = typeof topic === 'string' ? topic : topic && topic.slug; + if (slug) { + ucTopics.push({ + uc_code: data.code || ucCodeFromPath, + topic_slug: slug, + }); + } + }); + } + } else if (/^courses\/[^/]+\/[^/]+\/uc\/[^/]+\/[^/]+\.md$/i.test(relPath)) { + const fileName = path.basename(relPath); + if (fileName.toLowerCase() === 'index.md') { + return; + } + const segments = relPath.split('/'); + const ucCodeFromPath = segments[4]; + const slugFromPath = fileName.replace(/\.md$/, ''); + + const slug = data.slug || slugFromPath; + topicRecords.push({ + slug, + name: data.title || slug, + summary: data.summary || null, + }); + + topicContents.push({ + topic_slug: slug, + content_md: content, + }); + + if (Array.isArray(data.tags)) { + data.tags.forEach((tag) => { + if (typeof tag === 'string' && tag.trim().length > 0) { + topicTags.push({ topic_slug: slug, tag }); + } + }); + } + + if (Array.isArray(data.youtube_playlists)) { + data.youtube_playlists.forEach((playlist) => { + if (playlist && playlist.id) { + topicPlaylists.push({ + topic_slug: slug, + playlist_id: playlist.id, + priority: playlist.priority ?? null, + }); + } + }); + } + } +} + +async function upsert(table, rows, options = {}) { + if (!rows.length) return; + const { error } = await supabase.from(table).upsert(rows, options); + if (error) { + throw new Error(`Erro ao sincronizar ${table}: ${error.message}`); + } +} + +async function insert(table, rows) { + if (!rows.length) return; + const { error } = await supabase.from(table).insert(rows); + if (error) { + throw new Error(`Erro ao inserir em ${table}: ${error.message}`); + } +} + +async function purge(table, column, values) { + if (!values.length) return; + for (const value of values) { + const { error } = await supabase.from(table).delete().eq(column, value); + if (error) { + throw new Error(`Erro ao limpar ${table} (${column}=${value}): ${error.message}`); + } + } +} + +async function main() { + await walk(CONTENT_ROOT); + + console.log('🔄 Iniciando sincronização com Supabase…'); + + await upsert('catalog.course', courseRecords, { onConflict: 'code,plan_version' }); + await upsert('catalog.course_content', courseContents, { onConflict: 'course_code,plan_version' }); + await upsert('catalog.uc', ucRecords, { onConflict: 'code' }); + await upsert('catalog.uc_content', ucContents, { onConflict: 'uc_code' }); + await upsert('subjects.topic', topicRecords, { onConflict: 'slug' }); + await upsert('subjects.topic_content', topicContents, { onConflict: 'topic_slug' }); + + const ucCodes = [...new Set(ucRecords.map((uc) => uc.code))]; + await purge('catalog.uc_learning_outcome', 'uc_code', ucCodes); + await purge('mapping.uc_playlist', 'uc_code', ucCodes); + await purge('mapping.uc_topic', 'uc_code', ucCodes); + + await insert('catalog.uc_learning_outcome', ucOutcomes); + await insert('mapping.uc_playlist', ucPlaylists); + await insert('mapping.uc_topic', ucTopics); + + const topicSlugs = [...new Set(topicRecords.map((topic) => topic.slug))]; + await purge('subjects.topic_tag', 'topic_slug', topicSlugs); + await purge('mapping.topic_playlist', 'topic_slug', topicSlugs); + + await insert('subjects.topic_tag', topicTags); + await insert('mapping.topic_playlist', topicPlaylists); + + console.log('✅ Sincronização concluída com sucesso.'); +} + +main().catch((err) => { + console.error('❌ Falha na sincronização:', err.message); + process.exit(1); +}); diff --git a/scripts/validate-frontmatter.js b/scripts/validate-frontmatter.js new file mode 100755 index 00000000..30673eb8 --- /dev/null +++ b/scripts/validate-frontmatter.js @@ -0,0 +1,169 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const matter = require('gray-matter'); + +const CONTENT_DIR = path.resolve(process.cwd(), 'content'); +const errors = []; + +function walk(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(fullPath); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + validateFile(fullPath); + } + } +} + +function toPosix(p) { + return p.split(path.sep).join('/'); +} + +function expectFields(data, required, filePath) { + required.forEach((field) => { + if (data[field] === undefined) { + errors.push(`${filePath}: campo obrigatório "${field}" ausente.`); + } + }); +} + +function isString(value) { + return typeof value === 'string' && value.trim().length > 0; +} + +function validateCourse(relPath, data, filePath) { + expectFields( + data, + [ + 'code', + 'title', + 'degree', + 'ects_total', + 'duration_semesters', + 'plan_version', + 'institution', + 'school', + 'language', + 'summary', + ], + filePath, + ); + + if (data.type && data.type !== 'course') { + errors.push(`${filePath}: valor "type" deve ser "course".`); + } + + if (data.ects_total !== undefined && Number.isNaN(Number(data.ects_total))) { + errors.push(`${filePath}: "ects_total" deve ser numérico.`); + } + if (data.duration_semesters !== undefined && Number.isNaN(Number(data.duration_semesters))) { + errors.push(`${filePath}: "duration_semesters" deve ser numérico.`); + } +} + +function validateUC(relPath, data, filePath) { + expectFields( + data, + [ + 'code', + 'title', + 'description', + 'ects', + 'semester', + 'language', + 'prerequisites', + 'learning_outcomes', + 'youtube_playlists', + 'topics', + ], + filePath, + ); + + if (data.type && data.type !== 'uc') { + errors.push(`${filePath}: valor "type" deve ser "uc".`); + } + + if (!Array.isArray(data.prerequisites)) { + errors.push(`${filePath}: "prerequisites" deve ser uma lista.`); + } + if (!Array.isArray(data.learning_outcomes)) { + errors.push(`${filePath}: "learning_outcomes" deve ser uma lista.`); + } + if (!Array.isArray(data.youtube_playlists)) { + errors.push(`${filePath}: "youtube_playlists" deve ser uma lista.`); + } else { + data.youtube_playlists.forEach((item, index) => { + if (!item || !isString(item.id)) { + errors.push(`${filePath}: playlist #${index + 1} deve possuir "id".`); + } + }); + } + if (!Array.isArray(data.topics)) { + errors.push(`${filePath}: "topics" deve ser uma lista.`); + } else { + data.topics.forEach((topic, index) => { + const slug = typeof topic === 'string' ? topic : topic && topic.slug; + if (!isString(slug)) { + errors.push(`${filePath}: tópico #${index + 1} precisa de um "slug" válido.`); + } + }); + } +} + +function validateTopic(relPath, data, filePath, fileName) { + expectFields(data, ['slug', 'title', 'summary', 'youtube_playlists', 'tags'], filePath); + + if (data.type && data.type !== 'topic') { + errors.push(`${filePath}: valor "type" deve ser "topic".`); + } + + if (!Array.isArray(data.youtube_playlists)) { + errors.push(`${filePath}: "youtube_playlists" deve ser uma lista.`); + } + if (!Array.isArray(data.tags)) { + errors.push(`${filePath}: "tags" deve ser uma lista.`); + } + + const expectedSlug = fileName.replace(/\.md$/, ''); + if (data.slug && data.slug !== expectedSlug) { + errors.push(`${filePath}: slug "${data.slug}" difere do nome do ficheiro "${expectedSlug}".`); + } +} + +function validateFile(fullPath) { + const relPath = toPosix(path.relative(CONTENT_DIR, fullPath)); + const filePath = toPosix(path.relative(process.cwd(), fullPath)); + + const raw = fs.readFileSync(fullPath, 'utf8'); + const parsed = matter(raw); + const data = parsed.data || {}; + + if (!relPath || relPath.startsWith('en/')) { + return; + } + + if (/^courses\/[^/]+\/[^/]+\/index\.md$/i.test(relPath)) { + validateCourse(relPath, data, filePath); + } else if (/^courses\/[^/]+\/[^/]+\/uc\/[^/]+\/index\.md$/i.test(relPath)) { + validateUC(relPath, data, filePath); + } else if (/^courses\/[^/]+\/[^/]+\/uc\/[^/]+\/[^/]+\.md$/i.test(relPath)) { + const fileName = path.basename(relPath); + if (fileName.toLowerCase() !== 'index.md') { + validateTopic(relPath, data, filePath, fileName); + } + } +} + +walk(CONTENT_DIR); + +if (errors.length > 0) { + console.error('❌ Erros de frontmatter encontrados:'); + errors.forEach((msg) => console.error(` - ${msg}`)); + process.exit(1); +} + +console.log('✅ Frontmatter válido para todos os conteúdos.'); diff --git a/static/js/loaders.js b/static/js/loaders.js new file mode 100644 index 00000000..0fe005d8 --- /dev/null +++ b/static/js/loaders.js @@ -0,0 +1,411 @@ +(function () { + function getClient() { + if (!window.facodi || !window.facodi.supabase) { + console.warn('[FACODI] Cliente Supabase indisponível.'); + return null; + } + return window.facodi.supabase; + } + + function renderMarkdown(md) { + if (!md) { + return ''; + } + if (window.marked && typeof window.marked.parse === 'function') { + return window.marked.parse(md); + } + const escaped = md + .replace(/&/g, '&') + .replace(//g, '>'); + return '

' + escaped.replace(/\n{2,}/g, '

').replace(/\n/g, '
') + '

'; + } + + function toText(value, fallback = '—') { + if (Array.isArray(value)) { + return value.length ? value.join(', ') : fallback; + } + if (value === null || value === undefined || value === '') { + return fallback; + } + return value; + } + + async function loadCoursePage(courseCode, planVersion) { + const client = getClient(); + if (!client) return; + + const detailsEl = document.querySelector('[data-course-details]'); + const contentEl = document.querySelector('[data-course-content]'); + const ucContainer = document.querySelector('[data-course-ucs]'); + + if (!detailsEl || !contentEl || !ucContainer) { + return; + } + + try { + const { data: course, error: courseError } = await client + .from('catalog.course') + .select('*') + .eq('code', courseCode) + .eq('plan_version', planVersion) + .maybeSingle(); + + if (courseError || !course) { + throw courseError || new Error('Curso não encontrado.'); + } + + detailsEl.innerHTML = ` +
Instituição
+
${toText(course.institution)}
+
Escola
+
${toText(course.school)}
+
ECTS
+
${toText(course.ects_total)}
+
Duração
+
${toText(course.duration_semesters)} semestres
+
Idioma
+
${toText(course.language)}
+ `; + + const { data: courseContent, error: contentError } = await client + .from('catalog.course_content') + .select('content_md') + .eq('course_code', courseCode) + .eq('plan_version', planVersion) + .maybeSingle(); + + if (contentError) { + throw contentError; + } + + if (courseContent && courseContent.content_md) { + contentEl.innerHTML = renderMarkdown(courseContent.content_md); + } else { + contentEl.innerHTML = '

Ainda não há conteúdo sincronizado para este curso.

'; + } + + const { data: ucList, error: ucError } = await client + .from('catalog.uc') + .select('code,name,description,ects,semester,language,course_plan_version') + .eq('course_code', courseCode) + .eq('course_plan_version', planVersion) + .order('semester', { ascending: true }) + .order('name', { ascending: true }); + + if (ucError) { + throw ucError; + } + + ucContainer.innerHTML = ''; + if (!ucList || ucList.length === 0) { + ucContainer.innerHTML = '
Nenhuma unidade curricular sincronizada.
'; + return; + } + + const basePath = window.location.pathname.endsWith('/') + ? window.location.pathname + : window.location.pathname + '/'; + + ucList.forEach((uc) => { + const col = document.createElement('div'); + col.className = 'col-md-6'; + const ucUrl = `${basePath}uc/${encodeURIComponent(uc.code)}/`; + col.innerHTML = ` +
+
+

+ ${uc.code} · ${uc.name} +

+

${toText(uc.semester)}º semestre · ${toText(uc.ects)} ECTS

+

${toText(uc.description, 'Descrição em preparação.')}

+
+
+ `; + ucContainer.appendChild(col); + }); + } catch (err) { + console.error('[FACODI] Erro ao carregar curso:', err); + detailsEl.innerHTML = '
Falha ao carregar detalhes do curso.
'; + contentEl.innerHTML = '

Não foi possível carregar o conteúdo sincronizado.

'; + ucContainer.innerHTML = '
Não foi possível carregar as unidades curriculares.
'; + } + } + + async function loadUCPage(ucCode) { + const client = getClient(); + if (!client) return; + + const detailsEl = document.querySelector('[data-uc-details]'); + const contentEl = document.querySelector('[data-uc-content]'); + const outcomesEl = document.querySelector('[data-uc-outcomes]'); + const playlistsEl = document.querySelector('[data-uc-playlists]'); + const topicsEl = document.querySelector('[data-uc-topics]'); + + if (!detailsEl || !contentEl || !outcomesEl || !playlistsEl || !topicsEl) { + return; + } + + try { + const { data: uc, error: ucError } = await client + .from('catalog.uc') + .select('*') + .eq('code', ucCode) + .maybeSingle(); + + if (ucError || !uc) { + throw ucError || new Error('UC não encontrada.'); + } + + detailsEl.innerHTML = ` +
Curso
+
${toText(uc.course_code)}
+
ECTS
+
${toText(uc.ects)}
+
Semestre
+
${toText(uc.semester)}
+
Idioma
+
${toText(uc.language)}
+
Pré-requisitos
+
${toText(uc.prerequisites)}
+ `; + + const { data: ucContent, error: ucContentError } = await client + .from('catalog.uc_content') + .select('content_md') + .eq('uc_code', ucCode) + .maybeSingle(); + + if (ucContentError) { + throw ucContentError; + } + + if (ucContent && ucContent.content_md) { + contentEl.innerHTML = renderMarkdown(ucContent.content_md); + } else { + contentEl.innerHTML = '

Conteúdo ainda não disponível.

'; + } + + const { data: outcomes, error: outcomesError } = await client + .from('catalog.uc_learning_outcome') + .select('outcome, order') + .eq('uc_code', ucCode) + .order('order', { ascending: true }); + + if (outcomesError) { + throw outcomesError; + } + + outcomesEl.innerHTML = ''; + if (!outcomes || outcomes.length === 0) { + outcomesEl.innerHTML = '
  • Nenhum resultado cadastrado.
  • '; + } else { + outcomes.forEach((item) => { + const li = document.createElement('li'); + li.className = 'list-group-item'; + li.textContent = item.outcome; + outcomesEl.appendChild(li); + }); + } + + const { data: playlists, error: playlistsError } = await client + .from('mapping.uc_playlist') + .select('playlist_id, priority') + .eq('uc_code', ucCode) + .order('priority', { ascending: true }); + + if (playlistsError) { + throw playlistsError; + } + + playlistsEl.innerHTML = ''; + if (!playlists || playlists.length === 0) { + playlistsEl.innerHTML = '
    Sem playlists associadas.
    '; + } else { + playlists.forEach((playlist) => { + const col = document.createElement('div'); + col.className = 'col-md-6'; + const playlistUrl = `https://www.youtube.com/playlist?list=${encodeURIComponent(playlist.playlist_id)}`; + col.innerHTML = ` + + `; + playlistsEl.appendChild(col); + }); + } + + const { data: topicLinks, error: topicsError } = await client + .from('mapping.uc_topic') + .select('topic_slug') + .eq('uc_code', ucCode); + + if (topicsError) { + throw topicsError; + } + + topicsEl.innerHTML = ''; + if (!topicLinks || topicLinks.length === 0) { + topicsEl.innerHTML = '
    Sem tópicos associados.
    '; + } else { + const slugs = topicLinks.map((item) => item.topic_slug); + const { data: topics, error: topicsDetailError } = await client + .from('subjects.topic') + .select('slug, name, summary') + .in('slug', slugs); + + if (topicsDetailError) { + throw topicsDetailError; + } + + const basePath = window.location.pathname.endsWith('/') + ? window.location.pathname + : window.location.pathname + '/'; + + topics.forEach((topic) => { + const col = document.createElement('div'); + col.className = 'col-md-6'; + const topicUrl = `${basePath}${encodeURIComponent(topic.slug)}/`; + col.innerHTML = ` +
    +
    +

    ${topic.name}

    +

    ${toText(topic.summary, 'Resumo em preparação.')}

    +
    +
    + `; + topicsEl.appendChild(col); + }); + } + } catch (err) { + console.error('[FACODI] Erro ao carregar UC:', err); + detailsEl.innerHTML = '
    Falha ao carregar informações da UC.
    '; + contentEl.innerHTML = '

    Não foi possível carregar a ementa sincronizada.

    '; + outcomesEl.innerHTML = '
  • Erro ao carregar resultados de aprendizagem.
  • '; + playlistsEl.innerHTML = '
    Erro ao carregar playlists.
    '; + topicsEl.innerHTML = '
    Erro ao carregar tópicos.
    '; + } + } + + async function loadTopicPage(topicSlug) { + const client = getClient(); + if (!client) return; + + const contentEl = document.querySelector('[data-topic-content]'); + const playlistsEl = document.querySelector('[data-topic-playlists]'); + const tagsEl = document.querySelector('[data-topic-tags]'); + + if (!contentEl || !playlistsEl || !tagsEl) { + return; + } + + try { + const { data: topic, error: topicError } = await client + .from('subjects.topic') + .select('*') + .eq('slug', topicSlug) + .maybeSingle(); + + if (topicError || !topic) { + throw topicError || new Error('Tópico não encontrado.'); + } + + const { data: topicContent, error: topicContentError } = await client + .from('subjects.topic_content') + .select('content_md') + .eq('topic_slug', topicSlug) + .maybeSingle(); + + if (topicContentError) { + throw topicContentError; + } + + const summaryHtml = topic.summary ? `

    ${topic.summary}

    ` : ''; + if (topicContent && topicContent.content_md) { + contentEl.innerHTML = summaryHtml + renderMarkdown(topicContent.content_md); + } else { + contentEl.innerHTML = summaryHtml || '

    Conteúdo em preparação.

    '; + } + + const { data: topicPlaylists, error: topicPlaylistsError } = await client + .from('mapping.topic_playlist') + .select('playlist_id, priority') + .eq('topic_slug', topicSlug) + .order('priority', { ascending: true }); + + if (topicPlaylistsError) { + throw topicPlaylistsError; + } + + playlistsEl.innerHTML = ''; + if (!topicPlaylists || topicPlaylists.length === 0) { + playlistsEl.innerHTML = '
    Sem playlists cadastradas.
    '; + } else { + topicPlaylists.forEach((playlist) => { + const col = document.createElement('div'); + col.className = 'col-md-6'; + const playlistUrl = `https://www.youtube.com/playlist?list=${encodeURIComponent(playlist.playlist_id)}`; + col.innerHTML = ` + + `; + playlistsEl.appendChild(col); + }); + } + + const { data: tags, error: tagsError } = await client + .from('subjects.topic_tag') + .select('tag') + .eq('topic_slug', topicSlug); + + if (tagsError) { + throw tagsError; + } + + tagsEl.innerHTML = ''; + if (!tags || tags.length === 0) { + const span = document.createElement('span'); + span.className = 'badge text-bg-light text-muted'; + span.textContent = 'Sem etiquetas'; + tagsEl.appendChild(span); + } else { + tags.forEach((tagItem) => { + const span = document.createElement('span'); + span.className = 'badge rounded-pill text-bg-primary-subtle text-primary'; + span.textContent = tagItem.tag; + tagsEl.appendChild(span); + }); + } + } catch (err) { + console.error('[FACODI] Erro ao carregar tópico:', err); + contentEl.innerHTML = '

    Não foi possível carregar o conteúdo do tópico.

    '; + playlistsEl.innerHTML = '
    Erro ao carregar playlists.
    '; + tagsEl.innerHTML = 'Erro ao carregar tags'; + } + } + + function boot() { + const dataset = document.body ? document.body.dataset : null; + if (!dataset) return; + if (dataset.course && dataset.plan) { + loadCoursePage(dataset.course, dataset.plan); + } else if (dataset.uc) { + loadUCPage(dataset.uc); + } else if (dataset.topic) { + loadTopicPage(dataset.topic); + } + } + + document.addEventListener('DOMContentLoaded', boot); + + window.loadCoursePage = loadCoursePage; + window.loadUCPage = loadUCPage; + window.loadTopicPage = loadTopicPage; +})(); diff --git a/static/js/supabaseClient.js b/static/js/supabaseClient.js new file mode 100644 index 00000000..36a6ce5d --- /dev/null +++ b/static/js/supabaseClient.js @@ -0,0 +1,27 @@ +(function () { + if (typeof window === 'undefined') { + return; + } + + const url = window.__SUPABASE_URL__; + const anonKey = window.__SUPABASE_ANON_KEY__; + + if (!url || !anonKey) { + console.warn('[FACODI] Variáveis SUPABASE_URL ou SUPABASE_ANON_KEY não configuradas.'); + return; + } + + if (!window.supabase || typeof window.supabase.createClient !== 'function') { + console.error('[FACODI] Biblioteca supabase-js não carregada.'); + return; + } + + const client = window.supabase.createClient(url, anonKey, { + auth: { + persistSession: false, + }, + }); + + window.facodi = window.facodi || {}; + window.facodi.supabase = client; +})(); diff --git a/supabase/migrations/20240101000000_init_facodi.sql b/supabase/migrations/20240101000000_init_facodi.sql new file mode 100644 index 00000000..6e024892 --- /dev/null +++ b/supabase/migrations/20240101000000_init_facodi.sql @@ -0,0 +1,261 @@ +-- FACODI initial schema, tables and RLS policies +begin; + +create schema if not exists catalog; +create schema if not exists subjects; +create schema if not exists mapping; + +create table if not exists catalog.course ( + code text not null, + plan_version text not null, + name text not null, + degree text not null, + ects_total integer, + duration_semesters integer, + institution text, + school text, + language text, + summary text, + inserted_at timestamptz not null default timezone('utc', now()), + updated_at timestamptz not null default timezone('utc', now()), + constraint course_pk primary key (code, plan_version) +); + +create table if not exists catalog.course_content ( + course_code text not null, + plan_version text not null, + content_md text not null, + updated_at timestamptz not null default timezone('utc', now()), + constraint course_content_pk primary key (course_code, plan_version), + constraint course_content_course_fk + foreign key (course_code, plan_version) + references catalog.course (code, plan_version) + on delete cascade +); + +create table if not exists catalog.uc ( + code text primary key, + course_code text not null, + course_plan_version text not null, + name text not null, + description text, + ects integer, + semester integer, + language text, + prerequisites text[] not null default '{}', + inserted_at timestamptz not null default timezone('utc', now()), + updated_at timestamptz not null default timezone('utc', now()), + constraint uc_course_fk + foreign key (course_code, course_plan_version) + references catalog.course (code, plan_version) + on delete cascade +); + +create table if not exists catalog.uc_content ( + uc_code text primary key, + content_md text not null, + updated_at timestamptz not null default timezone('utc', now()), + constraint uc_content_uc_fk + foreign key (uc_code) + references catalog.uc (code) + on delete cascade +); + +create table if not exists catalog.uc_learning_outcome ( + uc_code text not null, + outcome text not null, + "order" integer not null, + constraint uc_learning_outcome_pk primary key (uc_code, "order"), + constraint uc_learning_outcome_uc_fk + foreign key (uc_code) + references catalog.uc (code) + on delete cascade +); + +create table if not exists subjects.topic ( + slug text primary key, + name text not null, + summary text, + inserted_at timestamptz not null default timezone('utc', now()), + updated_at timestamptz not null default timezone('utc', now()) +); + +create table if not exists subjects.topic_content ( + topic_slug text primary key, + content_md text not null, + updated_at timestamptz not null default timezone('utc', now()), + constraint topic_content_topic_fk + foreign key (topic_slug) + references subjects.topic (slug) + on delete cascade +); + +create table if not exists subjects.topic_tag ( + topic_slug text not null, + tag text not null, + constraint topic_tag_pk primary key (topic_slug, tag), + constraint topic_tag_topic_fk + foreign key (topic_slug) + references subjects.topic (slug) + on delete cascade +); + +create table if not exists mapping.uc_topic ( + uc_code text not null, + topic_slug text not null, + constraint uc_topic_pk primary key (uc_code, topic_slug), + constraint uc_topic_uc_fk + foreign key (uc_code) + references catalog.uc (code) + on delete cascade, + constraint uc_topic_topic_fk + foreign key (topic_slug) + references subjects.topic (slug) + on delete cascade +); + +create table if not exists mapping.uc_playlist ( + uc_code text not null, + playlist_id text not null, + priority integer, + constraint uc_playlist_pk primary key (uc_code, playlist_id), + constraint uc_playlist_uc_fk + foreign key (uc_code) + references catalog.uc (code) + on delete cascade +); + +create table if not exists mapping.topic_playlist ( + topic_slug text not null, + playlist_id text not null, + priority integer, + constraint topic_playlist_pk primary key (topic_slug, playlist_id), + constraint topic_playlist_topic_fk + foreign key (topic_slug) + references subjects.topic (slug) + on delete cascade +); + +-- Grant basic schema usage for public querying +grant usage on schema catalog to anon, authenticated, service_role; +grant usage on schema subjects to anon, authenticated, service_role; +grant usage on schema mapping to anon, authenticated, service_role; + +-- Enable Row Level Security +alter table catalog.course enable row level security; +alter table catalog.course_content enable row level security; +alter table catalog.uc enable row level security; +alter table catalog.uc_content enable row level security; +alter table catalog.uc_learning_outcome enable row level security; +alter table subjects.topic enable row level security; +alter table subjects.topic_content enable row level security; +alter table subjects.topic_tag enable row level security; +alter table mapping.uc_topic enable row level security; +alter table mapping.uc_playlist enable row level security; +alter table mapping.topic_playlist enable row level security; + +-- Helper function to check for service_role +create or replace function public.is_service_role() +returns boolean +language sql +stable +as $$ + select auth.role() = 'service_role'; +$$; + +-- Course policies +create policy "course_select_public" on catalog.course + for select to anon + using (true); +create policy "course_manage_service" on catalog.course + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +-- Course content +create policy "course_content_select_public" on catalog.course_content + for select to anon + using (true); +create policy "course_content_manage_service" on catalog.course_content + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +-- UC +create policy "uc_select_public" on catalog.uc + for select to anon + using (true); +create policy "uc_manage_service" on catalog.uc + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +-- UC content +create policy "uc_content_select_public" on catalog.uc_content + for select to anon + using (true); +create policy "uc_content_manage_service" on catalog.uc_content + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +-- UC learning outcomes +create policy "uc_outcome_select_public" on catalog.uc_learning_outcome + for select to anon + using (true); +create policy "uc_outcome_manage_service" on catalog.uc_learning_outcome + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +-- Topics +create policy "topic_select_public" on subjects.topic + for select to anon + using (true); +create policy "topic_manage_service" on subjects.topic + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +create policy "topic_content_select_public" on subjects.topic_content + for select to anon + using (true); +create policy "topic_content_manage_service" on subjects.topic_content + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +create policy "topic_tag_select_public" on subjects.topic_tag + for select to anon + using (true); +create policy "topic_tag_manage_service" on subjects.topic_tag + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +-- Mapping tables +create policy "uc_topic_select_public" on mapping.uc_topic + for select to anon + using (true); +create policy "uc_topic_manage_service" on mapping.uc_topic + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +create policy "uc_playlist_select_public" on mapping.uc_playlist + for select to anon + using (true); +create policy "uc_playlist_manage_service" on mapping.uc_playlist + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +create policy "topic_playlist_select_public" on mapping.topic_playlist + for select to anon + using (true); +create policy "topic_playlist_manage_service" on mapping.topic_playlist + for all to service_role + using (public.is_service_role()) + with check (public.is_service_role()); + +commit; diff --git a/supabase/seed.sql b/supabase/seed.sql new file mode 100644 index 00000000..ce34c119 --- /dev/null +++ b/supabase/seed.sql @@ -0,0 +1,122 @@ +begin; + +insert into catalog.course (code, plan_version, name, degree, ects_total, duration_semesters, institution, school, language, summary) +values ( + 'LESTI', + '2024/2025', + 'Licenciatura em Engenharia de Sistemas e Tecnologias da Informação', + 'bachelor', + 180, + 6, + 'Universidade do Algarve', + 'Escola Superior de Tecnologia', + 'pt', + 'Curso voltado para o desenvolvimento de soluções digitais com forte base em engenharia, programação e sistemas de informação.' +) +on conflict (code, plan_version) do update set + name = excluded.name, + degree = excluded.degree, + ects_total = excluded.ects_total, + duration_semesters = excluded.duration_semesters, + institution = excluded.institution, + school = excluded.school, + language = excluded.language, + summary = excluded.summary, + updated_at = timezone('utc', now()); + +insert into catalog.course_content (course_code, plan_version, content_md) +values ( + 'LESTI', + '2024/2025', + $$## Visão Geral + +A licenciatura em Engenharia de Sistemas e Tecnologias da Informação (LESTI) combina uma base sólida em matemática e programação com disciplinas de análise e conceção de sistemas. + +O plano 2024/2025 enfatiza competências práticas e colaboração em projetos reais, com foco em soluções abertas e acessíveis.$$ +) +on conflict (course_code, plan_version) do update set + content_md = excluded.content_md, + updated_at = timezone('utc', now()); + +insert into catalog.uc (code, course_code, course_plan_version, name, description, ects, semester, language, prerequisites) +values ( + 'LESTI-ALG1', + 'LESTI', + '2024/2025', + 'Algoritmos e Estruturas de Dados I', + 'Introdução aos fundamentos de algoritmos, análise de complexidade e estruturas de dados lineares.', + 6, + 1, + 'pt', + array['LESTI-MAT1'] +) +on conflict (code) do update set + course_code = excluded.course_code, + course_plan_version = excluded.course_plan_version, + name = excluded.name, + description = excluded.description, + ects = excluded.ects, + semester = excluded.semester, + language = excluded.language, + prerequisites = excluded.prerequisites, + updated_at = timezone('utc', now()); + +insert into catalog.uc_content (uc_code, content_md) +values ( + 'LESTI-ALG1', + $$## Ementa + +- Conceitos básicos de algoritmos e notação assintótica. +- Estruturas de dados lineares: arrays, listas ligadas, pilhas e filas. +- Introdução a árvores e tabelas de dispersão. +- Estratégias iterativas e recursivas de resolução de problemas.$$ +) +on conflict (uc_code) do update set + content_md = excluded.content_md, + updated_at = timezone('utc', now()); + +-- Limpa resultados antes de inserir seeds +delete from catalog.uc_learning_outcome where uc_code = 'LESTI-ALG1'; +insert into catalog.uc_learning_outcome (uc_code, outcome, "order") values + ('LESTI-ALG1', 'Compreender a análise de complexidade de algoritmos.', 1), + ('LESTI-ALG1', 'Implementar estruturas de dados lineares em diferentes linguagens.', 2), + ('LESTI-ALG1', 'Selecionar a estrutura de dados mais adequada para um problema.', 3); + +delete from mapping.uc_playlist where uc_code = 'LESTI-ALG1'; +insert into mapping.uc_playlist (uc_code, playlist_id, priority) values + ('LESTI-ALG1', 'PLlestiALG1Aulas', 1), + ('LESTI-ALG1', 'PLlestiALG1Exercicios', 2); + +delete from mapping.uc_topic where uc_code = 'LESTI-ALG1'; +insert into mapping.uc_topic (uc_code, topic_slug) values + ('LESTI-ALG1', 'estruturas-de-dados'); + +insert into subjects.topic (slug, name, summary) +values ('estruturas-de-dados', 'Estruturas de Dados', 'Conceitos fundamentais de armazenamento e manipulação eficiente de dados.') +on conflict (slug) do update set + name = excluded.name, + summary = excluded.summary, + updated_at = timezone('utc', now()); + +insert into subjects.topic_content (topic_slug, content_md) +values ( + 'estruturas-de-dados', + $$As estruturas de dados definem como a informação é organizada na memória e determinam o desempenho dos algoritmos que as manipulam. + +Neste tópico abordamos listas ligadas, pilhas, filas, árvores e mapas, destacando cenários de aplicação e implicações na complexidade temporal.$$ +) +on conflict (topic_slug) do update set + content_md = excluded.content_md, + updated_at = timezone('utc', now()); + +delete from subjects.topic_tag where topic_slug = 'estruturas-de-dados'; +insert into subjects.topic_tag (topic_slug, tag) values + ('estruturas-de-dados', 'estruturas'), + ('estruturas-de-dados', 'listas'), + ('estruturas-de-dados', 'pilhas'); + +delete from mapping.topic_playlist where topic_slug = 'estruturas-de-dados'; +insert into mapping.topic_playlist (topic_slug, playlist_id, priority) values + ('estruturas-de-dados', 'PLtopicosEstruturas', 1); + +commit;