├── .all-contributorsrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.yml │ ├── FEATURE_REQUEST.yml │ ├── add-community.yml │ └── add-podcast.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── community.yml │ └── podcast.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .lintstagedrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── angular-hub ├── .eslintrc.json ├── index.html ├── manifest.webmanifest ├── ngsw-config.json ├── package.json ├── postcss.config.cjs ├── project.json ├── src │ ├── app │ │ ├── app.component.ts │ │ ├── app.config.server.ts │ │ ├── app.config.ts │ │ ├── components │ │ │ ├── banner.component.ts │ │ │ ├── cards │ │ │ │ ├── community-card.component.ts │ │ │ │ ├── event-card.component.ts │ │ │ │ └── podcast-card.component.ts │ │ │ ├── event-section.component.ts │ │ │ ├── message.component.ts │ │ │ ├── navigation │ │ │ │ ├── navigation.component.html │ │ │ │ ├── navigation.component.scss │ │ │ │ └── navigation.component.ts │ │ │ └── tag.component.ts │ │ ├── pages │ │ │ ├── [...page-not-found].page.ts │ │ │ ├── callforpapers │ │ │ │ ├── index.page.ts │ │ │ │ └── index.server.ts │ │ │ ├── communities │ │ │ │ ├── index.page.ts │ │ │ │ └── index.server.ts │ │ │ ├── index.page.ts │ │ │ ├── index.server.ts │ │ │ ├── podcasts │ │ │ │ ├── index.page.ts │ │ │ │ └── index.server.ts │ │ │ └── watch-party │ │ │ │ └── index.page.ts │ │ └── services │ │ │ ├── json-ld.service.ts │ │ │ ├── pwa.service.ts │ │ │ └── supabase.service.ts │ ├── main.server.ts │ ├── main.ts │ ├── models │ │ ├── call-for-papers.model.ts │ │ ├── community.model.ts │ │ ├── event-type.model.ts │ │ ├── event.model.ts │ │ └── podcast.model.ts │ ├── public │ │ └── assets │ │ │ ├── .gitkeep │ │ │ ├── analog.svg │ │ │ ├── data │ │ │ ├── community.json │ │ │ └── podcast.json │ │ │ ├── favicon.ico │ │ │ ├── fonts │ │ │ ├── Luciole-Regular.ttf │ │ │ └── PixelifySans-Regular.ttf │ │ │ ├── icons │ │ │ ├── bluesky-icon.svg │ │ │ ├── event-icon.svg │ │ │ ├── group-icon.svg │ │ │ ├── icon-128x128.png │ │ │ ├── icon-144x144.png │ │ │ ├── icon-152x152.png │ │ │ ├── icon-192x192.png │ │ │ ├── icon-36x36.png │ │ │ ├── icon-384x384.png │ │ │ ├── icon-48x48.png │ │ │ ├── icon-512x512.png │ │ │ ├── icon-72x72.png │ │ │ ├── icon-96x96.png │ │ │ ├── linkedin-icon.svg │ │ │ ├── website_link-icon.svg │ │ │ ├── x-twitter-icon.svg │ │ │ └── youtube-icon.svg │ │ │ ├── images │ │ │ ├── hero.webp │ │ │ ├── logo.png │ │ │ ├── logo.webp │ │ │ └── og-image.webp │ │ │ ├── logos │ │ │ ├── adventures-in-angular.webp │ │ │ ├── angular-air.webp │ │ │ ├── angular-belgrade.webp │ │ │ ├── angular-catch-up.jpg │ │ │ ├── angular-connect.png │ │ │ ├── angular-devs-france.webp │ │ │ ├── angular-experts.webp │ │ │ ├── angular-love.png │ │ │ ├── angular-master-podcast.webp │ │ │ ├── angular-plus-show.webp │ │ │ ├── angular-rocks.webp │ │ │ ├── angular-sp.webp │ │ │ ├── angular-space.webp │ │ │ ├── angular-toronto.png │ │ │ ├── angular-warsaw.jpeg │ │ │ ├── angularidades.webp │ │ │ ├── dutch-angular-group.svg │ │ │ ├── ng-be.png │ │ │ ├── ng-de.svg │ │ │ ├── ng-gunma.png │ │ │ ├── ng-jp.svg │ │ │ ├── ng-kato.svg │ │ │ ├── ng-news.webp │ │ │ ├── ng-poznan.png │ │ │ ├── ng-rome.svg │ │ │ ├── official-logo.webp │ │ │ ├── paris-angular.png │ │ │ └── startup-angular.png │ │ │ ├── nav │ │ │ └── icons │ │ │ │ └── swagger-svgrepo-com.svg │ │ │ └── swagger │ │ │ └── openapi.yml │ ├── robots.txt │ ├── server │ │ ├── auth │ │ │ └── index.ts │ │ ├── routes │ │ │ └── v1 │ │ │ │ ├── auth │ │ │ │ └── callback.get.ts │ │ │ │ ├── communities │ │ │ │ ├── callforpapers.get.ts │ │ │ │ └── index.get.ts │ │ │ │ ├── community.ts │ │ │ │ ├── events │ │ │ │ ├── callforpapers │ │ │ │ │ └── index.get.ts │ │ │ │ ├── index.get.ts │ │ │ │ ├── past │ │ │ │ │ └── index.get.ts │ │ │ │ └── upcoming │ │ │ │ │ └── index.get.ts │ │ │ │ ├── og-image.ts │ │ │ │ ├── podcasts.ts │ │ │ │ └── swagger.ts │ │ └── schemas │ │ │ ├── community.schema.ts │ │ │ ├── event.schema.ts │ │ │ └── podcast.schema.ts │ ├── styles.scss │ ├── test-setup.ts │ └── vite-env.d.ts ├── tailwind.config.cjs ├── tsconfig.app.json ├── tsconfig.editor.json ├── tsconfig.json ├── tsconfig.spec.json └── vite.config.ts ├── commitlint.config.js ├── jest.config.ts ├── jest.preset.js ├── libs └── plugin │ ├── .eslintrc.json │ ├── README.md │ ├── generators.json │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ ├── generators │ │ ├── create-community │ │ │ ├── generator.spec.ts │ │ │ ├── generator.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ │ ├── create-event │ │ │ ├── generator.spec.ts │ │ │ ├── generator.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ │ ├── create-podcast │ │ │ ├── generator.spec.ts │ │ │ ├── generator.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ │ ├── models │ │ │ ├── community.ts │ │ │ ├── event.ts │ │ │ └── podcast.ts │ │ └── utils │ │ │ └── isPublicAsset.ts │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── migrations.json ├── nx.json ├── package.json ├── scripts ├── add-community.js └── add-podcast.js ├── tsconfig.base.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "angular-hub", 3 | "projectOwner": "angular-sanctuary", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributorsPerLine": 7, 10 | "contributors": [ 11 | { 12 | "login": "grand-rick001", 13 | "name": "Patrick Murimi", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/89421020?v=4", 15 | "profile": "https://github.com/grand-rick001", 16 | "contributions": [ 17 | "code" 18 | ] 19 | }, 20 | { 21 | "login": "rlmestre", 22 | "name": "Rafael Mestre", 23 | "avatar_url": "https://avatars.githubusercontent.com/u/277805?v=4", 24 | "profile": "https://github.com/rlmestre", 25 | "contributions": [ 26 | "bug", 27 | "code" 28 | ] 29 | }, 30 | { 31 | "login": "DominikPieper", 32 | "name": "Dominik Pieper", 33 | "avatar_url": "https://avatars.githubusercontent.com/u/77470?v=4", 34 | "profile": "http://nxext.dev", 35 | "contributions": [ 36 | "code" 37 | ] 38 | }, 39 | { 40 | "login": "giovnzcr", 41 | "name": "giovnzcr", 42 | "avatar_url": "https://avatars.githubusercontent.com/u/11030212?v=4", 43 | "profile": "https://github.com/giovnzcr", 44 | "contributions": [ 45 | "code" 46 | ] 47 | }, 48 | { 49 | "login": "Dyqmin", 50 | "name": "Dominik Donoch", 51 | "avatar_url": "https://avatars.githubusercontent.com/u/23712053?v=4", 52 | "profile": "https://github.com/Dyqmin", 53 | "contributions": [ 54 | "bug", 55 | "code" 56 | ] 57 | }, 58 | { 59 | "login": "ilirbeqirii", 60 | "name": "Ilir Beqiri", 61 | "avatar_url": "https://avatars.githubusercontent.com/u/24731032?v=4", 62 | "profile": "https://github.com/ilirbeqirii", 63 | "contributions": [ 64 | "code" 65 | ] 66 | }, 67 | { 68 | "login": "eneajaho", 69 | "name": "Enea Jahollari", 70 | "avatar_url": "https://avatars.githubusercontent.com/u/25394362?v=4", 71 | "profile": "https://eneajaho.me", 72 | "contributions": [ 73 | "code", 74 | "bug" 75 | ] 76 | }, 77 | { 78 | "login": "ahmedhmf", 79 | "name": "Ahmed Moustafa", 80 | "avatar_url": "https://avatars.githubusercontent.com/u/43710157?v=4", 81 | "profile": "https://ahmed-moustafa.de/", 82 | "contributions": [ 83 | "code" 84 | ] 85 | }, 86 | { 87 | "login": "rainerhahnekamp", 88 | "name": "Rainer Hahnekamp", 89 | "avatar_url": "https://avatars.githubusercontent.com/u/5721205?v=4", 90 | "profile": "https://www.rainerhahnekamp.com", 91 | "contributions": [ 92 | "code", 93 | "doc" 94 | ] 95 | }, 96 | { 97 | "login": "ajitzero", 98 | "name": "Ajit Panigrahi", 99 | "avatar_url": "https://avatars.githubusercontent.com/u/19947758?v=4", 100 | "profile": "https://beta.ajitpanigrahi.com", 101 | "contributions": [ 102 | "code", 103 | "bug" 104 | ] 105 | }, 106 | { 107 | "login": "nekomamoushi", 108 | "name": "nekomamoushi", 109 | "avatar_url": "https://avatars.githubusercontent.com/u/46743117?v=4", 110 | "profile": "https://github.com/nekomamoushi", 111 | "contributions": [ 112 | "code", 113 | "bug" 114 | ] 115 | }, 116 | { 117 | "login": "d-koppenhagen", 118 | "name": "Danny Koppenhagen", 119 | "avatar_url": "https://avatars.githubusercontent.com/u/4279702?v=4", 120 | "profile": "https://k9n.dev", 121 | "contributions": [ 122 | "code" 123 | ] 124 | }, 125 | { 126 | "login": "lucianomurr", 127 | "name": "Luciano", 128 | "avatar_url": "https://avatars.githubusercontent.com/u/281553?v=4", 129 | "profile": "http://ngrome.io", 130 | "contributions": [ 131 | "code" 132 | ] 133 | }, 134 | { 135 | "login": "AdrianRomanski", 136 | "name": "Adrian Romanski", 137 | "avatar_url": "https://avatars.githubusercontent.com/u/44946000?v=4", 138 | "profile": "https://www.linkedin.com/in/adrianromanski/", 139 | "contributions": [ 140 | "code" 141 | ] 142 | }, 143 | { 144 | "login": "eduardoRoth", 145 | "name": "Eduardo Roth", 146 | "avatar_url": "https://avatars.githubusercontent.com/u/5419161?v=4", 147 | "profile": "https://eduardoroth.dev", 148 | "contributions": [ 149 | "doc", 150 | "code" 151 | ] 152 | }, 153 | { 154 | "login": "danilolmc", 155 | "name": "Danilo Lima", 156 | "avatar_url": "https://avatars.githubusercontent.com/u/31545128?v=4", 157 | "profile": "https://github.com/danilolmc", 158 | "contributions": [ 159 | "code" 160 | ] 161 | }, 162 | { 163 | "login": "jdegand", 164 | "name": "J. Degand", 165 | "avatar_url": "https://avatars.githubusercontent.com/u/70610011?v=4", 166 | "profile": "https://github.com/jdegand", 167 | "contributions": [ 168 | "code" 169 | ] 170 | }, 171 | { 172 | "login": "DavidePassafaro", 173 | "name": "Davide Passafaro", 174 | "avatar_url": "https://avatars.githubusercontent.com/u/29404081?v=4", 175 | "profile": "https://www.linkedin.com/in/davide-passafaro/", 176 | "contributions": [ 177 | "code" 178 | ] 179 | }, 180 | { 181 | "login": "ver-1000000", 182 | "name": "Shota Sasaki -- AKAI", 183 | "avatar_url": "https://avatars.githubusercontent.com/u/6778957?v=4", 184 | "profile": "https://ver1000000.com/", 185 | "contributions": [ 186 | "code" 187 | ] 188 | } 189 | ] 190 | } 191 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx", "html"], 5 | "overrides": [ 6 | { 7 | "files": "*.json", 8 | "parser": "jsonc-eslint-parser", 9 | "rules": {} 10 | }, 11 | { 12 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 13 | "rules": { 14 | "@nx/enforce-module-boundaries": [ 15 | "error", 16 | { 17 | "enforceBuildableLibDependency": true, 18 | "allow": [], 19 | "depConstraints": [ 20 | { 21 | "sourceTag": "*", 22 | "onlyDependOnLibsWithTags": ["*"] 23 | } 24 | ] 25 | } 26 | ], 27 | "@typescript-eslint/no-unused-vars": "off" 28 | } 29 | }, 30 | { 31 | "files": ["*.ts", "*.tsx"], 32 | "extends": ["plugin:@nx/typescript"], 33 | "rules": { 34 | "@typescript-eslint/no-unused-vars": "off", 35 | "@typescript-eslint/no-empty-function": "off", 36 | "@typescript-eslint/no-non-null-assertion": "off", 37 | "@typescript-eslint/no-extra-semi": "error", 38 | "no-extra-semi": "off" 39 | } 40 | }, 41 | { 42 | "files": ["*.js", "*.jsx"], 43 | "extends": ["plugin:@nx/javascript"], 44 | "rules": { 45 | "@typescript-eslint/no-extra-semi": "error", 46 | "no-extra-semi": "off" 47 | } 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Report a bug in the Angular Hub project 3 | title: '[Bug]: ' 4 | labels: 5 | - bug 6 | body: 7 | - type: dropdown 8 | attributes: 9 | label: Relevant scope 10 | description: What is the scope of this request? 11 | options: 12 | - angular application 13 | - event content 14 | - 'Other: describe below' 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Description 20 | description: A clear and concise description of the problem 21 | validations: 22 | required: true 23 | - type: dropdown 24 | id: contribute 25 | attributes: 26 | label: Do you want to contribute with a pull request? 27 | options: 28 | - 'Yes' 29 | - 'No' 30 | validations: 31 | required: true 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Suggest a feature for Angular Hub project 3 | title: '[Feature Request]:' 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: '# Feature Request' 8 | - type: dropdown 9 | attributes: 10 | label: Relevant Scope 11 | description: What is the scope of this request? 12 | options: 13 | - angular application 14 | - event content 15 | - 'Other: describe below' 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Description 21 | description: ' ' 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Describe the solution you'd like 27 | description: If you have a solution in mind, please describe it. 28 | - type: textarea 29 | attributes: 30 | label: Describe alternatives you've considered 31 | description: Have you considered any alternative solutions or workarounds? 32 | - type: dropdown 33 | id: contribute 34 | attributes: 35 | label: Do you want to contribute with a pull request? 36 | options: 37 | - 'Yes' 38 | - 'No' 39 | validations: 40 | required: true 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/add-community.yml: -------------------------------------------------------------------------------- 1 | name: New community 2 | description: Submit a community non listed yet for angular-hub.com/communities 3 | title: '[New Community]: ' 4 | body: 5 | - type: input 6 | id: name 7 | attributes: 8 | label: Community name 9 | validations: 10 | required: true 11 | - type: dropdown 12 | id: type 13 | attributes: 14 | label: Community main activity 15 | options: 16 | - conference 17 | - meetup 18 | validations: 19 | required: true 20 | - type: input 21 | id: location 22 | attributes: 23 | label: Location 24 | description: , format expected if any 25 | - type: input 26 | id: url 27 | attributes: 28 | label: Community preferred website 29 | - type: input 30 | id: logo 31 | attributes: 32 | label: Logo URL 33 | description: >- 34 | optional, can be provided as an image on a pull request for better 35 | quality 36 | - type: input 37 | id: bluesky 38 | attributes: 39 | label: Bluesky 40 | - type: input 41 | id: x 42 | attributes: 43 | label: X 44 | - type: input 45 | id: linkedin 46 | attributes: 47 | label: LinkedIn 48 | - type: input 49 | id: callforpapers 50 | attributes: 51 | label: Call For Papers URL 52 | description: encouraged as a way to get more speakers 53 | - type: markdown 54 | attributes: 55 | value: >- 56 | This template was generated with [Issue Forms 57 | Creator](https://issue-forms-creator.netlify.app) 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/add-podcast.yml: -------------------------------------------------------------------------------- 1 | name: New podcast 2 | description: submit a new podcast for angular-hub.com/podcasts 3 | title: '[New podcast]: ' 4 | body: 5 | - type: input 6 | id: name 7 | attributes: 8 | label: Podcast name 9 | validations: 10 | required: true 11 | - type: input 12 | id: url 13 | attributes: 14 | label: Podcast URL 15 | description: Choose the preferred platform to share the podcast content 16 | validations: 17 | required: true 18 | - type: input 19 | id: logo 20 | attributes: 21 | label: Podcast logo 22 | - type: input 23 | id: language 24 | attributes: 25 | label: Language 26 | validations: 27 | required: true 28 | - type: markdown 29 | attributes: 30 | value: >- 31 | This template was generated with [Issue Forms 32 | Creator](https://issue-forms-creator.netlify.app) 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | ## PR Checklist 9 | 10 | Please check if your PR fulfills the following requirements: 11 | 12 | - [ ] The commit message follows our guidelines: https://github.com/ngneat/transloco/blob/master/CONTRIBUTING.md#commit 13 | - [ ] Tests for the changes have been added (for bug fixes / features) 14 | - [ ] Docs have been added / updated (for bug fixes / features) 15 | 16 | ## PR Type 17 | 18 | What kind of change does this PR introduce? 19 | 20 | 21 | 22 | - [ ] Bugfix 23 | - [ ] Feature 24 | - [ ] Code style update (formatting, local variables) 25 | - [ ] Refactoring (no functional changes, no api changes) 26 | - [ ] Build related changes 27 | - [ ] CI related changes 28 | - [ ] Documentation content changes 29 | - [ ] Other... Please describe: 30 | 31 | ## What is the current behavior? 32 | 33 | 34 | 35 | Issue Number: N/A 36 | 37 | ## What is the new behavior? 38 | 39 | ## Does this PR introduce a breaking change? 40 | 41 | - [ ] Yes 42 | - [ ] No 43 | 44 | 45 | 46 | ## Other information 47 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: npm 10 | directory: '/' 11 | schedule: 12 | interval: monthly 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | # Cache node_modules 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 20 19 | cache: 'npm' 20 | - run: yarn install --frozen-lockfile 21 | - uses: nrwl/nx-set-shas@v4 22 | 23 | - run: npx nx format:check 24 | - run: npx nx run angular-hub:lint 25 | - run: npx nx run angular-hub:build:production 26 | -------------------------------------------------------------------------------- /.github/workflows/community.yml: -------------------------------------------------------------------------------- 1 | name: Add community 2 | on: 3 | issues: 4 | types: 5 | - labeled 6 | permissions: 7 | contents: write 8 | issues: write 9 | concurrency: 'main' 10 | jobs: 11 | add_community: 12 | if: github.event.label.name == 'community-approved' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: '20' 20 | 21 | - uses: stefanbuck/github-issue-praser@v3 22 | id: issue-parser 23 | with: 24 | template-path: .github/ISSUE_TEMPLATE/add-community.yml 25 | 26 | - run: echo '${{ steps.issue-parser.outputs.jsonString }}' > add-community.json 27 | 28 | - run: cat add-community.json 29 | 30 | - run: node ./scripts/add-community.js 31 | 32 | - name: Commit changes 33 | shell: bash 34 | run: | 35 | git config --global user.email "github-actions[bot]@users.noreply.github.com" && \ 36 | git config --global user.name "github-actions[bot]" && \ 37 | git add angular-hub/src/public/assets/data/community.json && \ 38 | git commit -m 'Add community' && \ 39 | git push 40 | 41 | - uses: peter-evans/close-issue@v3 42 | with: 43 | comment: 'Community added: thanks for your contribution!' 44 | -------------------------------------------------------------------------------- /.github/workflows/podcast.yml: -------------------------------------------------------------------------------- 1 | name: Add podcast 2 | on: 3 | issues: 4 | types: 5 | - labeled 6 | permissions: 7 | contents: write 8 | issues: write 9 | concurrency: 'main' 10 | jobs: 11 | add_podcast: 12 | if: github.event.label.name == 'podcast-approved' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: '20' 20 | 21 | - uses: stefanbuck/github-issue-praser@v3 22 | id: issue-parser 23 | with: 24 | template-path: .github/ISSUE_TEMPLATE/add-podcast.yml 25 | 26 | - run: echo '${{ steps.issue-parser.outputs.jsonString }}' > add-podcast.json 27 | 28 | - run: cat add-podcast.json 29 | 30 | - run: node ./scripts/add-podcast.js 31 | 32 | - name: Commit changes 33 | shell: bash 34 | run: | 35 | git config --global user.email "github-actions[bot]@users.noreply.github.com" && \ 36 | git config --global user.name "github-actions[bot]" && \ 37 | git add angular-hub/src/public/assets/data/podcast.json && \ 38 | git commit -m 'Add podcast' && \ 39 | git push 40 | 41 | - uses: peter-evans/close-issue@v3 42 | with: 43 | comment: 'Podcast added: thanks for your contribution!' 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .nx/cache 42 | .nx/workspace-data 43 | .angular 44 | 45 | .env 46 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,html}": ["nx affected -t lint"], 3 | "*": ["npx nx format:write --files"] 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage 4 | /.nx/cache 5 | .angular 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["json"] 3 | } 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | gerome.grignon.lp2@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Run the project 2 | 3 | ## Prerequisites 4 | 5 | - [Node.js](https://nodejs.org/en/) (v12.16.1 or higher) 6 | 7 | ### Setup 8 | 9 | Run `yarn install --frozen-lockfile` to install the application dependencies. 10 | 11 | ### Development 12 | 13 | Run `yarn start` for a dev server. Navigate to `http://localhost:4200/`. The application automatically reloads if you change any of the source files. 14 | 15 | ### Build 16 | 17 | Run `yarn build` to build the client project. The client build artifacts are located in the `dist/angular-hub/analog/public` directory. 18 | 19 | --- 20 | 21 | #### Before running your app 22 | 23 | In order for the app to run correctly, you must have an **.env** file at the root of your app folder, with the following content: 24 | 25 | ```text 26 | VITE_ANALOG_PUBLIC_BASE_URL="http://localhost:4200" 27 | ``` 28 | 29 | ## Licenses 30 | 31 | This project uses the MIT License for the project code. 32 | It excludes the `angular-hub/src/content` folder, which includes trademarks and logos from the Angular community. 33 | 34 | # Contribute 35 | 36 | ## Content 37 | 38 | ### Communities 39 | 40 | > The goal of this feature is to help people be part of communities. 41 | > For this reason, only active communities are accepted. 42 | > Removal of communities might be considered if not active for the last 2 years. 43 | 44 | To add a new community, update the `angular-hub/src/public/assets/data/community.json` by adding a new item to the array. 45 | The item should match this format: 46 | 47 | ```typescript 48 | export interface Community { 49 | name: string; 50 | type: 'workshop' | 'conference' | 'meetup' | 'other'; 51 | location: string | null; 52 | url: string | null; 53 | mediaChannel: MediaChannel | null; 54 | logo: string | null; 55 | bluesky: string | null; 56 | x: string | null; 57 | linkedin: string | null; 58 | callForPapers: string | null; 59 | events: Event[]; 60 | } 61 | ``` 62 | 63 | ### Events 64 | 65 | > If you add a conference event, mind listing it on [developers-conferences-agenda](https://github.com/scraly/developers-conferences-agenda) too for a broader audience! 66 | 67 | To add a new event, update the `angular-hub/src/public/assets/data/community.json` by adding a new item to the related community item (firstly add a community with previous section instructions if needed). 68 | The item should match this format: 69 | 70 | ```typescript 71 | export interface Event { 72 | name?: string; 73 | type: 'workshop' | 'conference' | 'meetup' | 'other'; 74 | location?: string | null; 75 | date: string; 76 | language: string; 77 | isFree: boolean; 78 | isRemote: boolean; 79 | isOnsite: boolean; 80 | callForPapers?: string | null; 81 | callForPapersDueDate?: string | null; 82 | url?: string; 83 | } 84 | ``` 85 | 86 | ### Podcasts 87 | 88 | To add a new podcast, update the `angular-hub/src/public/assets/data/podcast.json` by adding a new item to the array. 89 | The item should match this format: 90 | 91 | ```typescript 92 | export interface Podcast { 93 | name: string; 94 | url: string; 95 | logo: string; 96 | language: string; 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 angular-hub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![All Contributors](https://img.shields.io/github/all-contributors/angular-sanctuary/angular-hub?color=ee8449&style=flat-square)](#contributors) 2 | 3 | # Angular Hub 4 | 5 | > Curated list of our beloved Angular events! 6 | 7 | - Angular events 8 | - Angular Communities 9 | - Angular CFPs 10 | - Angular Podcasts 11 | 12 | ## Scope 13 | 14 | The project aims to promote Angular driven community content in the most simplest way by providing minimalistic information to redirect users to the original source. 15 | 16 | The promoted content is limited to community driven events as in: 17 | 18 | - meetups 19 | - free and paid community driven conferences 20 | - free workshops 21 | - workshops aiming to support open source projects 22 | - podcasts inviting community members 23 | 24 | In its current state the project does not aim to promote: 25 | 26 | - commercial workshops 27 | - commercial conferences 28 | - direct learning content like articles or youtube videos 29 | 30 | Feel free to contact me if you have any questions or suggestions. 31 | 32 | ## Contributing 33 | 34 | Please read the [contribution guidelines](CONTRIBUTING.md) to learn how to contribute to this project. 35 | 36 | ## Licenses 37 | 38 | This project uses the MIT License for the project code. 39 | It excludes the `src/content` folder, which includes trademarks and logos from the Angular community. 40 | 41 | ## Contributors 42 | 43 | > We are appreciative of all non-coding contributors who have helped shape this project through discussions or promotions. Your input has had a significant impact on the project, and we aim to acknowledge and recognize all contributions made. 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
Patrick Murimi
Patrick Murimi

💻
Rafael Mestre
Rafael Mestre

🐛 💻
Dominik Pieper
Dominik Pieper

💻
giovnzcr
giovnzcr

💻
Dominik Donoch
Dominik Donoch

🐛 💻
Ilir Beqiri
Ilir Beqiri

💻
Enea Jahollari
Enea Jahollari

💻 🐛
Ahmed Moustafa
Ahmed Moustafa

💻
Rainer Hahnekamp
Rainer Hahnekamp

💻 📖
Ajit Panigrahi
Ajit Panigrahi

💻 🐛
nekomamoushi
nekomamoushi

💻 🐛
Danny Koppenhagen
Danny Koppenhagen

💻
Luciano
Luciano

💻
Adrian Romanski
Adrian Romanski

💻
Eduardo Roth
Eduardo Roth

📖 💻
Danilo Lima
Danilo Lima

💻
J. Degand
J. Degand

💻
Davide Passafaro
Davide Passafaro

💻
Shota Sasaki -- AKAI
Shota Sasaki -- AKAI

💻
77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /angular-hub/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /angular-hub/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Hub 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /angular-hub/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ANGULAR HUB", 3 | "short_name": "ANGULAR HUB", 4 | "theme_color": "#BF25B9", 5 | "background_color": "#121212", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "/assets/icons/icon-36x36.png", 12 | "sizes": "36x36", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "/assets/icons/icon-48x48.png", 18 | "sizes": "48x48", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | }, 22 | { 23 | "src": "/assets/icons/icon-72x72.png", 24 | "sizes": "72x72", 25 | "type": "image/png", 26 | "purpose": "maskable any" 27 | }, 28 | { 29 | "src": "/assets/icons/icon-96x96.png", 30 | "sizes": "96x96", 31 | "type": "image/png", 32 | "purpose": "maskable any" 33 | }, 34 | { 35 | "src": "/assets/icons/icon-128x128.png", 36 | "sizes": "128x128", 37 | "type": "image/png", 38 | "purpose": "maskable any" 39 | }, 40 | { 41 | "src": "/assets/icons/icon-144x144.png", 42 | "sizes": "144x144", 43 | "type": "image/png", 44 | "purpose": "maskable any" 45 | }, 46 | { 47 | "src": "/assets/icons/icon-152x152.png", 48 | "sizes": "152x152", 49 | "type": "image/png", 50 | "purpose": "maskable any" 51 | }, 52 | { 53 | "src": "/assets/icons/icon-192x192.png", 54 | "sizes": "192x192", 55 | "type": "image/png", 56 | "purpose": "maskable any" 57 | }, 58 | { 59 | "src": "/assets/icons/icon-384x384.png", 60 | "sizes": "384x384", 61 | "type": "image/png", 62 | "purpose": "maskable any" 63 | }, 64 | { 65 | "src": "/assets/icons/icon-512x512.png", 66 | "sizes": "512x512", 67 | "type": "image/png", 68 | "purpose": "maskable any" 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /angular-hub/ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" 26 | ] 27 | } 28 | } 29 | ], 30 | "dataGroups": [ 31 | { 32 | "name": "api", 33 | "urls": ["/api/**"], 34 | "cacheConfig": { 35 | "strategy": "freshness", 36 | "maxSize": 100, 37 | "maxAge": "1h", 38 | "timeout": "10s" 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /angular-hub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /angular-hub/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: { 6 | config: join(__dirname, 'tailwind.config.cjs'), 7 | }, 8 | autoprefixer: {}, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /angular-hub/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-hub", 3 | "$schema": "../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "./angular-hub/src", 6 | "tags": [], 7 | "targets": { 8 | "build": { 9 | "executor": "@analogjs/platform:vite", 10 | "outputs": [ 11 | "{options.outputPath}", 12 | "{workspaceRoot}/dist/angular-hub/.nitro", 13 | "{workspaceRoot}/dist/angular-hub/ssr", 14 | "{workspaceRoot}/dist/angular-hub/analog" 15 | ], 16 | "options": { 17 | "main": "angular-hub/src/main.ts", 18 | "configFile": "angular-hub/vite.config.ts", 19 | "outputPath": "dist/angular-hub/client", 20 | "tsConfig": "angular-hub/tsconfig.app.json", 21 | "serviceWorker": true, 22 | "ngswConfigPath": "angular-hub/ngsw-config.json", 23 | "assets": ["src/robots.txt"] 24 | }, 25 | "defaultConfiguration": "production", 26 | "configurations": { 27 | "development": { 28 | "mode": "development" 29 | }, 30 | "production": { 31 | "sourcemap": false, 32 | "mode": "production" 33 | } 34 | } 35 | }, 36 | "serve": { 37 | "executor": "@analogjs/platform:vite-dev-server", 38 | "defaultConfiguration": "development", 39 | "options": { 40 | "buildTarget": "angular-hub:build", 41 | "port": 4200 42 | }, 43 | "configurations": { 44 | "development": { 45 | "buildTarget": "angular-hub:build:development", 46 | "hmr": true 47 | }, 48 | "production": { 49 | "buildTarget": "angular-hub:build:production" 50 | } 51 | } 52 | }, 53 | "extract-i18n": { 54 | "executor": "@angular-devkit/build-angular:extract-i18n", 55 | "options": { 56 | "browserTarget": "angular-hub:build" 57 | } 58 | }, 59 | "test": { 60 | "executor": "@analogjs/platform:vitest", 61 | "outputs": ["{projectRoot}/coverage"] 62 | }, 63 | "lint": { 64 | "executor": "@nx/eslint:lint", 65 | "options": { 66 | "lintFilePatterns": ["./angular-hub/**/*.{ts,html}"] 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /angular-hub/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { afterNextRender, Component, inject } from '@angular/core'; 2 | import { NavigationEnd, Router } from '@angular/router'; 3 | import { NavigationComponent } from './components/navigation/navigation.component'; 4 | import { filter, switchMap } from 'rxjs'; 5 | import { SwUpdate, VersionReadyEvent } from '@angular/service-worker'; 6 | import { PwaService } from './services/pwa.service'; 7 | 8 | @Component({ 9 | selector: 'angular-hub-root', 10 | standalone: true, 11 | imports: [NavigationComponent], 12 | template: ` `, 13 | styles: [ 14 | ` 15 | :host { 16 | width: 100%; 17 | } 18 | `, 19 | ], 20 | }) 21 | export class AppComponent { 22 | readonly #router = inject(Router); 23 | readonly #swUpdate = inject(SwUpdate); 24 | readonly #pwaService = inject(PwaService); 25 | 26 | constructor() { 27 | afterNextRender(() => { 28 | this.#pwaService.initPwaPrompt(); 29 | this.#router.events 30 | .pipe( 31 | filter((event) => event instanceof NavigationEnd), 32 | switchMap(() => this.#swUpdate.versionUpdates), 33 | filter( 34 | (evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY', 35 | ), 36 | ) 37 | .subscribe(() => { 38 | document.location.reload(); 39 | }); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /angular-hub/src/app/app.config.server.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'; 2 | import { provideServerRendering } from '@angular/platform-server'; 3 | import { appConfig } from './app.config'; 4 | 5 | const serverConfig: ApplicationConfig = { 6 | providers: [provideServerRendering()], 7 | }; 8 | 9 | export const config = mergeApplicationConfig(appConfig, serverConfig); 10 | -------------------------------------------------------------------------------- /angular-hub/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, isDevMode } from '@angular/core'; 2 | import { DATE_PIPE_DEFAULT_OPTIONS } from '@angular/common'; 3 | import { provideHttpClient, withFetch } from '@angular/common/http'; 4 | import { provideClientHydration } from '@angular/platform-browser'; 5 | import { provideFileRouter } from '@analogjs/router'; 6 | import { provideContent, withMarkdownRenderer } from '@analogjs/content'; 7 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; 8 | import { 9 | withComponentInputBinding, 10 | withViewTransitions, 11 | } from '@angular/router'; 12 | import { provideServiceWorker } from '@angular/service-worker'; 13 | 14 | export const appConfig: ApplicationConfig = { 15 | providers: [ 16 | { provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: { timezone: '+0000' } }, 17 | provideFileRouter(withViewTransitions(), withComponentInputBinding()), 18 | provideClientHydration(), 19 | provideHttpClient(withFetch()), 20 | provideContent(withMarkdownRenderer()), 21 | provideAnimationsAsync(), 22 | provideServiceWorker('ngsw-worker.js', { 23 | enabled: !isDevMode(), 24 | registrationStrategy: 'registerWhenStable:30000', 25 | }), 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /angular-hub/src/app/components/banner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-banner', 5 | template: ` 6 | 15 | `, 16 | standalone: true, 17 | }) 18 | export class BannerComponent { 19 | title = input('ANGULAR HUB'); 20 | description = input.required(); 21 | } 22 | -------------------------------------------------------------------------------- /angular-hub/src/app/components/cards/community-card.component.ts: -------------------------------------------------------------------------------- 1 | import { DatePipe, NgOptimizedImage, TitleCasePipe } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | computed, 6 | input, 7 | } from '@angular/core'; 8 | import { Community } from '../../../models/community.model'; 9 | 10 | @Component({ 11 | selector: 'app-community-card', 12 | standalone: true, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | imports: [NgOptimizedImage, TitleCasePipe, DatePipe], 15 | template: ` 16 |
17 |
18 | {{ community().type | titlecase }} 23 | @if (upcomingEventLength() > 0) { 24 | 28 | {{ upcomingEventLength() }} upcoming event(s) 29 | 30 | } 31 | @if (inactive()) { 32 | 36 | inactive since {{ inactiveSince() | date: 'MMMM y' }} 37 | 38 | } 39 |
40 |
41 | 48 |
49 |

50 | {{ community().name }} 51 |

52 | {{ 53 | community().location 54 | }} 55 |
56 | 163 |
164 |
165 | @if (community().eventsUrl) { 166 | 172 | 178 | 179 | } 180 | @if (community().websiteUrl) { 181 | 187 | 193 | 194 | } 195 | @if (community().organizersUrl) { 196 | 202 | 208 | 209 | } 210 | @if (community().youtubeUrl) { 211 | 217 | 223 | 224 | } 225 | @if (community().blueskyUrl) { 226 | 232 | 238 | 239 | } 240 | @if (community().xUrl) { 241 | 247 | 253 | 254 | } 255 | @if (community().linkedinUrl) { 256 | 261 | 267 | 268 | } 269 |
270 |
271 | `, 272 | styles: [ 273 | ` 274 | :host { 275 | display: block; 276 | padding-block: 0.5rem; 277 | } 278 | 279 | .type { 280 | top: -0.7rem; 281 | left: 1.2rem; 282 | } 283 | `, 284 | ], 285 | }) 286 | export class CommunityCardComponent { 287 | community = input.required(); 288 | 289 | upcomingEventLength = computed(() => { 290 | return this.community().events?.filter( 291 | (event) => new Date(event.date).getTime() > new Date().getTime(), 292 | ).length; 293 | }); 294 | 295 | inactive = computed(() => { 296 | const inactivityLimit = new Date(); 297 | inactivityLimit.setMonth(inactivityLimit.getMonth() - 6); 298 | 299 | const newestEvent = this.community().events?.sort( 300 | (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), 301 | )[0]; 302 | 303 | return newestEvent 304 | ? new Date(newestEvent.date).getTime() < inactivityLimit.getTime() && 305 | this.community().type === 'meetup' 306 | : false; 307 | }); 308 | 309 | inactiveSince = computed(() => { 310 | const event = this.community().events?.sort( 311 | (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), 312 | )[0]; 313 | 314 | return event ? new Date(event.date) : null; 315 | }); 316 | } 317 | -------------------------------------------------------------------------------- /angular-hub/src/app/components/cards/event-card.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | computed, 5 | input, 6 | } from '@angular/core'; 7 | import { Event } from '../../../models/event.model'; 8 | import { DatePipe } from '@angular/common'; 9 | 10 | @Component({ 11 | selector: 'app-event-card', 12 | standalone: true, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | template: ` 15 |
16 |
20 | @if (!event().toBeAnnounced) { 21 | {{ formatDate(event().date) }} 22 | @let endDate = event().endDate; 23 | @if (endDate) { 24 | {{ formatDate(endDate) }} 25 | } 26 | } @else { 27 | TBA 28 | } 29 |
30 |
31 |
32 | 40 |
41 |
42 |

43 | {{ event().name }} 44 |

45 | 46 |
50 | {{ event().location ?? 'Online' }} 51 | @if (isRemoteFriendly() && event().location) { 52 | - Online 53 | } 54 |
55 |
56 |
57 |
58 | `, 59 | }) 60 | export class EventCardComponent { 61 | event = input.required(); 62 | 63 | eventLinkTitle = computed(() => { 64 | return (this.event().name || this.event().community?.name) + 'event link'; 65 | }); 66 | 67 | isRemoteFriendly = computed(() => { 68 | return this.event().isRemote; 69 | }); 70 | 71 | formatDate(date: string): string | null { 72 | if (!date) return ''; 73 | // Check if the date includes a day component (YYYY-MM-DD) 74 | if (date.length > 7) { 75 | return new DatePipe('en-US').transform(date, 'dd MMM'); 76 | } 77 | // For YYYY-MM format, show only month 78 | return new DatePipe('en-US').transform(date + '-01', 'MMM'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /angular-hub/src/app/components/cards/podcast-card.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, input } from '@angular/core'; 2 | import { NgOptimizedImage } from '@angular/common'; 3 | import { Podcast } from '../../../models/podcast.model'; 4 | import { TagComponent } from '../tag.component'; 5 | 6 | @Component({ 7 | selector: 'app-podcast-card', 8 | standalone: true, 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | template: ` 11 | 43 | `, 44 | imports: [NgOptimizedImage, TagComponent], 45 | styles: [ 46 | ` 47 | :host { 48 | display: block; 49 | padding-block: 0.5rem; 50 | cursor: pointer; 51 | 52 | &:hover { 53 | h3 { 54 | @apply text-secondary underline; 55 | } 56 | } 57 | } 58 | `, 59 | ], 60 | }) 61 | export class PodcastCardComponent { 62 | podcast = input.required(); 63 | } 64 | -------------------------------------------------------------------------------- /angular-hub/src/app/components/event-section.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, input } from '@angular/core'; 2 | import { Event } from '../../models/event.model'; 3 | import { EventCardComponent } from './cards/event-card.component'; 4 | 5 | @Component({ 6 | selector: 'app-event-section', 7 | template: ` 8 | @if (isTitleVisible()) { 9 |

{{ title() }}

10 | } 11 |
    12 | @for (event of events(); track $index) { 13 |
  • 17 | 18 | 19 | 20 |
  • 21 | } 22 |
23 | `, 24 | styles: [ 25 | ` 26 | h3 { 27 | display: flex; 28 | width: 100%; 29 | justify-content: center; 30 | align-items: center; 31 | text-align: center; 32 | } 33 | 34 | h3:before, 35 | h3:after { 36 | content: ''; 37 | border-top: 1px solid; 38 | margin: 0 20px 0 0; 39 | flex: 1 0 20px; 40 | } 41 | 42 | h3:after { 43 | margin: 0 0 0 20px; 44 | } 45 | `, 46 | ], 47 | imports: [EventCardComponent], 48 | standalone: true, 49 | }) 50 | export class EventSectionComponent { 51 | events = input.required(); 52 | title = input.required(); 53 | isTitleVisible = input(true); 54 | } 55 | -------------------------------------------------------------------------------- /angular-hub/src/app/components/message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, computed, input } from '@angular/core'; 2 | import { MessagesModule } from 'primeng/messages'; 3 | 4 | @Component({ 5 | selector: 'app-message', 6 | template: ` 7 | 12 | `, 13 | imports: [MessagesModule], 14 | standalone: true, 15 | }) 16 | export class MessageComponent { 17 | title = input.required(); 18 | severity = input.required(); 19 | 20 | message = computed(() => [ 21 | { severity: this.severity(), detail: this.title() }, 22 | ]); 23 | } 24 | -------------------------------------------------------------------------------- /angular-hub/src/app/components/navigation/navigation.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | } 4 | 5 | .routerLinkActive { 6 | @apply text-secondary underline; 7 | } 8 | 9 | .pwa-install, 10 | .pwa-install:hover span, 11 | li a:hover, 12 | .sidebarActive { 13 | background: rgb(191, 37, 185); 14 | background: linear-gradient( 15 | 148deg, 16 | rgba(191, 37, 185, 1) 23%, 17 | rgba(146, 73, 194, 1) 48%, 18 | rgba(119, 94, 200, 1) 76%, 19 | rgba(86, 120, 207, 1) 86%, 20 | rgba(38, 159, 216, 1) 96%, 21 | rgba(0, 190, 224, 1) 100% 22 | ); 23 | } 24 | 25 | nav { 26 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.75); 27 | clip-path: inset(0px -15px 0px 0px); 28 | } 29 | 30 | .close-button { 31 | position: absolute; 32 | right: 20px; 33 | } 34 | -------------------------------------------------------------------------------- /angular-hub/src/app/components/navigation/navigation.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, viewChild } from '@angular/core'; 2 | import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; 3 | import { ButtonModule } from 'primeng/button'; 4 | import { Sidebar, SidebarModule } from 'primeng/sidebar'; 5 | import { DividerModule } from 'primeng/divider'; 6 | import { PwaService } from '../../services/pwa.service'; 7 | import { IconFieldModule } from 'primeng/iconfield'; 8 | import { InputIconModule } from 'primeng/inputicon'; 9 | import { InputTextModule } from 'primeng/inputtext'; 10 | 11 | @Component({ 12 | selector: 'app-navigation', 13 | templateUrl: './navigation.component.html', 14 | styleUrls: ['./navigation.component.scss'], 15 | standalone: true, 16 | imports: [ 17 | RouterLink, 18 | RouterOutlet, 19 | ButtonModule, 20 | SidebarModule, 21 | RouterLinkActive, 22 | DividerModule, 23 | IconFieldModule, 24 | InputIconModule, 25 | InputTextModule, 26 | ], 27 | }) 28 | export class NavigationComponent { 29 | readonly #pwaService = inject(PwaService); 30 | 31 | sidebarVisible = false; 32 | isInstallButtonVisible = this.#pwaService.isInstallButtonVisible; 33 | 34 | sidebarRef = viewChild.required(Sidebar); 35 | 36 | installPwa(): void { 37 | void this.#pwaService.installPwa(); 38 | } 39 | 40 | closeCallback(e: Event): void { 41 | this.sidebarRef().close(e); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /angular-hub/src/app/components/tag.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-tag', 5 | standalone: true, 6 | changeDetection: ChangeDetectionStrategy.OnPush, 7 | template: ` 8 |
12 | {{ title() }} 13 |
14 | `, 15 | }) 16 | export class TagComponent { 17 | title = input.required(); 18 | color = input('#9051ff'); 19 | } 20 | -------------------------------------------------------------------------------- /angular-hub/src/app/pages/[...page-not-found].page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterLink } from '@angular/router'; 3 | 4 | @Component({ 5 | standalone: true, 6 | imports: [RouterLink], 7 | template: ` 8 |

Page Not Found

9 | 10 | Go Back Home 11 | `, 12 | styles: [ 13 | ` 14 | :host { 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | align-items: center; 19 | height: 100%; 20 | } 21 | `, 22 | ], 23 | }) 24 | export default class PageNotFoundComponent {} 25 | -------------------------------------------------------------------------------- /angular-hub/src/app/pages/callforpapers/index.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { load } from './index.server'; 3 | import { toSignal } from '@angular/core/rxjs-interop'; 4 | import { injectLoad, RouteMeta } from '@analogjs/router'; 5 | import { DividerModule } from 'primeng/divider'; 6 | import { Title } from '@angular/platform-browser'; 7 | import { JsonLdService } from '../../services/json-ld.service'; 8 | import { BannerComponent } from '../../components/banner.component'; 9 | import { DatePipe } from '@angular/common'; 10 | 11 | export const routeMeta: RouteMeta = { 12 | meta: [ 13 | { 14 | name: 'description', 15 | content: 'Curated list of Angular Calls for papers', 16 | }, 17 | ], 18 | }; 19 | 20 | @Component({ 21 | selector: 'app-callforpapers', 22 | standalone: true, 23 | template: ` 24 | 25 |
26 |

Events

27 | 76 | 77 |

Communities

78 | 112 |
113 | `, 114 | imports: [DividerModule, BannerComponent, DatePipe], 115 | }) 116 | export default class CallForPapersComponent { 117 | callForPapers = toSignal(injectLoad(), { requireSync: true }); 118 | 119 | constructor( 120 | private title: Title, 121 | private jsonldService: JsonLdService, 122 | ) { 123 | this.title.setTitle('Angular HUB - Call for papers'); 124 | this.jsonldService.updateJsonLd(this.setJsonLd()); 125 | } 126 | 127 | setJsonLd() { 128 | return { 129 | '@context': 'https://schema.org', 130 | '@type': 'ItemList', 131 | itemListElement: [ 132 | ...this.callForPapers().events.map((event, index) => ({ 133 | '@type': 'ListItem', 134 | position: index + 1, 135 | item: { 136 | '@type': 'Event', 137 | name: event.name, 138 | description: event.name, 139 | startDate: event.date, 140 | location: event.location, 141 | url: event.callForPapersUrl, 142 | }, 143 | })), 144 | ...this.callForPapers().communities.map((community, index) => ({ 145 | '@type': 'ListItem', 146 | position: index + 1, 147 | item: { 148 | '@type': 'Organization', 149 | name: community.name, 150 | description: community.name, 151 | location: community.location, 152 | url: community.callForPapersUrl, 153 | }, 154 | })), 155 | ], 156 | }; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /angular-hub/src/app/pages/callforpapers/index.server.ts: -------------------------------------------------------------------------------- 1 | import { PageServerLoad } from '@analogjs/router'; 2 | import { 3 | CallForPapers, 4 | EventCallForPapers, 5 | } from '../../../models/call-for-papers.model'; 6 | 7 | export const load = async ({ 8 | fetch, 9 | }: PageServerLoad): Promise<{ 10 | events: EventCallForPapers[]; 11 | communities: CallForPapers[]; 12 | }> => { 13 | return await Promise.all([ 14 | fetch('/api/v1/events/callforpapers'), 15 | fetch('/api/v1/communities/callforpapers'), 16 | ]).then(([events, communities]) => ({ events, communities })); 17 | }; 18 | -------------------------------------------------------------------------------- /angular-hub/src/app/pages/communities/index.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, computed, signal } from '@angular/core'; 2 | import { CommunityCardComponent } from '../../components/cards/community-card.component'; 3 | import { toSignal } from '@angular/core/rxjs-interop'; 4 | import { injectLoad, RouteMeta } from '@analogjs/router'; 5 | import { load } from './index.server'; 6 | import { Title } from '@angular/platform-browser'; 7 | import { JsonLdService } from '../../services/json-ld.service'; 8 | import { DropdownModule } from 'primeng/dropdown'; 9 | import { FormsModule } from '@angular/forms'; 10 | import { BannerComponent } from '../../components/banner.component'; 11 | import { CheckboxModule } from 'primeng/checkbox'; 12 | 13 | export const routeMeta: RouteMeta = { 14 | meta: [ 15 | { 16 | name: 'description', 17 | content: 'Curated list of Angular communities', 18 | }, 19 | ], 20 | }; 21 | 22 | @Component({ 23 | selector: 'app-communities', 24 | standalone: true, 25 | imports: [ 26 | CommunityCardComponent, 27 | DropdownModule, 28 | FormsModule, 29 | BannerComponent, 30 | CheckboxModule, 31 | ], 32 | template: ` 33 | 34 |
35 |
36 | 46 | 47 | 48 |
    49 | @for (community of communitiesWithUpcomingEvents(); track community) { 50 |
  • 51 | 52 |
  • 53 | } 54 | @for (community of activeCommunities(); track community) { 55 |
  • 56 | 57 |
  • 58 | } 59 | @for (community of inactiveCommunities(); track community) { 60 |
  • 61 | 62 |
  • 63 | } 64 |
65 |
66 | `, 67 | }) 68 | export default class CommunitiesComponent { 69 | communities = toSignal(injectLoad(), { requireSync: true }); 70 | selectedCountry = signal(null); 71 | countries = computed(() => 72 | this.communities() 73 | .map((community) => community.location) 74 | .reduce((acc, curr) => { 75 | const location = curr 76 | ? curr.includes(',') 77 | ? curr.split(',').at(-1) 78 | : curr 79 | : ''; 80 | if (location && !acc.includes(location.trim())) { 81 | acc.push(location.trim()); 82 | } 83 | return acc; 84 | }, []) 85 | .sort((a, b) => 86 | a.toLocaleUpperCase().localeCompare(b.toLocaleUpperCase()), 87 | ), 88 | ); 89 | 90 | communitiesWithUpcomingEvents = computed(() => 91 | this.filteredCommunities().filter((community) => 92 | community.events?.some((event) => { 93 | const eventDate = new Date(event.date); 94 | return eventDate.getTime() > new Date().getTime(); 95 | }), 96 | ), 97 | ); 98 | 99 | activeCommunities = computed(() => { 100 | return this.filteredCommunities() 101 | .filter((community) => { 102 | const inactivityLimit = new Date(); 103 | inactivityLimit.setMonth(inactivityLimit.getMonth() - 6); 104 | 105 | const newestEvent = community.events?.sort( 106 | (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), 107 | )[0]; 108 | 109 | return newestEvent 110 | ? new Date(newestEvent.date).getTime() > inactivityLimit.getTime() && 111 | community.type === 'meetup' 112 | : true; 113 | }) 114 | .filter((community) => { 115 | return ( 116 | !community.events?.some((event) => { 117 | const eventDate = new Date(event.date); 118 | return eventDate.getTime() > new Date().getTime(); 119 | }) || community.type !== 'meetup' 120 | ); 121 | }); 122 | }); 123 | 124 | inactiveCommunities = computed(() => 125 | this.filteredCommunities() 126 | .filter((community) => community.events && community.events.length > 0) 127 | .filter((community) => { 128 | const inactivityLimit = new Date(); 129 | inactivityLimit.setMonth(inactivityLimit.getMonth() - 6); 130 | 131 | const newestEvent = community.events.sort( 132 | (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), 133 | )[0]; 134 | 135 | return ( 136 | new Date(newestEvent.date).getTime() < inactivityLimit.getTime() && 137 | community.type === 'meetup' 138 | ); 139 | }) 140 | .sort((a, b) => { 141 | const newestEventA = a.events.sort( 142 | (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), 143 | )[0]; 144 | const newestEventB = b.events.sort( 145 | (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), 146 | )[0]; 147 | 148 | return ( 149 | new Date(newestEventB.date).getTime() - 150 | new Date(newestEventA.date).getTime() 151 | ); 152 | }), 153 | ); 154 | 155 | filteredCommunities = computed(() => 156 | this.communities().filter((community) => 157 | this.selectedCountry() 158 | ? community.location?.includes(this.selectedCountry() ?? '') 159 | : true, 160 | ), 161 | ); 162 | 163 | constructor( 164 | private title: Title, 165 | private jsonldService: JsonLdService, 166 | ) { 167 | this.title.setTitle('Angular HUB - Communities'); 168 | this.jsonldService.updateJsonLd(this.setJsonLd()); 169 | } 170 | 171 | setJsonLd() { 172 | return { 173 | '@context': 'https://schema.org', 174 | '@type': 'ItemList', 175 | itemListElement: this.communities().map((community, index) => ({ 176 | '@type': 'ListItem', 177 | position: index + 1, 178 | item: { 179 | '@type': 'Organization', 180 | name: community.name, 181 | url: community.websiteUrl, 182 | ...(community.location 183 | ? { 184 | address: { 185 | '@type': 'PostalAddress', 186 | addressLocality: community.location, 187 | }, 188 | } 189 | : {}), 190 | knowsAbout: 'Angular', 191 | }, 192 | })), 193 | }; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /angular-hub/src/app/pages/communities/index.server.ts: -------------------------------------------------------------------------------- 1 | import { PageServerLoad } from '@analogjs/router'; 2 | import { Community } from '../../../models/community.model'; 3 | 4 | export const load = async ({ fetch }: PageServerLoad): Promise => { 5 | return await fetch('/api/v1/communities'); 6 | }; 7 | -------------------------------------------------------------------------------- /angular-hub/src/app/pages/index.server.ts: -------------------------------------------------------------------------------- 1 | import { PageServerLoad } from '@analogjs/router'; 2 | import { Event } from '../../models/event.model'; 3 | 4 | export const load = async ({ fetch }: PageServerLoad): Promise => { 5 | return await fetch('/api/v1/events/upcoming'); 6 | }; 7 | -------------------------------------------------------------------------------- /angular-hub/src/app/pages/podcasts/index.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { toSignal } from '@angular/core/rxjs-interop'; 4 | import { PodcastCardComponent } from '../../components/cards/podcast-card.component'; 5 | import { injectLoad, RouteMeta } from '@analogjs/router'; 6 | 7 | import { load } from './index.server'; 8 | import { JsonLdService } from '../../services/json-ld.service'; 9 | import { Title } from '@angular/platform-browser'; 10 | import { BannerComponent } from '../../components/banner.component'; 11 | 12 | export const routeMeta: RouteMeta = { 13 | meta: [ 14 | { 15 | name: 'description', 16 | content: 'Curated list of Angular Talks', 17 | }, 18 | ], 19 | }; 20 | 21 | @Component({ 22 | selector: 'app-podcasts', 23 | standalone: true, 24 | imports: [PodcastCardComponent, FormsModule, BannerComponent], 25 | template: ` 26 | 27 |
    30 | @for (podcast of podcasts(); track podcast.name) { 31 |
  • 32 | 33 |
  • 34 | } 35 |
36 | `, 37 | }) 38 | export default class PodcastsComponent { 39 | podcasts = toSignal(injectLoad(), { requireSync: true }); 40 | 41 | constructor( 42 | private title: Title, 43 | private jsonldService: JsonLdService, 44 | ) { 45 | this.title.setTitle('Angular HUB - Podcasts'); 46 | this.jsonldService.updateJsonLd(this.setJsonLd()); 47 | } 48 | 49 | setJsonLd() { 50 | return { 51 | '@context': 'https://schema.org', 52 | '@type': 'ItemList', 53 | itemListElement: this.podcasts().map((podcast, index) => ({ 54 | '@type': 'ListItem', 55 | position: index + 1, 56 | item: { 57 | '@type': 'AudioObject', 58 | name: podcast.name, 59 | description: podcast.name, 60 | url: podcast.url, 61 | }, 62 | })), 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /angular-hub/src/app/pages/podcasts/index.server.ts: -------------------------------------------------------------------------------- 1 | import { PageServerLoad } from '@analogjs/router'; 2 | import { Podcast } from '../../../models/podcast.model'; 3 | 4 | export const load = async ({ fetch }: PageServerLoad): Promise => { 5 | return await fetch('/api/v1/podcasts'); 6 | }; 7 | -------------------------------------------------------------------------------- /angular-hub/src/app/pages/watch-party/index.page.ts: -------------------------------------------------------------------------------- 1 | import { RouteMeta } from '@analogjs/router'; 2 | import { afterNextRender, Component, signal } from '@angular/core'; 3 | import { DialogModule } from 'primeng/dialog'; 4 | import { InputTextModule } from 'primeng/inputtext'; 5 | import { TableModule } from 'primeng/table'; 6 | import { BannerComponent } from '../../components/banner.component'; 7 | 8 | type WatchParty = { 9 | location: string; 10 | host: string; 11 | organizer: string; 12 | link: string; 13 | }; 14 | 15 | export const routeMeta: RouteMeta = { 16 | meta: [ 17 | { 18 | name: 'description', 19 | content: 'Angular release watch parties', 20 | }, 21 | { 22 | property: 'og:title', 23 | content: 'Angular release watch parties', 24 | }, 25 | { 26 | property: 'og:image', 27 | content: '/assets/images/og-image.webp', 28 | }, 29 | { 30 | name: 'twitter:image', 31 | content: '/assets/images/og-image.webp', 32 | }, 33 | ], 34 | }; 35 | 36 | @Component({ 37 | selector: 'app-events', 38 | standalone: true, 39 | template: ` 40 | 44 | 45 |
46 | 62 | 63 | 76 | 77 | @if (show()) { 78 | 107 | } 108 |
109 | `, 110 | styles: [ 111 | ` 112 | :host { 113 | display: flex; 114 | flex-direction: column; 115 | align-items: stretch; 116 | } 117 | 118 | .highlighted-btn { 119 | background: rgb(191, 37, 185); 120 | } 121 | `, 122 | ], 123 | imports: [BannerComponent, TableModule, DialogModule, InputTextModule], 124 | }) 125 | export default class EventsComponent { 126 | show = signal(false); 127 | // TODO: Replace with proper json file 128 | parties: WatchParty[] = [ 129 | { 130 | location: 'Roma, Italy', 131 | host: 'David Passafaro', 132 | organizer: 'NG Rome', 133 | link: 'https://www.eventbrite.it/e/biglietti-angular-v20-release-watch-party-1370987904729', 134 | }, 135 | { 136 | location: 'Online', 137 | host: '', 138 | organizer: 'Angular Community Meetup', 139 | link: 'https://www.meetup.com/angularcommunity/events/307613608', 140 | }, 141 | { 142 | location: 'Online (Spanish)', 143 | host: '', 144 | organizer: 'Angular Community Meetup', 145 | link: 'https://www.meetup.com/angularcommunity/events/307613725', 146 | }, 147 | { 148 | location: 'Online', 149 | host: '', 150 | organizer: 'Santosh Yadav', 151 | link: 'https://www.youtube.com/watch?v=spiuVrYa7v8', 152 | }, 153 | { 154 | location: 'Online', 155 | host: '', 156 | organizer: 'Muhammad Ahsan Ayaz', 157 | link: 'https://www.youtube.com/watch?v=zlCGsg7FgIY', 158 | }, 159 | ]; 160 | 161 | constructor() { 162 | afterNextRender(() => { 163 | this.show.set(true); 164 | }); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /angular-hub/src/app/services/json-ld.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { DOCUMENT } from '@angular/common'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class JsonLdService { 8 | constructor(@Inject(DOCUMENT) private document: Document) {} 9 | 10 | updateJsonLd(jsonLd: unknown) { 11 | const scriptTag = this.document.querySelector( 12 | 'script[type="application/ld+json"]', 13 | ); 14 | scriptTag!.innerHTML = JSON.stringify(jsonLd); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /angular-hub/src/app/services/pwa.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, signal } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class PwaService { 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | private promptEvent: any; 9 | isInstallButtonVisible = signal(false); 10 | 11 | constructor() {} 12 | 13 | public initPwaPrompt() { 14 | window.addEventListener('beforeinstallprompt', (event: Event) => { 15 | event.preventDefault(); 16 | this.promptEvent = event; 17 | this.isInstallButtonVisible.set(true); 18 | }); 19 | } 20 | 21 | public async installPwa() { 22 | this.promptEvent.prompt(); 23 | const { outcome } = await this.promptEvent.userChoice; 24 | this.isInstallButtonVisible.set(false); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /angular-hub/src/app/services/supabase.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AuthSession, SupabaseClient } from '@supabase/supabase-js'; 3 | import { createBrowserClient } from '@supabase/ssr'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class SupabaseService { 9 | public supabase: SupabaseClient = createBrowserClient( 10 | import.meta.env['VITE_SUPABASE_URL'], 11 | import.meta.env['VITE_SUPABASE_KEY'], 12 | ); 13 | _session: AuthSession | null = null; 14 | 15 | init(): void { 16 | this.supabase.auth.onAuthStateChange((event, session) => { 17 | this._session = session; 18 | }); 19 | } 20 | 21 | async signInWithGithub(): Promise { 22 | await this.supabase.auth.signInWithOAuth({ 23 | provider: 'github', 24 | options: { 25 | redirectTo: window.location.origin + '/api/v1/auth/callback', 26 | }, 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /angular-hub/src/main.server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/node'; 2 | import '@angular/platform-server/init'; 3 | 4 | import { enableProdMode } from '@angular/core'; 5 | import { renderApplication } from '@angular/platform-server'; 6 | import { bootstrapApplication } from '@angular/platform-browser'; 7 | 8 | import { AppComponent } from './app/app.component'; 9 | import { config } from './app/app.config.server'; 10 | 11 | if (import.meta.env.PROD) { 12 | enableProdMode(); 13 | } 14 | 15 | const bootstrap = () => bootstrapApplication(AppComponent, config); 16 | 17 | export default async function render(url: string, document: string) { 18 | const html = await renderApplication(bootstrap, { 19 | document, 20 | url, 21 | }); 22 | return html; 23 | } 24 | -------------------------------------------------------------------------------- /angular-hub/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; 2 | import { bootstrapApplication } from '@angular/platform-browser'; 3 | 4 | import { AppComponent } from './app/app.component'; 5 | import { appConfig } from './app/app.config'; 6 | 7 | bootstrapApplication(AppComponent, appConfig).catch((err) => 8 | console.error(err), 9 | ); 10 | -------------------------------------------------------------------------------- /angular-hub/src/models/call-for-papers.model.ts: -------------------------------------------------------------------------------- 1 | export interface CallForPapers { 2 | name: string; 3 | type: 'workshop' | 'conference' | 'meetup' | 'other'; 4 | logo: string | null; 5 | location: string | null; 6 | callForPapersUrl: string; 7 | } 8 | 9 | export type EventCallForPapers = CallForPapers & { 10 | date: string; 11 | callForPapersDueDate: string | null; 12 | isRemote: boolean; 13 | isOnsite: boolean; 14 | }; 15 | -------------------------------------------------------------------------------- /angular-hub/src/models/community.model.ts: -------------------------------------------------------------------------------- 1 | import { Event } from './event.model'; 2 | 3 | export interface Community { 4 | name: string; 5 | type: 'workshop' | 'conference' | 'meetup' | 'other'; 6 | // change to nested 2 properties (city / country) || Online 7 | location: string | null; 8 | logo: string | null; 9 | eventsUrl: string | null; 10 | websiteUrl: string | null; 11 | organizersUrl: string | null; 12 | blueskyUrl: string | null; 13 | xUrl: string | null; 14 | linkedinUrl: string | null; 15 | youtubeUrl: string | null; 16 | twitchUrl: string | null; 17 | callForPapersUrl: string | null; 18 | events: Event[]; 19 | } 20 | -------------------------------------------------------------------------------- /angular-hub/src/models/event-type.model.ts: -------------------------------------------------------------------------------- 1 | export type EventType = 'workshop' | 'conference' | 'meetup' | 'other'; 2 | -------------------------------------------------------------------------------- /angular-hub/src/models/event.model.ts: -------------------------------------------------------------------------------- 1 | import { Community } from './community.model'; 2 | 3 | export interface Event { 4 | name: string | null; 5 | type: 'workshop' | 'conference' | 'meetup' | 'other'; 6 | location: string | null; 7 | url: string | null; 8 | date: string; 9 | endDate?: string; 10 | language: string; 11 | isFree: boolean; 12 | isRemote: boolean; 13 | isOnsite: boolean; 14 | callForPapersUrl: string | null; 15 | callForPapersDueDate: string | null; 16 | community?: Community; 17 | toBeAnnounced?: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /angular-hub/src/models/podcast.model.ts: -------------------------------------------------------------------------------- 1 | export interface Podcast { 2 | name: string; 3 | url: string; 4 | logo: string; 5 | language: string; 6 | } 7 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/.gitkeep -------------------------------------------------------------------------------- /angular-hub/src/public/assets/data/podcast.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Adventures in Angular", 4 | "url": "https://topenddevs.com/podcasts/adventures-in-angular", 5 | "logo": "assets/logos/adventures-in-angular.webp", 6 | "language": "English" 7 | }, 8 | { 9 | "name": "Angular Air", 10 | "url": "https://www.youtube.com/@AngularAirPodcast", 11 | "logo": "assets/logos/angular-air.webp", 12 | "language": "English" 13 | }, 14 | { 15 | "name": "Angular Master Podcast", 16 | "url": "https://www.youtube.com/playlist?list=PLYJFRoKhU5SNcu5GBjIn4X3oVpy4fP1wV", 17 | "logo": "assets/logos/angular-master-podcast.webp", 18 | "language": "English" 19 | }, 20 | { 21 | "name": "The Angular Plus Show", 22 | "url": "https://open.spotify.com/show/1PrLErQHBqBhZsRV1KHhGM", 23 | "logo": "assets/logos/angular-plus-show.webp", 24 | "language": "English" 25 | }, 26 | { 27 | "name": "Angular Space", 28 | "url": "https://x.com/i/communities/1726237719762714636", 29 | "logo": "assets/logos/angular-space.webp", 30 | "language": "English" 31 | }, 32 | { 33 | "name": "Angularidades", 34 | "url": "https://www.youtube.com/@angularidades", 35 | "logo": "assets/logos/angularidades.webp", 36 | "language": "Spanish" 37 | }, 38 | { 39 | "name": "Ng-News", 40 | "url": "https://www.youtube.com/@ng-news", 41 | "logo": "assets/logos/ng-news.webp", 42 | "language": "English" 43 | }, 44 | { 45 | "name": "Angular Rocks", 46 | "url": "https://angularrocks.com/", 47 | "logo": "assets/logos/angular-rocks.webp", 48 | "language": "English" 49 | }, 50 | { 51 | "name": "Angular Experts", 52 | "url": "https://angularexperts.io/podcasts", 53 | "logo": "assets/logos/angular-experts.webp", 54 | "language": "English" 55 | }, 56 | { 57 | "name": "Angular Catch-Up", 58 | "url": "https://angular-catch-up.podbean.com/", 59 | "logo": "assets/logos/angular-catch-up.jpg", 60 | "language": "English" 61 | }, 62 | { 63 | "name": "Angular Japan User Group | Angular 日本ユーザー会", 64 | "url": "https://www.youtube.com/@ng_japan", 65 | "logo": "assets/logos/ng-jp.svg", 66 | "language": "Japanese" 67 | }, 68 | { 69 | "name": "Startup Angular", 70 | "url": "https://www.youtube.com/@StartupAngular", 71 | "logo": "assets/logos/startup-angular.png", 72 | "language": "Japanese" 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/favicon.ico -------------------------------------------------------------------------------- /angular-hub/src/public/assets/fonts/Luciole-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/fonts/Luciole-Regular.ttf -------------------------------------------------------------------------------- /angular-hub/src/public/assets/fonts/PixelifySans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/fonts/PixelifySans-Regular.ttf -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/bluesky-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/event-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/group-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/icons/icon-36x36.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/icons/icon-48x48.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/linkedin-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | linkedin [#161] 6 | Created with Sketch. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/website_link-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/x-twitter-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/icons/youtube-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | youtube [#168] 6 | Created with Sketch. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/images/hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/images/hero.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/images/logo.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/images/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/images/logo.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/images/og-image.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/images/og-image.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/adventures-in-angular.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/adventures-in-angular.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-air.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-air.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-belgrade.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-belgrade.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-catch-up.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-catch-up.jpg -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-connect.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-devs-france.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-devs-france.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-experts.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-experts.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-love.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-love.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-master-podcast.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-master-podcast.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-plus-show.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-plus-show.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-rocks.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-rocks.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-sp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-sp.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-space.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-space.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-toronto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-toronto.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angular-warsaw.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angular-warsaw.jpeg -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/angularidades.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/angularidades.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/dutch-angular-group.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 26 | 30 | 31 | 34 | 38 | 39 | 42 | 46 | 47 | 50 | 54 | 55 | 58 | 62 | 63 | 74 | 83 | 92 | 101 | 110 | 111 | 129 | 131 | 132 | 134 | image/svg+xml 135 | 137 | 138 | 139 | 140 | 144 | 147 | 152 | 157 | 158 | 161 | 164 | 167 | 172 | 177 | 182 | 187 | 192 | 193 | 194 | 195 | 196 | 203 | 204 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/ng-be.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/ng-be.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/ng-gunma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/ng-gunma.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/ng-jp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/ng-kato.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/ng-news.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/ng-news.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/ng-poznan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/ng-poznan.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/official-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/official-logo.webp -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/paris-angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/paris-angular.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/logos/startup-angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/angular-hub/src/public/assets/logos/startup-angular.png -------------------------------------------------------------------------------- /angular-hub/src/public/assets/nav/icons/swagger-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | Swagger icon -------------------------------------------------------------------------------- /angular-hub/src/public/assets/swagger/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Angular Communities API 4 | version: 1.0.0 5 | description: Find Angular Event and Communities 6 | contact: 7 | name: Angular HUB 8 | url: https://angular-hub.com 9 | tags: 10 | - name: Communities 11 | - name: Events 12 | - name: Podcasts 13 | servers: 14 | - url: https://angular-hub.com/api/v1 15 | paths: 16 | /communities: 17 | get: 18 | summary: Get a list of Angular communities 19 | tags: 20 | - Communities 21 | responses: 22 | '200': 23 | description: A list of Angular events and communities 24 | content: 25 | application/json: 26 | schema: 27 | type: array 28 | items: 29 | type: object 30 | properties: 31 | name: 32 | type: string 33 | type: 34 | type: string 35 | location: 36 | type: string 37 | logo: 38 | type: string 39 | eventsUrl: 40 | type: string 41 | websiteUrl: 42 | type: string 43 | organizersUrl: 44 | type: string 45 | blueskyUrl: 46 | type: string 47 | xUrl: 48 | type: string 49 | linkedinUrl: 50 | type: string 51 | youtubeUrl: 52 | type: string 53 | twitchUrl: 54 | type: string 55 | callForPapersUrl: 56 | type: string 57 | events: 58 | type: array 59 | items: 60 | type: object 61 | properties: 62 | name: 63 | type: string 64 | type: 65 | type: string 66 | location: 67 | type: string 68 | date: 69 | type: string 70 | language: 71 | type: string 72 | isFree: 73 | type: boolean 74 | isRemote: 75 | type: boolean 76 | isOnsite: 77 | type: boolean 78 | callForPapersUrl: 79 | type: string 80 | callForPapersDueDate: 81 | type: string 82 | url: 83 | type: string 84 | /events/upcoming: 85 | get: 86 | summary: Get a list of upcoming Angular events 87 | tags: 88 | - Events 89 | responses: 90 | '200': 91 | description: A list of upcoming Angular events 92 | content: 93 | application/json: 94 | schema: 95 | type: array 96 | items: 97 | type: object 98 | properties: 99 | name: 100 | type: string 101 | type: 102 | type: string 103 | location: 104 | type: string 105 | date: 106 | type: string 107 | language: 108 | type: string 109 | isFree: 110 | type: boolean 111 | isRemote: 112 | type: boolean 113 | isOnsite: 114 | type: boolean 115 | callForPapersUrl: 116 | type: string 117 | callForPapersDueDate: 118 | type: string 119 | url: 120 | type: string 121 | /events/callforpapers: 122 | get: 123 | summary: Get a list of Angular communities with open call for papers 124 | tags: 125 | - Events 126 | parameters: 127 | - name: type 128 | in: query 129 | description: Event type (meetup, conference, workshop) 130 | required: false 131 | schema: 132 | type: string 133 | responses: 134 | '200': 135 | description: A list of Angular events with open call for papers 136 | content: 137 | application/json: 138 | schema: 139 | type: array 140 | items: 141 | type: object 142 | properties: 143 | name: 144 | type: string 145 | type: 146 | type: string 147 | location: 148 | type: string 149 | logo: 150 | type: string 151 | eventsUrl: 152 | type: string 153 | websiteUrl: 154 | type: string 155 | organizersUrl: 156 | type: string 157 | blueskyUrl: 158 | type: string 159 | xUrl: 160 | type: string 161 | linkedinUrl: 162 | type: string 163 | youtubeUrl: 164 | type: string 165 | twitchUrl: 166 | type: string 167 | callForPapersUrl: 168 | type: string 169 | events: 170 | type: array 171 | items: 172 | type: object 173 | properties: 174 | name: 175 | type: string 176 | type: 177 | type: string 178 | location: 179 | type: string 180 | date: 181 | type: string 182 | language: 183 | type: string 184 | isFree: 185 | type: boolean 186 | isRemote: 187 | type: boolean 188 | isOnsite: 189 | type: boolean 190 | callForPapersUrl: 191 | type: string 192 | callForPapersDueDate: 193 | type: string 194 | url: 195 | type: string 196 | /podcasts: 197 | get: 198 | summary: Get a list of Angular podcasts 199 | tags: 200 | - Podcasts 201 | responses: 202 | '200': 203 | description: A list of Angular podcasts 204 | content: 205 | application/json: 206 | schema: 207 | type: array 208 | items: 209 | type: object 210 | properties: 211 | name: 212 | type: string 213 | url: 214 | type: string 215 | logo: 216 | type: string 217 | language: 218 | type: string 219 | -------------------------------------------------------------------------------- /angular-hub/src/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Sitemap: https://angular-hub.com/sitemap.xml 5 | -------------------------------------------------------------------------------- /angular-hub/src/server/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { deleteCookie, H3Event, parseCookies, setCookie } from 'h3'; 2 | import { createServerClient } from '@supabase/ssr'; 3 | 4 | export function nitroServerClient(event: H3Event) { 5 | const cookies = parseCookies(event); 6 | 7 | return createServerClient( 8 | import.meta.env['VITE_SUPABASE_URL'], 9 | import.meta.env['VITE_SUPABASE_KEY'], 10 | { 11 | cookies: { 12 | get: (name) => { 13 | return cookies[name]; 14 | }, 15 | set: async (name, value, options) => { 16 | setCookie(event, name, value, options); 17 | }, 18 | remove: (name) => { 19 | deleteCookie(event, name); 20 | }, 21 | }, 22 | }, 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/auth/callback.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, getQuery, sendRedirect } from 'h3'; 2 | import { nitroServerClient } from '../../../auth'; 3 | 4 | export default defineEventHandler(async (event) => { 5 | try { 6 | const { code, next }: { code: string; next: string } = getQuery(event); 7 | 8 | if (code) { 9 | const serverClient = nitroServerClient(event); 10 | await serverClient.auth.exchangeCodeForSession(code); 11 | } 12 | 13 | return sendRedirect(event, next || '/', 302); 14 | } catch (e) { 15 | // TODO to be handled properly 16 | console.error('callback error', e); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/communities/callforpapers.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3'; 2 | import communities from '../../../../public/assets/data/community.json'; 3 | import { Community } from '../../../../models/community.model'; 4 | import { CommunityListSchema } from '../../../schemas/community.schema'; 5 | import { parse } from 'valibot'; 6 | 7 | export default defineEventHandler((event) => { 8 | try { 9 | parse(CommunityListSchema, communities); 10 | 11 | return communities 12 | .filter( 13 | (community: Community) => 14 | community.callForPapersUrl && community.type === 'meetup', 15 | ) 16 | .map(({ events, ...community }) => ({ ...community })); 17 | } catch (error) { 18 | throw new Error('Invalid community data format'); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/communities/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3'; 2 | import communities from '../../../../public/assets/data/community.json'; 3 | import { parse } from 'valibot'; 4 | import { CommunityListSchema } from '../../../schemas/community.schema'; 5 | 6 | export default defineEventHandler(() => { 7 | try { 8 | parse(CommunityListSchema, communities); 9 | return communities.filter( 10 | (community) => 11 | community.type !== 'workshop' && community.type !== 'other', 12 | ); 13 | } catch (error) { 14 | throw new Error('Invalid community data format'); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/community.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3'; 2 | import communities from '../../../public/assets/data/community.json'; 3 | import { parse } from 'valibot'; 4 | import { CommunityListSchema } from '../../schemas/community.schema'; 5 | 6 | // TODO duplicate to /communities, keep it as promoted on social media 7 | export default defineEventHandler(() => { 8 | try { 9 | parse(CommunityListSchema, communities); 10 | return communities; 11 | } catch (error) { 12 | throw new Error('Invalid community data format'); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/events/callforpapers/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3'; 2 | import communities from '../../../../../public/assets/data/community.json'; 3 | import { Community } from '../../../../../models/community.model'; 4 | import { CommunityListSchema } from '../../../../schemas/community.schema'; 5 | import { parse } from 'valibot'; 6 | import { Event } from '../../../../../models/event.model'; 7 | import { EventCallForPapers } from '../../../../../models/call-for-papers.model'; 8 | 9 | export default defineEventHandler((event) => { 10 | try { 11 | parse(CommunityListSchema, communities); 12 | 13 | return communities 14 | .reduce((acc: any[], community: Community) => { 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | const events = community.events.reduce( 17 | (acc: EventCallForPapers[], event: Event) => { 18 | if (event.callForPapersUrl) { 19 | if ( 20 | event.callForPapersDueDate && 21 | new Date(event.callForPapersDueDate) < new Date() 22 | ) { 23 | return acc; 24 | } 25 | 26 | if (event.date && new Date(event.date) < new Date()) { 27 | return acc; 28 | } 29 | 30 | acc.push({ 31 | name: event.name ?? community.name, 32 | type: event.type, 33 | location: event.location, 34 | logo: community.logo, 35 | callForPapersUrl: event.callForPapersUrl, 36 | date: event.date, 37 | callForPapersDueDate: event.callForPapersDueDate, 38 | isRemote: event.isRemote, 39 | isOnsite: event.isOnsite, 40 | }); 41 | } 42 | return acc; 43 | }, 44 | [], 45 | ); 46 | return [...acc, events]; 47 | }, []) 48 | .flat() 49 | .sort( 50 | (a, b) => 51 | new Date(a.callForPapersDueDate).getTime() - 52 | new Date(b.callForPapersDueDate).getTime(), 53 | ); 54 | } catch (error) { 55 | console.error(error); 56 | throw new Error('Invalid community data format'); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/events/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, getQuery } from 'h3'; 2 | import communities from '../../../../public/assets/data/community.json'; 3 | import { Event } from '../../../../models/event.model'; 4 | import { Community } from '../../../../models/community.model'; 5 | import { CommunityListSchema } from '../../../schemas/community.schema'; 6 | import { parse } from 'valibot'; 7 | 8 | export default defineEventHandler((evt) => { 9 | try { 10 | parse(CommunityListSchema, communities); 11 | return communities 12 | .map((community: Community) => { 13 | const { events, ...communityMeta } = community; 14 | return community.events 15 | .map((event: Event) => { 16 | return { 17 | ...event, 18 | name: event.name ?? communityMeta.name, 19 | community: communityMeta, 20 | }; 21 | }) 22 | .flat(); 23 | }) 24 | .flat() 25 | .filter((event: Event) => applyQueryFilter(event, getQuery(evt))) 26 | .sort( 27 | (a: Event, b: Event) => 28 | new Date(a.date).getTime() - new Date(b.date).getTime(), 29 | ); 30 | } catch (error) { 31 | throw new Error('Invalid community data format'); 32 | } 33 | }); 34 | 35 | function applyQueryFilter(event: Event, query): boolean { 36 | for (const key in query) { 37 | if (event[key] !== Boolean(query[key])) { 38 | return false; 39 | } 40 | } 41 | return true; 42 | } 43 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/events/past/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3'; 2 | import communities from '../../../../../public/assets/data/community.json'; 3 | import { parse } from 'valibot'; 4 | import { CommunityListSchema } from '../../../../schemas/community.schema'; 5 | import { isPast, isToday } from 'date-fns'; 6 | 7 | export default defineEventHandler(() => { 8 | try { 9 | parse(CommunityListSchema, communities); 10 | const events = communities 11 | .map((community) => { 12 | const { events, ...communityMeta } = community; 13 | return community.events 14 | .map((event) => { 15 | return { 16 | ...event, 17 | name: event.name ?? communityMeta.name, 18 | community: communityMeta, 19 | }; 20 | }) 21 | .flat(); 22 | }) 23 | .flat() 24 | .filter((event) => { 25 | const date = new Date(event.date); 26 | return isToday(date) || isPast(date); 27 | }) 28 | .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); 29 | 30 | return events; 31 | } catch (error) { 32 | throw new Error('Invalid community data format'); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/events/upcoming/index.get.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3'; 2 | import communities from '../../../../../public/assets/data/community.json'; 3 | import { CommunityListSchema } from '../../../../schemas/community.schema'; 4 | import { parse } from 'valibot'; 5 | import { isFuture, isToday } from 'date-fns'; 6 | 7 | export default defineEventHandler(() => { 8 | try { 9 | const parsed = parse(CommunityListSchema, communities); 10 | const events = communities 11 | .map((community) => { 12 | const { events, ...communityMeta } = community; 13 | return community.events 14 | .map((event) => { 15 | return { 16 | ...event, 17 | name: event.name ?? communityMeta.name, 18 | community: communityMeta, 19 | }; 20 | }) 21 | .flat(); 22 | }) 23 | .flat() 24 | .filter((event) => { 25 | const date = new Date(event.date); 26 | return isToday(date) || isFuture(date) || event.toBeAnnounced; 27 | }) 28 | .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); 29 | 30 | return events; 31 | } catch (error) { 32 | throw new Error(error); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/og-image.ts: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from '@analogjs/content/og'; 2 | import { Event } from '../../../models/event.model'; 3 | 4 | export default defineEventHandler(async (event) => { 5 | const data = await fetch('https://angular-hub.com/api/v1/events/upcoming'); 6 | const events: Event[] = await data.json(); 7 | const eventLength = events.length; 8 | const topEvents = events.slice(0, 3); 9 | 10 | const fontFile = await fetch( 11 | 'https://og-playground.vercel.app/inter-latin-ext-700-normal.woff', 12 | ); 13 | const fontData: ArrayBuffer = await fontFile.arrayBuffer(); 14 | const query = getQuery(event); // query params 15 | 16 | const template = ` 17 |
18 |
19 | OG Playground 20 |

ANGULAR HUB

21 |
22 |

Discover ${eventLength} upcoming community events

23 |
24 | `; 25 | 26 | const cards = ` 27 | ${topEvents.map((event) => { 28 | return `

29 | ${event.name} 30 |

31 |
32 |
33 | 41 |
42 |
43 | 44 | ${event.date} 45 | ${event.endDate ? '- ' + event.endDate : ''} 46 | 47 |
48 |
`; 49 | })} 50 | `; 51 | 52 | return new ImageResponse(template, { 53 | debug: true, // disable caching 54 | fonts: [ 55 | { 56 | name: 'Inter Latin', 57 | data: fontData, 58 | style: 'normal', 59 | }, 60 | ], 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/podcasts.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, getQuery } from 'h3'; 2 | import podcasts from '../../../public/assets/data/podcast.json'; 3 | import { parse } from 'valibot'; 4 | import { PodcastListSchema } from '../../schemas/podcast.schema'; 5 | import { CommunityListSchema } from '../../schemas/community.schema'; 6 | import communities from '../../../public/assets/data/community.json'; 7 | 8 | export default defineEventHandler((evt) => { 9 | const { order } = getQuery(evt); 10 | try { 11 | parse(PodcastListSchema, podcasts); 12 | parse(CommunityListSchema, communities); 13 | 14 | const communitiesMedia = communities 15 | .filter((community) => community.mediaChannel) 16 | .map((community) => ({ 17 | name: community.name, 18 | url: community.mediaChannel.url, 19 | logo: community.logo, 20 | language: community.mediaChannel.language, 21 | })); 22 | 23 | return [...podcasts, ...communitiesMedia].sort((a, b) => 24 | orderPredicate(order, a.name, b.name), 25 | ); 26 | } catch (error) { 27 | throw new Error('Invalid podcast data format'); 28 | } 29 | }); 30 | 31 | function orderPredicate(order: 'ASC' | 'DESC' = 'DESC', a: string, b: string) { 32 | if (order === 'DESC') { 33 | return a.localeCompare(b); 34 | } else { 35 | return b.localeCompare(a); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /angular-hub/src/server/routes/v1/swagger.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler } from 'h3'; 2 | 3 | export default defineEventHandler(() => { 4 | return ` 5 | 6 | 7 | 8 | 9 | Angular Hub 10 | 11 | 12 | 13 | 17 | 21 | 22 | 66 | 67 | 68 |
69 | 70 |

ANGULAR HUB

71 |
72 |
73 | 74 | 82 | 83 | 84 | 85 | `; 86 | }); 87 | -------------------------------------------------------------------------------- /angular-hub/src/server/schemas/community.schema.ts: -------------------------------------------------------------------------------- 1 | import { array, nullable, object, picklist, string } from 'valibot'; 2 | import { EventSchema } from './event.schema'; 3 | 4 | export const CommunitySchema = object({ 5 | name: string(), 6 | type: picklist(['workshop', 'conference', 'meetup', 'other']), 7 | location: nullable(string()), 8 | logo: nullable(string()), 9 | eventsUrl: nullable(string()), 10 | websiteUrl: nullable(string()), 11 | organizersUrl: nullable(string()), 12 | blueskyUrl: nullable(string()), 13 | xUrl: nullable(string()), 14 | linkedinUrl: nullable(string()), 15 | youtubeUrl: nullable(string()), 16 | twitchUrl: nullable(string()), 17 | callForPapersUrl: nullable(string()), 18 | events: array(EventSchema), 19 | }); 20 | 21 | export const CommunityListSchema = array(CommunitySchema); 22 | -------------------------------------------------------------------------------- /angular-hub/src/server/schemas/event.schema.ts: -------------------------------------------------------------------------------- 1 | import { boolean, nullable, object, picklist, string, optional } from 'valibot'; 2 | 3 | export const EventSchema = object({ 4 | name: nullable(string()), 5 | type: picklist(['workshop', 'conference', 'meetup', 'other']), 6 | location: nullable(string()), 7 | url: nullable(string()), 8 | date: nullable(string()), 9 | language: string(), 10 | isFree: boolean(), 11 | isRemote: boolean(), 12 | isOnsite: boolean(), 13 | callForPapersUrl: nullable(string()), 14 | callForPapersDueDate: nullable(string()), 15 | toBeAnnounced: optional(boolean()), 16 | }); 17 | -------------------------------------------------------------------------------- /angular-hub/src/server/schemas/podcast.schema.ts: -------------------------------------------------------------------------------- 1 | import { array, object, string } from 'valibot'; 2 | 3 | export const PodcastSchema = object({ 4 | name: string(), 5 | url: string(), 6 | logo: string(), 7 | language: string(), 8 | }); 9 | 10 | export const PodcastListSchema = array(PodcastSchema); 11 | -------------------------------------------------------------------------------- /angular-hub/src/styles.scss: -------------------------------------------------------------------------------- 1 | @layer tw-base, primeng, tw-components, tw-utilities, tw-variants; 2 | 3 | // Please take note of the order in which these are imported 4 | @import 'tailwindcss/base.css' layer(tw-base); 5 | @import 'primeng/resources/primeng.min.css'; 6 | @import 'primeng/resources/themes/aura-dark-purple/theme.css'; 7 | @import 'primeicons/primeicons'; 8 | @import 'tailwindcss/components.css' layer(tw-components); 9 | @import 'tailwindcss/utilities.css' layer(tw-utilities); 10 | @import 'tailwindcss/variants.css' layer(tw-variants); 11 | 12 | @font-face { 13 | font-family: 'Pixelify'; 14 | src: local('Trebutchet MS'), url('/assets/fonts/PixelifySans-Regular.ttf'); 15 | font-display: swap; 16 | } 17 | 18 | @font-face { 19 | font-family: 'Luciole'; 20 | src: local('Trebutchet MS'), url('/assets/fonts/Luciole-Regular.ttf'); 21 | font-display: swap; 22 | } 23 | 24 | /* You can add global styles to this file, and also import other style files */ 25 | :root { 26 | font-family: Luciole, sans-serif; 27 | } 28 | 29 | body { 30 | margin: 0; 31 | display: flex; 32 | min-height: 100vh; 33 | font-family: Inter, sans-serif; 34 | line-height: 1.5; 35 | } 36 | 37 | .logo { 38 | @apply box-content; 39 | } 40 | 41 | .title { 42 | font-family: Pixelify, sans-serif; 43 | background-image: linear-gradient( 44 | to right, 45 | #bf25b9, 46 | #836ae9, 47 | #1690fa, 48 | #00aaf3, 49 | #00bee0 50 | ); 51 | background-size: 100%; 52 | background-repeat: repeat; 53 | background-clip: text; 54 | -webkit-background-clip: text; 55 | -webkit-text-fill-color: transparent; 56 | } 57 | 58 | .p-button { 59 | font-family: Verdana, sans-serif; 60 | } 61 | 62 | .p-message { 63 | margin: unset; 64 | } 65 | 66 | .p-calendar .p-highlight { 67 | border-width: 2px; 68 | box-shadow: 0 0 5px 5px #00b8e6; 69 | } 70 | 71 | .p-datepicker-other-month { 72 | color: grey; 73 | } 74 | 75 | @media (prefers-reduced-motion) { 76 | ::view-transition-group(*), 77 | ::view-transition-old(*), 78 | ::view-transition-new(*) { 79 | animation: none !important; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /angular-hub/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import '@analogjs/vite-plugin-angular/setup-vitest'; 2 | 3 | import { 4 | BrowserDynamicTestingModule, 5 | platformBrowserDynamicTesting, 6 | } from '@angular/platform-browser-dynamic/testing'; 7 | import { getTestBed } from '@angular/core/testing'; 8 | 9 | getTestBed().initTestEnvironment( 10 | BrowserDynamicTestingModule, 11 | platformBrowserDynamicTesting(), 12 | ); 13 | -------------------------------------------------------------------------------- /angular-hub/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /angular-hub/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); 2 | const { join } = require('path'); 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | module.exports = { 6 | content: [ 7 | join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), 8 | ...createGlobPatternsForDependencies(__dirname), 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | primary: '#ff56f8', 14 | secondary: '#00bee0', 15 | }, 16 | backgroundColor: { 17 | primary: '#ff56f8', 18 | secondary: '#00bee0', 19 | }, 20 | borderColor: { 21 | primary: '#ff56f8', 22 | secondary: '#00bee0', 23 | }, 24 | dropShadow: { 25 | 't-sm': '0 -1px 2px 0 rgba(0, 0, 0, 0.05)', 26 | 't-md': 27 | '0 -4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', 28 | 't-lg': 29 | '0 -10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', 30 | 't-xl': 31 | '0 -20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', 32 | 't-2xl': '0 -25px 50px -12px rgba(0, 0, 0, 0.25)', 33 | 't-3xl': '0 -35px 60px -15px rgba(0, 0, 0, 0.3)', 34 | 'b-sm': '0 1px 2px 0 rgba(0, 0, 0, 0.05)', 35 | 'b-md': 36 | '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 -2px 4px -1px rgba(0, 0, 0, 0.06)', 37 | 'b-lg': 38 | '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 -4px 6px -2px rgba(0, 0, 0, 0.05)', 39 | 'b-xl': 40 | '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 -10px 10px -5px rgba(0, 0, 0, 0.04)', 41 | 'b-2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)', 42 | 'b-3xl': '0 35px 60px -15px rgba(0, 0, 0, 0.3)', 43 | 'l-sm': '-1px 0 2px 0 rgba(0, 0, 0, 0.05)', 44 | 'l-md': 45 | '-4px 0 6px -1px rgba(0, 0, 0, 0.1), 2px 0 4px -1px rgba(0, 0, 0, 0.06)', 46 | 'l-lg': 47 | '-10px 0 15px -3px rgba(0, 0, 0, 0.1), 4px 0 6px -2px rgba(0, 0, 0, 0.05)', 48 | 'l-xl': 49 | '-20px 0 25px -5px rgba(0, 0, 0, 0.1), 10px 0 10px -5px rgba(0, 0, 0, 0.04)', 50 | 'l-2xl': '-25px 0 50px -12px rgba(0, 0, 0, 0.25)', 51 | 'l-3xl': '-35px 0 60px -15px rgba(0, 0, 0, 0.3)', 52 | 'r-sm': '1px 0 2px 0 rgba(0, 0, 0, 0.05)', 53 | 'r-md': 54 | '4px 0 6px -1px rgba(0, 0, 0, 0.1), -2px 0 4px -1px rgba(0, 0, 0, 0.06)', 55 | 'r-lg': 56 | '10px 0 15px -3px rgba(0, 0, 0, 0.1), -4px 0 6px -2px rgba(0, 0, 0, 0.05)', 57 | 'r-xl': 58 | '20px 0 25px -5px rgba(0, 0, 0, 0.1), -10px 0 10px -5px rgba(0, 0, 0, 0.04)', 59 | 'r-2xl': '25px 0 50px -12px rgba(0, 0, 0, 0.25)', 60 | 'r-3xl': '35px 0 60px -15px rgba(0, 0, 0, 0.3)', 61 | 'all-sm': '0 0 2px 0 rgba(0, 0, 0, 0.05)', 62 | 'all-md': 63 | '0 0 6px -1px rgba(0, 0, 0, 0.1), 0 0 4px -1px rgba(0, 0, 0, 0.06)', 64 | 'all-lg': 65 | '0 0 15px -3px rgba(0, 0, 0, 0.1), 0 0 6px -2px rgba(0, 0, 0, 0.05)', 66 | 'all-xl': 67 | '0 0 25px -5px rgba(0, 0, 0, 0.1), 0 0 10px -5px rgba(0, 0, 0, 0.04)', 68 | 'all-2xl': '0 0 50px -12px rgba(0, 0, 0, 0.25)', 69 | 'all-3xl': '0 0 60px -15px rgba(0, 0, 0, 0.3)', 70 | }, 71 | }, 72 | }, 73 | plugins: [], 74 | }; 75 | -------------------------------------------------------------------------------- /angular-hub/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc", 5 | "types": [], 6 | "target": "ES2022", 7 | "useDefineForClassFields": false, 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true 10 | }, 11 | "files": ["src/main.ts", "src/main.server.ts"], 12 | "include": [ 13 | "src/**/*.d.ts", 14 | "src/app/routes/**/*.ts", 15 | "src/app/pages/**/*.page.ts" 16 | ], 17 | "exclude": ["**/*.test.ts", "**/*.spec.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /angular-hub/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["node", "vitest/globals"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /angular-hub/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "exclude": ["./src/server/**/*"], 6 | "references": [ 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.spec.json" 12 | }, 13 | { 14 | "path": "./tsconfig.editor.json" 15 | } 16 | ], 17 | "compilerOptions": { 18 | "target": "es2020", 19 | "forceConsistentCasingInFileNames": true, 20 | "strict": true, 21 | "noImplicitOverride": true, 22 | "noPropertyAccessFromIndexSignature": true, 23 | "noImplicitReturns": true, 24 | "noFallthroughCasesInSwitch": true 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /angular-hub/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc", 5 | "types": ["node", "vitest/globals"] 6 | }, 7 | "files": ["src/test-setup.ts"], 8 | "include": ["src/**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /angular-hub/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import analog from '@analogjs/platform'; 4 | import { defineConfig, splitVendorChunkPlugin, Plugin, UserConfig } from 'vite'; 5 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 6 | // @ts-expect-error private API 7 | import { augmentAppWithServiceWorker } from '@angular/build/private'; 8 | import * as path from 'path'; 9 | 10 | function swBuildPlugin(): Plugin { 11 | let config: UserConfig; 12 | return { 13 | name: 'analog-sw', 14 | config(_config) { 15 | config = _config; 16 | }, 17 | async closeBundle() { 18 | console.log('Building service worker'); 19 | await augmentAppWithServiceWorker( 20 | './angular-hub', 21 | process.cwd(), 22 | path.join(process.cwd(), 'dist/angular-hub/client'), 23 | '/', 24 | ); 25 | }, 26 | }; 27 | } 28 | 29 | // https://vitejs.dev/config/ 30 | export default defineConfig(({ mode }) => { 31 | return { 32 | publicDir: 'src/public', 33 | ssr: { 34 | noExternal: ['primeng/**'], 35 | }, 36 | server: { 37 | fs: { 38 | allow: ['..'], 39 | }, 40 | cors: true, 41 | }, 42 | build: { 43 | target: ['es2020'], 44 | }, 45 | plugins: [ 46 | analog({ 47 | prerender: { 48 | sitemap: { 49 | host: 'https://angular-hub.com/', 50 | }, 51 | }, 52 | nitro: { 53 | routeRules: { 54 | '/api/v1/**': { 55 | cors: true, 56 | }, 57 | }, 58 | }, 59 | }), 60 | nxViteTsPaths(), 61 | splitVendorChunkPlugin(), 62 | swBuildPlugin(), 63 | ], 64 | test: { 65 | globals: true, 66 | environment: 'jsdom', 67 | setupFiles: ['src/test-setup.ts'], 68 | include: ['**/*.spec.ts'], 69 | cache: { 70 | dir: `../node_modules/.vitest`, 71 | }, 72 | }, 73 | define: { 74 | 'import.meta.vitest': mode !== 'production', 75 | }, 76 | }; 77 | }); 78 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjectsAsync } from '@nx/jest'; 2 | 3 | export default async () => ({ 4 | projects: await getJestProjectsAsync(), 5 | }); 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/plugin/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["./package.json", "./generators.json"], 19 | "parser": "jsonc-eslint-parser", 20 | "rules": { 21 | "@nx/nx-plugin-checks": "error" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /libs/plugin/README.md: -------------------------------------------------------------------------------- 1 | # plugin 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Building 6 | 7 | Run `nx build plugin` to build the library. 8 | 9 | ## Running unit tests 10 | 11 | Run `nx test plugin` to execute the unit tests via [Jest](https://jestjs.io). 12 | -------------------------------------------------------------------------------- /libs/plugin/generators.json: -------------------------------------------------------------------------------- 1 | { 2 | "generators": { 3 | "create-community": { 4 | "factory": "./src/generators/create-community/generator", 5 | "schema": "./src/generators/create-community/schema.json", 6 | "description": "Create a new community" 7 | }, 8 | "create-podcast": { 9 | "factory": "./src/generators/create-podcast/generator", 10 | "schema": "./src/generators/create-podcast/schema.json", 11 | "description": "Create a new podcast" 12 | }, 13 | "create-event": { 14 | "factory": "./src/generators/create-event/generator", 15 | "schema": "./src/generators/create-event/schema.json", 16 | "description": "Create a new event" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libs/plugin/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'plugin', 4 | preset: '../jest.preset.js', 5 | transform: { 6 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], 7 | }, 8 | moduleFileExtensions: ['ts', 'js', 'html'], 9 | coverageDirectory: '../coverage/libs/plugin', 10 | }; 11 | -------------------------------------------------------------------------------- /libs/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular-hub/plugin", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "@nx/devkit": "19.3.1", 6 | "tslib": "^2.3.0" 7 | }, 8 | "type": "commonjs", 9 | "main": "./src/index.js", 10 | "typings": "./src/index.d.ts", 11 | "private": true, 12 | "generators": "./generators.json" 13 | } 14 | -------------------------------------------------------------------------------- /libs/plugin/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin", 3 | "$schema": "../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/plugin/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "build": { 9 | "executor": "@nx/js:tsc", 10 | "outputs": ["{options.outputPath}"], 11 | "options": { 12 | "outputPath": "dist/libs/plugin", 13 | "main": "libs/plugin/src/index.ts", 14 | "tsConfig": "libs/plugin/tsconfig.lib.json", 15 | "assets": [ 16 | "libs/plugin/*.md", 17 | { 18 | "input": "./libs/plugin/src", 19 | "glob": "**/!(*.ts)", 20 | "output": "./src" 21 | }, 22 | { 23 | "input": "./libs/plugin/src", 24 | "glob": "**/*.d.ts", 25 | "output": "./src" 26 | }, 27 | { 28 | "input": "./libs/plugin", 29 | "glob": "generators.json", 30 | "output": "." 31 | }, 32 | { 33 | "input": "./libs/plugin", 34 | "glob": "executors.json", 35 | "output": "." 36 | } 37 | ] 38 | } 39 | }, 40 | "lint": { 41 | "executor": "@nx/eslint:lint" 42 | }, 43 | "test": { 44 | "executor": "@nx/jest:jest", 45 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 46 | "options": { 47 | "jestConfig": "libs/plugin/jest.config.ts" 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-community/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; 2 | import { Tree, readProjectConfiguration } from '@nx/devkit'; 3 | 4 | import { createCommunityGenerator } from './generator'; 5 | import { CreateCommunityGeneratorSchema } from './schema'; 6 | 7 | describe('create-community generator', () => { 8 | let tree: Tree; 9 | const options: CreateCommunityGeneratorSchema = { name: 'test' }; 10 | 11 | beforeEach(() => { 12 | tree = createTreeWithEmptyWorkspace(); 13 | }); 14 | 15 | it('should run successfully', async () => { 16 | await createCommunityGenerator(tree, options); 17 | const config = readProjectConfiguration(tree, 'test'); 18 | expect(config).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-community/generator.ts: -------------------------------------------------------------------------------- 1 | import { logger, readJson, Tree, updateJson } from '@nx/devkit'; 2 | import { exit } from 'node:process'; 3 | import { Community } from '../models/community'; 4 | import { isPublicAsset } from '../utils/isPublicAsset'; 5 | import { CreateCommunityGeneratorSchema } from './schema'; 6 | 7 | const COMMUNITIES_PATH = 'angular-hub/src/public/assets/data/community.json'; 8 | 9 | export async function createCommunityGenerator( 10 | tree: Tree, 11 | options: CreateCommunityGeneratorSchema, 12 | ) { 13 | const { name, type, url, logo } = options; 14 | 15 | if (!name) { 16 | logger.error('[angular-hub] Name is missing'); 17 | return exit(1); 18 | } 19 | 20 | if (!type) { 21 | logger.error('[angular-hub] Type is missing'); 22 | return exit(1); 23 | } 24 | 25 | if (url && !isPublicAsset(url)) { 26 | logger.error( 27 | '[angular-hub] Url is not valid (should start with https or http). ', 28 | ); 29 | return exit(1); 30 | } 31 | 32 | if (logo && !isPublicAsset(logo)) { 33 | logger.info( 34 | '[angular-hub] Make sure you upload the logo file at logos folder in the assets directory', 35 | ); 36 | } 37 | 38 | const existingCommunities: Community[] = readJson(tree, COMMUNITIES_PATH); 39 | 40 | if (isCommunityExisting(existingCommunities, name)) { 41 | logger.error(`[angular-hub] ${name} Community already exists`); 42 | return exit(1); 43 | } 44 | 45 | updateJson(tree, COMMUNITIES_PATH, (communities: Community[]) => { 46 | communities.push({ 47 | name, 48 | type, 49 | url: options.url ?? '', 50 | location: options.location ?? '', 51 | logo: options.logo ?? '', 52 | bluesky: options.bluesky ?? '', 53 | x: options.x ?? '', 54 | linkedin: options.linkedin ?? '', 55 | callForPapers: options.callForPapers ?? '', 56 | events: [], 57 | }); 58 | 59 | return communities; 60 | }); 61 | 62 | logger.info( 63 | `[angular-hub] Community is added successfully. You can manually update the data if needed afterwards`, 64 | ); 65 | } 66 | 67 | function isCommunityExisting(communities: Community[], name: string): boolean { 68 | return communities 69 | .map((community) => community.name.toLowerCase()) 70 | .includes(name.toLowerCase()); 71 | } 72 | 73 | export default createCommunityGenerator; 74 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-community/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface CreateCommunityGeneratorSchema { 2 | name: string; 3 | type: 'workshop' | 'conference' | 'meetup' | 'other'; 4 | location?: string; 5 | url?: string; 6 | logo?: string; 7 | bluesky?: string; 8 | x?: string; 9 | linkedin?: string; 10 | callForPapers?: string; 11 | } 12 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-community/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/schema", 3 | "$id": "CreateCommunity", 4 | "title": "", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Provide the community name", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | }, 14 | "x-prompt": "What is the name of community?" 15 | }, 16 | "type": { 17 | "type": "string", 18 | "enum": ["workshop", "conference", "meetup", "other"], 19 | "description": "Provide the community type:", 20 | "x-prompt": { 21 | "message": "What is the type of community?", 22 | "type": "list", 23 | "items": [ 24 | { 25 | "value": "workshop", 26 | "label": "Workshop" 27 | }, 28 | { 29 | "value": "conference", 30 | "label": "Conference" 31 | }, 32 | { 33 | "value": "meetup", 34 | "label": "Meetup" 35 | }, 36 | { 37 | "value": "other", 38 | "label": "Other" 39 | } 40 | ] 41 | } 42 | }, 43 | "location": { 44 | "type": "string", 45 | "description": "Provide the location where community takes place", 46 | "x-prompt": "Provide the community location:" 47 | }, 48 | "url": { 49 | "type": "string", 50 | "description": "Provide the site url", 51 | "x-prompt": "Provide the site url:" 52 | }, 53 | "logo": { 54 | "type": "string", 55 | "description": "Provide the brand logo", 56 | "x-prompt": "Provide the brand logo url:" 57 | }, 58 | "bluesky": { 59 | "type": "string", 60 | "description": "Provide the bluesky account link", 61 | "x-prompt": "Provide the bluesky profile:" 62 | }, 63 | "x": { 64 | "type": "string", 65 | "description": "Provide the x account link", 66 | "x-prompt": "Provide the x profile:" 67 | }, 68 | "linkedin": { 69 | "type": "string", 70 | "description": "Provide the linkedin account link", 71 | "x-prompt": "Provide the linkedin profile:" 72 | }, 73 | "callForPapers": { 74 | "type": "string", 75 | "description": "Provide the Call-For-Paper (CFP) form url", 76 | "x-prompt": "Provide the CFP form url:" 77 | } 78 | }, 79 | "required": ["name", "type"] 80 | } 81 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-event/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; 2 | import { Tree, readProjectConfiguration } from '@nx/devkit'; 3 | 4 | import { createEventGenerator } from './generator'; 5 | import { CreateEventGeneratorSchema } from './schema'; 6 | 7 | describe('create-event generator', () => { 8 | let tree: Tree; 9 | const options: CreateEventGeneratorSchema = { name: 'test' }; 10 | 11 | beforeEach(() => { 12 | tree = createTreeWithEmptyWorkspace(); 13 | }); 14 | 15 | it('should run successfully', async () => { 16 | await createEventGenerator(tree, options); 17 | const config = readProjectConfiguration(tree, 'test'); 18 | expect(config).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-event/generator.ts: -------------------------------------------------------------------------------- 1 | import { Tree, logger, readJson, updateJson } from '@nx/devkit'; 2 | import { CreateEventGeneratorSchema } from './schema'; 3 | import { exit } from 'node:process'; 4 | import { Community } from '../models/community'; 5 | 6 | const COMMUNITIES_PATH = 'angular-hub/src/public/assets/data/community.json'; 7 | 8 | export async function createEventGenerator( 9 | tree: Tree, 10 | options: CreateEventGeneratorSchema, 11 | ) { 12 | if (!options.communityName) { 13 | logger.error('[angular-hub] Community name is missing'); 14 | return exit(1); 15 | } 16 | 17 | if (!options.type) { 18 | logger.error('[angular-hub] Type is missing'); 19 | return exit(1); 20 | } 21 | 22 | if (!options.date) { 23 | logger.error('[angular-hub] Date is missing'); 24 | return exit(1); 25 | } 26 | 27 | if (!options.language) { 28 | logger.error('[angular-hub] Language is missing'); 29 | return exit(1); 30 | } 31 | 32 | if (options.isFree === null || options.isFree === undefined) { 33 | logger.error('[angular-hub] isFree is missing'); 34 | return exit(1); 35 | } 36 | 37 | if (options.isRemote === null || options.isRemote === undefined) { 38 | logger.error('[angular-hub] isRemote is missing'); 39 | return exit(1); 40 | } 41 | 42 | if (options.isOnsite === null || options.isOnsite === undefined) { 43 | logger.error('[angular-hub] isOnsite is missing'); 44 | return exit(1); 45 | } 46 | 47 | const communities: Community[] = readJson(tree, COMMUNITIES_PATH); 48 | const selectedCommunity = communities.find( 49 | (community) => 50 | community.name.toLowerCase() == options.communityName.toLowerCase(), 51 | ); 52 | 53 | if (!selectedCommunity) { 54 | logger.error( 55 | `[angular-hub] ${options.communityName} Community does not exists`, 56 | ); 57 | return exit(1); 58 | } 59 | 60 | if (isEventExisting(selectedCommunity, options)) { 61 | logger.error(`[angular-hub] ${options.name} Event already exists`); 62 | return exit(1); 63 | } 64 | 65 | updateJson(tree, COMMUNITIES_PATH, (communities: Community[]) => { 66 | const communityToUpdateIdx = communities.findIndex( 67 | (community) => community.name == selectedCommunity.name, 68 | ); 69 | 70 | if (communityToUpdateIdx != -1) { 71 | communities[communityToUpdateIdx].events.push({ 72 | name: options.name ?? '', 73 | type: options.type, 74 | location: options.location ?? '', 75 | date: options.date, 76 | language: options.language, 77 | isFree: options.isFree, 78 | isRemote: options.isRemote, 79 | isOnsite: options.isOnsite, 80 | callForPapers: options.callForPapers ?? '', 81 | callForPapersDueDate: options.callForPapersDueDate ?? '', 82 | url: options.url ?? '', 83 | }); 84 | } 85 | 86 | return communities; 87 | }); 88 | 89 | logger.info( 90 | `[angular-hub] Event is added successfully. You can manually update the data if needed afterwards`, 91 | ); 92 | } 93 | 94 | function isEventExisting( 95 | community: Community, 96 | { 97 | name, 98 | date, 99 | type, 100 | }: Pick, 101 | ): boolean { 102 | return !!community.events.find( 103 | (event) => 104 | event.type == type && 105 | event.date == date && 106 | event.name.toLowerCase() == name.toLowerCase(), 107 | ); 108 | } 109 | 110 | export default createEventGenerator; 111 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-event/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface CreateEventGeneratorSchema { 2 | name?: string; 3 | type: 'workshop' | 'conference' | 'meetup' | 'other'; 4 | location?: string | null; 5 | date: string; 6 | language: string; 7 | isFree: boolean; 8 | isRemote: boolean; 9 | isOnsite: boolean; 10 | callForPapers?: string | null; 11 | callForPapersDueDate?: string | null; 12 | url?: string; 13 | communityName: string; 14 | } 15 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-event/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/schema", 3 | "$id": "CreateEvent", 4 | "title": "", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Provide the event name", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | }, 14 | "x-prompt": "What is the name of event?" 15 | }, 16 | "communityName": { 17 | "type": "string", 18 | "description": "Provide the community organizer name", 19 | "x-prompt": "Provide the community organizer name:" 20 | }, 21 | "type": { 22 | "type": "string", 23 | "enum": ["workshop", "conference", "meetup", "other"], 24 | "description": "Provide the event type", 25 | "x-prompt": { 26 | "message": "What is the type of event?", 27 | "type": "list", 28 | "items": [ 29 | { 30 | "value": "workshop", 31 | "label": "Workshop" 32 | }, 33 | { 34 | "value": "conference", 35 | "label": "Conference" 36 | }, 37 | { 38 | "value": "meetup", 39 | "label": "Meetup" 40 | }, 41 | { 42 | "value": "other", 43 | "label": "Other" 44 | } 45 | ] 46 | } 47 | }, 48 | "location": { 49 | "type": "string", 50 | "description": "Provide the location where community takes place", 51 | "x-prompt": "Provide the community location:" 52 | }, 53 | "date": { 54 | "type": "string", 55 | "description": "Provide the event date", 56 | "x-prompt": "Provide the event date (ex: 2024-05-30):" 57 | }, 58 | "language": { 59 | "type": "string", 60 | "description": "Provide the event language", 61 | "x-prompt": "Provide the event language (ex: English):" 62 | }, 63 | "isFree": { 64 | "type": "boolean", 65 | "description": "if it's a paid event or not", 66 | "x-prompt": "Is event free of charge?" 67 | }, 68 | "isRemote": { 69 | "type": "boolean", 70 | "description": "if it's a remote event or not", 71 | "x-prompt": "Is it a remote event?" 72 | }, 73 | "isOnsite": { 74 | "type": "boolean", 75 | "description": "if it's an on-site event or not", 76 | "x-prompt": "Is it on-site event?" 77 | }, 78 | "callForPapers": { 79 | "type": "string", 80 | "description": "Provide the Call-For-Paper (CFP) form url", 81 | "x-prompt": "Provide the CFP form url:" 82 | }, 83 | "callForPapersDueDate": { 84 | "type": "string", 85 | "description": "Call-For-Paper (CFP) due date", 86 | "x-prompt": "Provide the Call-For-Paper (CFP) due date:" 87 | }, 88 | "url": { 89 | "type": "string", 90 | "description": "Provide the event url", 91 | "x-prompt": "Provide the event url:" 92 | } 93 | }, 94 | "required": [ 95 | "communityName", 96 | "type", 97 | "date", 98 | "language", 99 | "isFree", 100 | "isRemote", 101 | "isOnsite" 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-podcast/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; 2 | import { Tree, readProjectConfiguration } from '@nx/devkit'; 3 | 4 | import { createPodcastGenerator } from './generator'; 5 | import { CreatePodcastGeneratorSchema } from './schema'; 6 | 7 | describe('create-podcast generator', () => { 8 | let tree: Tree; 9 | const options: CreatePodcastGeneratorSchema = { name: 'test' }; 10 | 11 | beforeEach(() => { 12 | tree = createTreeWithEmptyWorkspace(); 13 | }); 14 | 15 | it('should run successfully', async () => { 16 | await createPodcastGenerator(tree, options); 17 | const config = readProjectConfiguration(tree, 'test'); 18 | expect(config).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-podcast/generator.ts: -------------------------------------------------------------------------------- 1 | import { logger, readJson, Tree, updateJson } from '@nx/devkit'; 2 | import { CreatePodcastGeneratorSchema } from './schema'; 3 | import { exit } from 'node:process'; 4 | import { Podcast } from '../models/podcast'; 5 | import { isPublicAsset } from '../utils/isPublicAsset'; 6 | 7 | const PODCASTS_PATH = 'angular-hub/src/public/assets/data/podcast.json'; 8 | 9 | export async function createPodcastGenerator( 10 | tree: Tree, 11 | options: CreatePodcastGeneratorSchema, 12 | ) { 13 | const { name, logo, language, url } = options; 14 | 15 | if (!name) { 16 | logger.error('[angular-hub] Name is missing'); 17 | return exit(1); 18 | } 19 | 20 | if (!logo) { 21 | logger.error('[angular-hub] Logo is missing'); 22 | return exit(1); 23 | } 24 | 25 | if (!url) { 26 | logger.error('[angular-hub] Platform link is missing'); 27 | return exit(1); 28 | } 29 | 30 | if (!language) { 31 | logger.error('[angular-hub] Language is missing'); 32 | return exit(1); 33 | } 34 | 35 | if (!isPublicAsset(url)) { 36 | logger.error( 37 | '[angular-hub] Platform link is not valid (should start with https or http). ', 38 | ); 39 | return exit(1); 40 | } 41 | 42 | if (!isPublicAsset(logo)) { 43 | logger.info( 44 | '[angular-hub] Make sure you upload the logo file at logos folder in the assets directory', 45 | ); 46 | } 47 | 48 | const existingPodcasts: Podcast[] = readJson(tree, PODCASTS_PATH); 49 | 50 | if (isPodcastExisting(existingPodcasts, name)) { 51 | logger.error(`[angular-hub] ${name} Podcast already exists`); 52 | return exit(1); 53 | } 54 | 55 | updateJson(tree, PODCASTS_PATH, (podcasts: Podcast[]) => { 56 | podcasts.push({ 57 | name, 58 | language, 59 | url, 60 | logo, 61 | }); 62 | 63 | return podcasts; 64 | }); 65 | 66 | logger.info( 67 | `[angular-hub] Podcast is added successfully. You can manually update the data if needed afterwards`, 68 | ); 69 | } 70 | 71 | function isPodcastExisting(podcasts: Podcast[], name: string): boolean { 72 | return podcasts 73 | .map((podcast) => podcast.name.toLowerCase()) 74 | .includes(name.toLowerCase()); 75 | } 76 | 77 | export default createPodcastGenerator; 78 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-podcast/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface CreatePodcastGeneratorSchema { 2 | name: string; 3 | url: string; 4 | logo: string; 5 | language: string; 6 | } 7 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/create-podcast/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/schema", 3 | "$id": "CreatePodcast", 4 | "title": "", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Provide the podcast name", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | }, 14 | "x-prompt": "What is the name of podcast?" 15 | }, 16 | "url": { 17 | "type": "string", 18 | "description": "Provide the site url", 19 | "x-prompt": "Provide the preferred podcast platform link:" 20 | }, 21 | "logo": { 22 | "type": "string", 23 | "description": "Provide the brand logo", 24 | "x-prompt": "Provide the brand logo url:" 25 | }, 26 | "language": { 27 | "type": "string", 28 | "description": "Provide the language", 29 | "x-prompt": "Provide the language (ex: English):" 30 | } 31 | }, 32 | "required": ["name", "url", "logo", "language"] 33 | } 34 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/models/community.ts: -------------------------------------------------------------------------------- 1 | import { Event } from './event'; 2 | 3 | export interface Community { 4 | name: string; 5 | type: 'workshop' | 'conference' | 'meetup' | 'other'; 6 | location?: string; 7 | url?: string; 8 | logo?: string; 9 | bluesky?: string; 10 | x?: string; 11 | linkedin?: string; 12 | callForPapers?: string; 13 | events: Event[]; 14 | } 15 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/models/event.ts: -------------------------------------------------------------------------------- 1 | export interface Event { 2 | name?: string; 3 | type: 'workshop' | 'conference' | 'meetup' | 'other'; 4 | location?: string | null; 5 | date: string; 6 | language: string; 7 | isFree: boolean; 8 | isRemote: boolean; 9 | isOnsite: boolean; 10 | callForPapers?: string | null; 11 | callForPapersDueDate?: string | null; 12 | url?: string; 13 | } 14 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/models/podcast.ts: -------------------------------------------------------------------------------- 1 | export interface Podcast { 2 | name: string; 3 | url: string; 4 | logo: string; 5 | language: string; 6 | } 7 | -------------------------------------------------------------------------------- /libs/plugin/src/generators/utils/isPublicAsset.ts: -------------------------------------------------------------------------------- 1 | export function isPublicAsset(assetLink: string): boolean { 2 | return assetLink.startsWith('https://') || assetLink.startsWith('http://'); 3 | } 4 | -------------------------------------------------------------------------------- /libs/plugin/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-sanctuary/angular-hub/ad8a72537f8de21ba76e0e197d132a21f8661ad3/libs/plugin/src/index.ts -------------------------------------------------------------------------------- /libs/plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.lib.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/plugin/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"] 7 | }, 8 | "include": ["src/**/*.ts"], 9 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/plugin/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "cli": "nx", 5 | "version": "19.6.0-beta.4", 6 | "description": "Update ciWebServerCommand to use static serve for the application.", 7 | "implementation": "./src/migrations/update-19-6-0/update-ci-webserver-for-static-serve", 8 | "package": "@nx/cypress", 9 | "name": "update-19-6-0-update-ci-webserver-for-vite" 10 | }, 11 | { 12 | "cli": "nx", 13 | "version": "19.5.0-beta.1", 14 | "requires": { "@angular/core": ">=18.1.0" }, 15 | "description": "Update the @angular/cli package version to ~18.1.0.", 16 | "factory": "./src/migrations/update-19-5-0/update-angular-cli", 17 | "package": "@nx/angular", 18 | "name": "update-angular-cli-version-18-1-0" 19 | }, 20 | { 21 | "cli": "nx", 22 | "version": "19.6.0-beta.4", 23 | "description": "Ensure Module Federation DTS is turned off by default.", 24 | "factory": "./src/migrations/update-19-6-0/turn-off-dts-by-default", 25 | "package": "@nx/angular", 26 | "name": "update-19-6-0" 27 | }, 28 | { 29 | "cli": "nx", 30 | "version": "19.6.0-beta.7", 31 | "requires": { "@angular/core": ">=18.2.0" }, 32 | "description": "Update the @angular/cli package version to ~18.2.0.", 33 | "factory": "./src/migrations/update-19-6-0/update-angular-cli", 34 | "package": "@nx/angular", 35 | "name": "update-angular-cli-version-18-2-0" 36 | }, 37 | { 38 | "cli": "nx", 39 | "version": "19.6.1-beta.0", 40 | "description": "Ensure Target Defaults are set correctly for Module Federation.", 41 | "factory": "./src/migrations/update-19-6-1/ensure-depends-on-for-mf", 42 | "package": "@nx/angular", 43 | "name": "update-19-6-1-ensure-module-federation-target-defaults" 44 | }, 45 | { 46 | "version": "19.6.0-beta.0", 47 | "description": "Add dependsOn: [build] to preview targets using preview-server", 48 | "implementation": "./src/migrations/update-19-6-0/add-depends-on-for-preview", 49 | "package": "@nx/vite", 50 | "name": "update-19-6-0-add-depends-on-for-preview-server" 51 | }, 52 | { 53 | "version": "18.1.0", 54 | "description": "Updates calls to afterRender with an explicit phase to the new API", 55 | "factory": "./migrations/after-render-phase/bundle", 56 | "package": "@angular/core", 57 | "name": "migration-after-render-phase" 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "targetDefaults": { 4 | "build": { 5 | "cache": true, 6 | "dependsOn": ["^build"], 7 | "inputs": ["production", "^production"] 8 | }, 9 | "@nx/eslint:lint": { 10 | "cache": true, 11 | "inputs": [ 12 | "default", 13 | "{workspaceRoot}/.eslintrc.json", 14 | "{workspaceRoot}/.eslintignore", 15 | "{workspaceRoot}/eslint.config.js" 16 | ] 17 | }, 18 | "@nx/js:tsc": { 19 | "cache": true, 20 | "dependsOn": ["^build"], 21 | "inputs": ["production", "^production"] 22 | }, 23 | "@nx/jest:jest": { 24 | "cache": true, 25 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 26 | "options": { 27 | "passWithNoTests": true 28 | }, 29 | "configurations": { 30 | "ci": { 31 | "ci": true, 32 | "codeCoverage": true 33 | } 34 | } 35 | } 36 | }, 37 | "namedInputs": { 38 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 39 | "production": [ 40 | "default", 41 | "!{projectRoot}/.eslintrc.json", 42 | "!{projectRoot}/eslint.config.js", 43 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 44 | "!{projectRoot}/tsconfig.spec.json", 45 | "!{projectRoot}/jest.config.[jt]s", 46 | "!{projectRoot}/src/test-setup.[jt]s", 47 | "!{projectRoot}/test-setup.[jt]s" 48 | ], 49 | "sharedGlobals": [] 50 | }, 51 | "generators": { 52 | "@nx/angular:application": { 53 | "style": "css", 54 | "linter": "eslint", 55 | "unitTestRunner": "none", 56 | "e2eTestRunner": "cypress" 57 | }, 58 | "@nx/angular:library": { 59 | "linter": "eslint", 60 | "unitTestRunner": "none" 61 | }, 62 | "@nx/angular:component": { 63 | "style": "css" 64 | } 65 | }, 66 | "useInferencePlugins": false 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular-hub/source", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "prepare": "husky install", 7 | "start": "nx serve angular-hub", 8 | "build": "nx build angular-hub" 9 | }, 10 | "private": true, 11 | "engines": { 12 | "npm": "please-use-yarn", 13 | "yarn": ">= 1.19.1" 14 | }, 15 | "dependencies": { 16 | "@analogjs/content": "1.8.0-beta.2", 17 | "@analogjs/router": "1.8.0-beta.2", 18 | "@angular/animations": "18.2.0", 19 | "@angular/cdk": "18.2.0", 20 | "@angular/common": "18.2.0", 21 | "@angular/compiler": "18.2.0", 22 | "@angular/core": "18.2.0", 23 | "@angular/forms": "18.2.0", 24 | "@angular/platform-browser": "18.2.0", 25 | "@angular/platform-browser-dynamic": "18.2.0", 26 | "@angular/platform-server": "18.2.0", 27 | "@angular/pwa": "18.2.0", 28 | "@angular/router": "18.2.0", 29 | "@angular/service-worker": "18.2.0", 30 | "@nx/plugin": "19.6.4", 31 | "@supabase/ssr": "^0.4.0", 32 | "@supabase/supabase-js": "^2.44.2", 33 | "date-fns": "^2.30.0", 34 | "front-matter": "^4.0.2", 35 | "marked": "^5.0.2", 36 | "marked-gfm-heading-id": "^3.1.0", 37 | "marked-highlight": "^2.0.1", 38 | "marked-mangle": "^1.1.7", 39 | "mermaid": "^10.2.4", 40 | "primeicons": "^7.0.0", 41 | "primeng": "^17.18.7", 42 | "prismjs": "^1.29.0", 43 | "rxjs": "~7.8.0", 44 | "satori": "^0.10.14", 45 | "satori-html": "^0.3.2", 46 | "sharp": "^0.33.5", 47 | "tslib": "^2.6.3", 48 | "valibot": "^0.35.0", 49 | "zone.js": "0.14.7" 50 | }, 51 | "devDependencies": { 52 | "@analogjs/platform": "1.8.0-beta.2", 53 | "@analogjs/vite-plugin-angular": "1.8.0-beta.2", 54 | "@angular-devkit/build-angular": "18.2.0", 55 | "@angular-devkit/core": "18.2.0", 56 | "@angular-devkit/schematics": "18.2.0", 57 | "@angular/cli": "~18.2.0", 58 | "@angular/compiler-cli": "18.2.0", 59 | "@angular/language-service": "18.2.0", 60 | "@commitlint/cli": "^19.3.0", 61 | "@commitlint/config-conventional": "^19.2.2", 62 | "@nx/angular": "19.6.4", 63 | "@nx/cypress": "19.6.4", 64 | "@nx/devkit": "19.6.4", 65 | "@nx/eslint": "19.6.4", 66 | "@nx/eslint-plugin": "19.6.4", 67 | "@nx/jest": "19.6.4", 68 | "@nx/js": "19.6.4", 69 | "@nx/vite": "19.6.4", 70 | "@nx/workspace": "19.6.4", 71 | "@schematics/angular": "18.2.0", 72 | "@swc-node/register": "~1.9.1", 73 | "@swc/cli": "~0.3.12", 74 | "@swc/core": "~1.5.7", 75 | "@swc/helpers": "~0.5.11", 76 | "@types/jest": "29.5.12", 77 | "@types/node": "^18.16.9", 78 | "@typescript-eslint/eslint-plugin": "7.18.0", 79 | "@typescript-eslint/parser": "7.18.0", 80 | "autoprefixer": "^10.4.0", 81 | "eslint": "8.57.0", 82 | "eslint-config-prettier": "^9.0.0", 83 | "eslint-plugin-html": "^8.1.1", 84 | "husky": "^9.0.11", 85 | "jest": "29.7.0", 86 | "jest-environment-jsdom": "29.7.0", 87 | "jsdom": "^22.0.0", 88 | "lint-staged": "^15.1.0", 89 | "nx": "19.6.4", 90 | "postcss": "^8.4.5", 91 | "prettier": "3.3.0", 92 | "tailwindcss": "^3.0.2", 93 | "ts-jest": "^29.1.0", 94 | "ts-node": "10.9.1", 95 | "typescript": "5.5.4", 96 | "vite": "5.4.1", 97 | "vite-tsconfig-paths": "^5.0.1", 98 | "vitest": "2.0.5" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /scripts/add-community.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const community = require('./../add-community.json'); 3 | 4 | let communityRaw = fs.readFileSync( 5 | './angular-hub/src/public/assets/data/community.json', 6 | ); 7 | let communityData = JSON.parse(communityRaw); 8 | let json = JSON.stringify( 9 | [ 10 | ...communityData, 11 | { 12 | ...community, 13 | events: [], 14 | }, 15 | ], 16 | null, 17 | 2, 18 | ); 19 | 20 | fs.writeFileSync('./angular-hub/src/public/assets/data/community.json', json); 21 | -------------------------------------------------------------------------------- /scripts/add-podcast.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const order = require('./../add-podcast.json'); 3 | 4 | const [name, url, logo, language] = Object.values(order); 5 | 6 | let podcastRaw = fs.readFileSync( 7 | './angular-hub/src/public/assets/data/podcast.json', 8 | ); 9 | let podcastData = JSON.parse(podcastRaw); 10 | let json = JSON.stringify( 11 | [ 12 | ...podcastData, 13 | { 14 | name, 15 | url, 16 | logo, 17 | language, 18 | }, 19 | ], 20 | null, 21 | 2, 22 | ); 23 | 24 | fs.writeFileSync('./angular-hub/src/public/assets/data/podcast.json', json); 25 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2020", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@angular-hub/plugin": ["libs/plugin/src/index.ts"] 19 | } 20 | }, 21 | "exclude": ["node_modules", "tmp"] 22 | } 23 | --------------------------------------------------------------------------------