├── .dockerignore ├── .github └── workflows │ ├── directus_publish_on_demand.yml │ └── directus_publish_staging_on_push.yml ├── .gitignore ├── .prettierignore ├── Dockerfile.directus ├── LICENSE ├── README.md ├── directus-cms ├── .env ├── .env.example ├── .gitignore ├── _requests │ ├── .gitignore │ └── Directus.http ├── database │ └── .gitkeep ├── extensions │ └── directus-extension-programmierbar-bundle │ │ ├── .editorconfig │ │ ├── .env.dist │ │ ├── .gitignore │ │ ├── .prettierrc │ │ ├── assets │ │ └── browserless.js │ │ ├── eslint.config.js │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── src │ │ ├── algolia-index │ │ │ ├── cli │ │ │ │ └── rebuild-index.ts │ │ │ ├── handlers │ │ │ │ ├── ItemHandler.ts │ │ │ │ ├── MeetupHandler.ts │ │ │ │ ├── PickOfTheDayHandler.ts │ │ │ │ ├── PodcastHandler.ts │ │ │ │ ├── SpeakerHandler.ts │ │ │ │ ├── TranscriptHandler.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── util │ │ │ │ └── sanitizer.ts │ │ ├── buzzsprout │ │ │ ├── handlers │ │ │ │ ├── buzzsprout.ts │ │ │ │ ├── handlePickOfTheDayAction.ts │ │ │ │ ├── handlePodcastAction.ts │ │ │ │ ├── handleTagAction.ts │ │ │ │ ├── podcastData.ts │ │ │ │ └── types.ts │ │ │ └── index.ts │ │ ├── create-profile │ │ │ └── index.ts │ │ ├── deploy-website │ │ │ └── index.ts │ │ ├── podcast-transcript │ │ │ ├── generateTranscriptItem.ts │ │ │ ├── index.ts │ │ │ └── processTranscriptItem.ts │ │ ├── publishable │ │ │ ├── index.ts │ │ │ ├── presentation-publishable.vue │ │ │ └── shims.d.ts │ │ ├── schedule-publication │ │ │ └── index.ts │ │ ├── screenshot │ │ │ └── index.ts │ │ ├── set-published-on │ │ │ └── index.ts │ │ ├── set-slug │ │ │ └── index.ts │ │ └── shared │ │ │ ├── errors.ts │ │ │ ├── isPublishable.ts │ │ │ └── postSlackMessage.ts │ │ └── tsconfig.json ├── package-lock.json ├── package.json ├── run-container.sh ├── schema.json ├── uploads │ └── .gitkeep └── utils │ └── transcription.js ├── nuxt-app ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── README.md ├── app.vue ├── assets │ ├── fonts │ │ ├── AzeretMono-Medium.woff │ │ ├── AzeretMono-Medium.woff2 │ │ ├── DMMono-Light.woff │ │ ├── DMMono-Light.woff2 │ │ ├── museo-sans-100-italic.woff │ │ ├── museo-sans-100-italic.woff2 │ │ ├── museo-sans-100.woff │ │ ├── museo-sans-100.woff2 │ │ ├── museo-sans-300-italic.woff │ │ ├── museo-sans-300-italic.woff2 │ │ ├── museo-sans-300.woff │ │ ├── museo-sans-300.woff2 │ │ ├── museo-sans-500-italic.woff │ │ ├── museo-sans-500-italic.woff2 │ │ ├── museo-sans-500.woff │ │ ├── museo-sans-500.woff2 │ │ ├── museo-sans-700-italic.woff │ │ ├── museo-sans-700-italic.woff2 │ │ ├── museo-sans-700.woff │ │ ├── museo-sans-700.woff2 │ │ ├── museo-sans-900-italic.woff │ │ ├── museo-sans-900-italic.woff2 │ │ ├── museo-sans-900.woff │ │ └── museo-sans-900.woff2 │ ├── icons │ │ ├── 15-sec-backwards.svg │ │ ├── 15-sec-forwards.svg │ │ ├── angle-down.svg │ │ ├── angle-left.svg │ │ ├── angle-right.svg │ │ ├── angle-up.svg │ │ ├── backwards.svg │ │ ├── download.svg │ │ ├── forwards.svg │ │ ├── heart.svg │ │ ├── leave-site.svg │ │ ├── pause-circle-filled.svg │ │ ├── pause.svg │ │ ├── play-circle-filled.svg │ │ ├── play-circle.svg │ │ ├── play.svg │ │ ├── search.svg │ │ ├── semicircle.svg │ │ ├── share.svg │ │ └── sound.svg │ ├── images │ │ ├── brand-icon.svg │ │ ├── brand-logo.svg │ │ ├── cheers-icon.svg │ │ ├── feedback-figure.svg │ │ ├── flutter-day.svg │ │ ├── podcast-figure.svg │ │ ├── profile-picture-empty.svg │ │ └── search-figure.svg │ └── logos │ │ ├── algolia-logo-white.svg │ │ ├── apple-calendar.svg │ │ ├── apple-podcasts-color.svg │ │ ├── bluesky-color.svg │ │ ├── bluesky.svg │ │ ├── discord.svg │ │ ├── facebook-color.svg │ │ ├── facebook.svg │ │ ├── github.svg │ │ ├── google-calendar.svg │ │ ├── google-maps.svg │ │ ├── instagram-color.svg │ │ ├── instagram.svg │ │ ├── linkedin-color.svg │ │ ├── linkedin.svg │ │ ├── mail.svg │ │ ├── mastodon.svg │ │ ├── meetup.svg │ │ ├── rss-feed-color.svg │ │ ├── spotify-color.svg │ │ ├── twitter-color.svg │ │ ├── twitter.svg │ │ ├── website-color.svg │ │ ├── x.svg │ │ ├── youtube-color.svg │ │ └── youtube.svg ├── components │ ├── Breadcrumbs.vue │ ├── ConferenceAgenda.vue │ ├── ConferenceCard.vue │ ├── ConferenceCover.vue │ ├── ConferenceGallery.vue │ ├── ConferenceSection.vue │ ├── ConferenceSpeaker.vue │ ├── ConferenceSpeakersSlider.vue │ ├── ContactForm.vue │ ├── CookieBanner.vue │ ├── DirectusImage.vue │ ├── EmailLoginOption.vue │ ├── EmbeddedVideoPlayer.vue │ ├── EmojiPicker.vue │ ├── FadeAnimation.vue │ ├── FaqItem.vue │ ├── FaqList.vue │ ├── FeedbackSection.vue │ ├── FlyInContent.vue │ ├── Footer.vue │ ├── GenericLazyList.vue │ ├── GenericListItem.vue │ ├── Header.vue │ ├── IndividualPlatforms.vue │ ├── InnerHtml.vue │ ├── InputField.vue │ ├── InputFieldWithHeadline.vue │ ├── LikeButton.vue │ ├── LinkButton.vue │ ├── LoadingScreen.vue │ ├── LoginButton.vue │ ├── MeetupCalendarAndMaps.vue │ ├── MeetupCard.vue │ ├── MeetupCover.vue │ ├── MeetupSection.vue │ ├── MeetupStartAndEnd.vue │ ├── MemberCard.vue │ ├── MouseCursor.vue │ ├── NewsTicker.vue │ ├── PageCoverImage.vue │ ├── Pagination.vue │ ├── PickOfTheDayCard.vue │ ├── PickOfTheDayList.vue │ ├── PickOfTheDayListItem.vue │ ├── PodcastBanner.vue │ ├── PodcastCard.vue │ ├── PodcastPlayer.vue │ ├── PodcastSlider.vue │ ├── PodcastTranscript.vue │ ├── PrimaryPbButton.vue │ ├── ProfileCreationDetails.vue │ ├── ProfileCreationDone.vue │ ├── ProfileCreationEmojis.vue │ ├── ProfileCreationInterests.vue │ ├── ProfileCreationMainInfos.vue │ ├── ProfilePicture.vue │ ├── ScrollDownMouse.vue │ ├── SearchResultCard.vue │ ├── SectionHeading.vue │ ├── SelectionBox.vue │ ├── SimplePodcastTranscript.vue │ ├── SocialNetworks.vue │ ├── SpeakerBubble.vue │ ├── SpeakerList.vue │ ├── SpeakerListItem.vue │ ├── SsoLoginOption.vue │ ├── TagFilter.vue │ ├── TagList.vue │ ├── TalkItem.vue │ └── TypedText.vue ├── composables │ ├── index.ts │ ├── useBodyElement.ts │ ├── useClipboard.ts │ ├── useDirectus.ts │ ├── useDocument.ts │ ├── useEventListener.ts │ ├── useIntersectionObserver.ts │ ├── useLoadingScreen.ts │ ├── useLocaleString.ts │ ├── useMediaQuery.ts │ ├── useMotionParallax.ts │ ├── useMutationObserver.ts │ ├── useNavigator.ts │ ├── usePageMeta.ts │ ├── usePodcastPlayer.ts │ ├── useProfileCreationStore.ts │ ├── useScrollParallax.ts │ ├── useShare.ts │ ├── useTagFilter.ts │ ├── useTagFilterNew.ts │ └── useWindow.ts ├── config.ts ├── error.vue ├── helpers │ ├── getCookie.ts │ ├── getHashCode.ts │ ├── getMetaInfo.ts │ ├── getTrimmedString.ts │ ├── index.ts │ ├── jsonLdGenerator.ts │ ├── prepareTranscript.ts │ ├── setCookie.ts │ ├── sleep.ts │ └── trackGoal.ts ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── pages │ ├── aufnahmen.vue │ ├── datenschutz.vue │ ├── gewinnspiel.vue │ ├── hall-of-fame │ │ ├── [slug].vue │ │ └── index.vue │ ├── impressum.vue │ ├── index.vue │ ├── konferenzen │ │ ├── [slug].vue │ │ └── index.vue │ ├── kontakt.vue │ ├── login-callback.vue │ ├── login.vue │ ├── meetup │ │ ├── [slug].vue │ │ └── index.vue │ ├── pick-of-the-day.vue │ ├── podcast │ │ ├── [slug].vue │ │ └── index.vue │ ├── profile-creation.vue │ ├── profiles │ │ └── [slug].vue │ ├── suche.vue │ ├── ueber-uns.vue │ └── verhaltensregeln.vue ├── plugins │ └── fathom.client.ts ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── icon.png │ └── sw.js ├── server │ ├── api │ │ ├── cocktails.ts │ │ └── email.post.ts │ ├── middleware │ │ ├── discord.ts │ │ ├── flutterday.ts │ │ └── wellKnownWebfinger.ts │ └── utils │ │ ├── index.ts │ │ ├── schema.ts │ │ └── sendEmail.ts ├── services │ ├── directus.ts │ └── index.ts ├── tailwind.config.js ├── tsconfig.json └── types │ ├── directus.ts │ ├── index.ts │ ├── items.ts │ ├── search.ts │ └── types.d.ts ├── shared-code ├── helpers │ ├── getFullPodcastTitle.ts │ ├── getFullSpeakerName.ts │ ├── getPodcastTitleDivider.ts │ ├── getPodcastType.ts │ ├── getPodcastTypeAndNumber.ts │ ├── getUrlSlug.ts │ └── index.ts └── index.ts └── website.code-workspace /.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies, wherever they are 4 | /node_modules 5 | **/node_modules 6 | 7 | # built shared code 8 | directus-cms/shared-code 9 | 10 | /.pnp 11 | .pnp.js 12 | 13 | # testing 14 | /coverage 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | **/.DS_Store 21 | **/.env 22 | **/.env.local 23 | **/.env.development.local 24 | **/.env.test.local 25 | **/.env.production.local 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # ignore all locally built extension artifacts 32 | extensions-src/**/dist 33 | -------------------------------------------------------------------------------- /.github/workflows/directus_publish_on_demand.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Directus CMS 2 | 3 | on: workflow_dispatch 4 | jobs: 5 | build_and_push: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout the repo 9 | uses: actions/checkout@v2 10 | - name: Build image 11 | run: docker build -t programmierbar/cms -f Dockerfile.directus . 12 | - name: Install doctl 13 | uses: digitalocean/action-doctl@v2 14 | with: 15 | token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} 16 | - name: Log in to DO Container Registry 17 | run: doctl registry login --expiry-seconds 600 18 | - name: Get current date 19 | id: date 20 | run: echo "::set-output name=date::$(date +'%Y-%m-%d-%H-%M')" 21 | - name: Tag images 22 | run: | 23 | docker tag programmierbar/cms registry.digitalocean.com/programmierbar/cms:latest 24 | docker tag programmierbar/cms registry.digitalocean.com/programmierbar/cms:${{ steps.date.outputs.date }} 25 | - name: Push images to DO Container Registry 26 | run: | 27 | docker push registry.digitalocean.com/programmierbar/cms:latest 28 | docker push registry.digitalocean.com/programmierbar/cms:${{ steps.date.outputs.date }} 29 | -------------------------------------------------------------------------------- /.github/workflows/directus_publish_staging_on_push.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Directus CMS Staging System 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'staging' 7 | 8 | jobs: 9 | build_and_push: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout the repo 13 | uses: actions/checkout@v2 14 | - name: Build image 15 | run: docker build -t programmierbar/cms:staging -f Dockerfile.directus . 16 | - name: Install doctl 17 | uses: digitalocean/action-doctl@v2 18 | with: 19 | token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} 20 | - name: Log in to DO Container Registry 21 | run: doctl registry login --expiry-seconds 600 22 | - name: Tag image 23 | run: docker tag programmierbar/cms:staging registry.digitalocean.com/programmierbar/cms:staging 24 | - name: Push image to DO Container Registry 25 | run: docker push registry.digitalocean.com/programmierbar/cms:staging 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # tools 2 | ngrok.yaml 3 | 4 | # dependencies 5 | node_modules 6 | /.pnp 7 | .pnp.js 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # compiled code 13 | build 14 | dist 15 | lib 16 | 17 | # IDEs 18 | /.idea 19 | 20 | # misc 21 | *.pem 22 | 23 | # deploy files 24 | app.yaml 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # other 33 | .firebase 34 | /backups 35 | /setup-scripts 36 | 37 | # macos operating system 38 | .DS_Store 39 | .AppleDouble 40 | .LSOverride 41 | ._* 42 | .DocumentRevisions-V100 43 | .fseventsd 44 | .Spotlight-V100 45 | .TemporaryItems 46 | .Trashes 47 | .VolumeIcon.icns 48 | .com.apple.timemachine.donotpresent 49 | .AppleDB 50 | .AppleDesktop 51 | Network Trash Folder 52 | Temporary Items 53 | .apdisk 54 | 55 | # windows operating system 56 | Thumbs.db 57 | Thumbs.db:encryptable 58 | ehthumbs.db 59 | ehthumbs_vista.db 60 | *.stackdump 61 | [Dd]esktop.ini 62 | $RECYCLE.BIN/ 63 | *.cab 64 | *.msi 65 | *.msix 66 | *.msm 67 | *.msp 68 | *.lnk 69 | 70 | # linux operating system 71 | *~ 72 | .fuse_hidden* 73 | .directory 74 | .Trash-* 75 | .nfs* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # compiled code 5 | build 6 | dist 7 | lib 8 | 9 | # other 10 | .firebase 11 | .cache 12 | .tmp 13 | /backup 14 | -------------------------------------------------------------------------------- /Dockerfile.directus: -------------------------------------------------------------------------------- 1 | # Choose a base image 2 | FROM node:22 3 | 4 | # Set working directory 5 | WORKDIR /usr/src/app/directus-cms 6 | 7 | # Add package.json and package-lock.json 8 | COPY directus-cms/package.json . 9 | COPY directus-cms/package-lock.json . 10 | 11 | # Install dependencies 12 | RUN npm install 13 | 14 | # Copy the shared code that lives outside of directus dir 15 | COPY shared-code ../shared-code 16 | 17 | # Copy the rest of the application 18 | COPY directus-cms . 19 | 20 | # Set working directory to interface extension 21 | WORKDIR /usr/src/app/directus-cms/extensions/directus-extension-programmierbar-bundle 22 | 23 | # Install publishable interface extension dependencies 24 | RUN npm install 25 | 26 | # Build the extension 27 | RUN npm run build 28 | 29 | # Set working directory back to directus project root 30 | WORKDIR /usr/src/app/directus-cms 31 | 32 | # Start the app 33 | CMD [ "npm", "run", "start" ] 34 | 35 | # Expose port 36 | EXPOSE 8055 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 programmier.bar 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. -------------------------------------------------------------------------------- /directus-cms/.gitignore: -------------------------------------------------------------------------------- 1 | # database 2 | database/* 3 | !database/.gitkeep 4 | 5 | # uploads 6 | uploads/* 7 | !uploads/.gitkeep 8 | 9 | # env files 10 | .env 11 | 12 | shared-code 13 | 14 | # build artefacts for extensions 15 | node_modules 16 | dist -------------------------------------------------------------------------------- /directus-cms/_requests/.gitignore: -------------------------------------------------------------------------------- 1 | *.private.env.json -------------------------------------------------------------------------------- /directus-cms/_requests/Directus.http: -------------------------------------------------------------------------------- 1 | ### 2 | # Fetches a current schema snapshot from the production instance (You will need to provide your own Bearer auth token) 3 | # The bearer token can be taken from any recent request you made to the instance in your webbrowser. 4 | # 5 | # @Schema 6 | GET https://admin.programmier.bar/schema/snapshot 7 | accept: application/json, text/plain, */* 8 | authorization: Bearer {{bearer-auth-token}} 9 | cache-control: no-cache 10 | 11 | ### 12 | 13 | -------------------------------------------------------------------------------- /directus-cms/database/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/directus-cms/database/.gitkeep -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/.env.dist: -------------------------------------------------------------------------------- 1 | #################################################################################################### 2 | # Used for custom CLI commands (such as rebuilding the search query) 3 | #################################################################################################### 4 | ## General 5 | 6 | FRONTEND_URL="https://programmier.bar/" 7 | PUBLIC_URL="https://admin.programmier.bar/" 8 | 9 | #################################################################################################### 10 | ## Algolia 11 | 12 | ALGOLIA_APP_ID="" 13 | ALGOLIA_API_KEY="" 14 | ALGOLIA_INDEX="" 15 | 16 | #################################################################################################### 17 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@ianvs/prettier-plugin-sort-imports"], 3 | "printWidth": 120, 4 | "tabWidth": 4, 5 | "useTabs": false, 6 | "semi": false, 7 | "singleQuote": true, 8 | "quoteProps": "as-needed", 9 | "jsxSingleQuote": false, 10 | "trailingComma": "es5", 11 | "bracketSpacing": true, 12 | "jsxBracketSameLine": false, 13 | "arrowParens": "always", 14 | "endOfLine": "lf" 15 | } 16 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/assets/browserless.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({ page, context }) => { 2 | // Get URL from context 3 | const { url } = context 4 | 5 | // Set viewport size 6 | await page.setViewport({ width: 1366, height: 768 }) 7 | 8 | // Goto URL and wait for content and animations 9 | await page.goto(url, { timeout: 6000 }) 10 | await page.content() 11 | await page.waitForTimeout(2000) 12 | 13 | // Try accepting cookies to hide cookie notice 14 | // by clicking on any suspicious button 15 | const cookiesAgreed = await page.evaluate(() => { 16 | let cookiesAgreed = false 17 | if (document.body.textContent.match(/cookie/i)) { 18 | document.querySelectorAll('a, button, [type="button"], [type="submit"]').forEach((element) => { 19 | if ( 20 | [element.textContent, element.name, element.value].some( 21 | (content) => 22 | typeof content === 'string' && 23 | content.match( 24 | /(akzeptieren|verstanden|zustimmen|stimme zu|okay|^ok|accept|understand|agree|allow|enable|close)/i 25 | ) 26 | ) 27 | ) { 28 | element.click() 29 | cookiesAgreed = true 30 | } 31 | }) 32 | } 33 | return cookiesAgreed 34 | }) 35 | 36 | // If cookies have been agreed, wait for animations 37 | if (cookiesAgreed) { 38 | await page.waitForTimeout(500) 39 | } 40 | 41 | // Take screenshot of website 42 | const data = await page.screenshot({ 43 | type: 'jpeg', 44 | quality: 80, 45 | encoding: 'base64', 46 | }) 47 | 48 | // Return screenshot as buffer 49 | return { type: 'text/plain', data } 50 | } 51 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js' 2 | import tseslint from 'typescript-eslint' 3 | 4 | export default tseslint.config( 5 | { 6 | ignores: ['**/dist/**', '**/podcast-transcription/**', '**/assets/**', 'eslint.config.js'], 7 | }, 8 | eslint.configs.recommended, 9 | { 10 | plugins: { 11 | '@typescript-eslint': tseslint.plugin, 12 | }, 13 | languageOptions: { 14 | parser: tseslint.parser, 15 | parserOptions: { 16 | project: true, 17 | }, 18 | globals: { 19 | process: 'readable', 20 | }, 21 | }, 22 | rules: { 23 | '@typescript-eslint/consistent-type-imports': 'error', 24 | }, 25 | files: ['**/*.ts'], 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/algolia-index/handlers/ItemHandler.ts: -------------------------------------------------------------------------------- 1 | export interface ItemHandler { 2 | collectionName: string; 3 | updateRequired(item: any): boolean; 4 | buildAttributes(item: any): Record[]; 5 | requiresDistinctDeletionBeforeUpdate(): boolean; 6 | buildDistinctKey(item: any): string; 7 | buildDeletionFilter(item: any): string; 8 | buildDirectusReference(item: any): string; 9 | } 10 | 11 | export abstract class AbstractItemHandler { 12 | 13 | constructor(protected env, private logger) { 14 | } 15 | 16 | requiresDistinctDeletionBeforeUpdate(): boolean { 17 | return false; 18 | } 19 | 20 | buildDistinctKey(item: any): string { 21 | return `${item.id}`; 22 | } 23 | 24 | buildDirectusReference(item: any): string { 25 | return `${item.id}`; 26 | } 27 | 28 | buildDeletionFilter(item: any): string { 29 | return `_directus_reference:${this.buildDirectusReference(item)}`; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/algolia-index/handlers/MeetupHandler.ts: -------------------------------------------------------------------------------- 1 | import { AbstractItemHandler } from './ItemHandler.ts'; 2 | 3 | export class MeetupHandler extends AbstractItemHandler{ 4 | 5 | get collectionName(): string { 6 | return 'meetups'; 7 | } 8 | 9 | updateRequired(item: any): boolean { 10 | return ( 11 | item.title || 12 | item.slug || 13 | item.description || 14 | item.published_on || 15 | item.cover_image 16 | ) 17 | } 18 | 19 | buildAttributes(item: any): Record[] { 20 | return [{ 21 | _type : 'meetup', 22 | title: item.title, 23 | description: item.description, 24 | published_on: item.published_on, 25 | image: item.cover_image ? `${this.env.PUBLIC_URL}assets/${item.cover_image}` : undefined, 26 | slug: item.slug, 27 | }] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/algolia-index/handlers/PickOfTheDayHandler.ts: -------------------------------------------------------------------------------- 1 | import { AbstractItemHandler, ItemHandler } from './ItemHandler.ts'; 2 | 3 | export class PickOfTheDayHandler extends AbstractItemHandler { 4 | 5 | get collectionName(): string { 6 | return 'picks_of_the_day'; 7 | } 8 | 9 | updateRequired(item: any): boolean { 10 | return ( 11 | item.name || 12 | item.description || 13 | item.website_url || 14 | item.published_on || 15 | item.image 16 | ) 17 | } 18 | 19 | buildAttributes(item: any): Record[] { 20 | return [{ 21 | _type : 'pick_of_the_day', 22 | name: item.name, 23 | description: item.description, 24 | website_url: item.website_url, 25 | published_on: item.published_on, 26 | image: item.image ? `${this.env.PUBLIC_URL}assets/${item.image}` : undefined, 27 | }] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/algolia-index/handlers/PodcastHandler.ts: -------------------------------------------------------------------------------- 1 | import { AbstractItemHandler } from './ItemHandler.ts'; 2 | import { sanitize, sanitizeFull } from '../util/sanitizer.ts'; 3 | 4 | export class PodcastHandler extends AbstractItemHandler { 5 | 6 | get collectionName(): string { 7 | return 'podcasts'; 8 | } 9 | 10 | buildDistinctKey(item: any): string { 11 | return `podcast-${item.id}`; 12 | } 13 | 14 | updateRequired(item: any): boolean { 15 | return ( 16 | item.title || 17 | item.number || 18 | item.slug || 19 | item.description || 20 | item.type || 21 | item.published_on || 22 | item.cover_image 23 | ) 24 | } 25 | 26 | buildAttributes(item: any): Record[] { 27 | 28 | // This is a simple workaround for the algolia size-limit per index-entry 29 | // Ideally, we would split this out into multiple index entries later 30 | let description = sanitize(item.description); 31 | if (description.length > 2500) { 32 | description = sanitizeFull(item.description); 33 | } 34 | 35 | return [{ 36 | _type : 'podcast', 37 | title: item.title, 38 | number: item.number, 39 | description: description, 40 | type: item.type, 41 | published_on: item.published_on, 42 | image: item.cover_image ? `${this.env.PUBLIC_URL}assets/${item.cover_image}` : undefined, 43 | slug: item.slug, 44 | }] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/algolia-index/handlers/SpeakerHandler.ts: -------------------------------------------------------------------------------- 1 | import { AbstractItemHandler } from './ItemHandler.ts'; 2 | 3 | export class SpeakerHandler extends AbstractItemHandler { 4 | 5 | get collectionName(): string { 6 | return 'speakers'; 7 | } 8 | 9 | updateRequired(item: any): boolean { 10 | return ( 11 | item.first_name || 12 | item.last_name || 13 | item.academic_title || 14 | item.description || 15 | item.published_on || 16 | item.profile_image || 17 | item.slug 18 | ) 19 | } 20 | 21 | buildAttributes(item: any): Record[] { 22 | return [{ 23 | _type : 'speaker', 24 | first_name: item.first_name, 25 | last_name: item.last_name, 26 | academic_title: item.academic_title, 27 | description: item.description, 28 | published_on: item.published_on, 29 | slug: item.slug, 30 | image: item.profile_image ? `${this.env.PUBLIC_URL}assets/${item.profile_image}` : undefined, 31 | }] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/algolia-index/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { PodcastHandler } from "./PodcastHandler.ts" 2 | import { ItemHandler } from "./ItemHandler.ts" 3 | import { MeetupHandler } from './MeetupHandler.ts'; 4 | import { SpeakerHandler } from './SpeakerHandler.ts'; 5 | import { PickOfTheDayHandler } from './PickOfTheDayHandler.ts'; 6 | import { TranscriptHandler } from './TranscriptHandler.js'; 7 | 8 | type knownHandlers = "podcastHandler" | "meetupHandler" | "speakerHandler" | "pickOfTheDayHandler" | "transcriptHandler"; 9 | 10 | export function getHandlers(env, logger): Record { 11 | return { 12 | podcastHandler: new PodcastHandler(env, logger), 13 | meetupHandler: new MeetupHandler(env, logger), 14 | speakerHandler: new SpeakerHandler(env, logger), 15 | pickOfTheDayHandler: new PickOfTheDayHandler(env, logger), 16 | transcriptHandler: new TranscriptHandler(env, logger), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/algolia-index/util/sanitizer.ts: -------------------------------------------------------------------------------- 1 | import sanitizeHtml from 'sanitize-html'; 2 | 3 | export function sanitize(input: string): string { 4 | return sanitizeHtml(input, { 5 | allowedTags: [ 'a', 'p', 'ul', 'li' ], 6 | allowedAttributes: { 7 | 'a': [ 'href' ] 8 | } 9 | }); 10 | } 11 | 12 | export function sanitizeFull(input: string): string { 13 | return sanitizeHtml(input, { 14 | allowedTags: [ ], 15 | allowedAttributes: { } 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/create-profile/index.ts: -------------------------------------------------------------------------------- 1 | import { defineHook } from '@directus/extensions-sdk' 2 | import { createHookErrorConstructor } from '../shared/errors.ts' 3 | import { FilterHandler} from '@directus/types' 4 | 5 | const HOOK_NAME = 'create-profile' 6 | 7 | export default defineHook(({ filter }, hookContext) => { 8 | const logger = hookContext.logger 9 | const ItemsService = hookContext.services.ItemsService 10 | 11 | type UserPayloadType = { 12 | profiles: { 13 | profiles_id: string, 14 | }[] | undefined, 15 | } 16 | 17 | const handler: FilterHandler = async function(payload, _metadata, context): Promise { 18 | try { 19 | logger.info(`${HOOK_NAME} hook: Start filter function`) 20 | 21 | if (payload.profiles && payload.profiles.length > 0) { 22 | logger.info(`${HOOK_NAME} hook: User already has profile. Exiting early.`) 23 | return payload 24 | } 25 | 26 | // Note that we manually set the knex/database connection here. 27 | // As this called from a filter, we are within an ongoing database transaction, and the database is locked 28 | // Meaning we need to manually use the existing connection, as a new one would not be available. 29 | const profilesItemsService = new ItemsService('profiles', { 30 | accountability: context.accountability, 31 | schema: context.schema, 32 | knex: context.database, 33 | }) 34 | 35 | const newProfileId = await profilesItemsService.createOne({}); 36 | 37 | logger.info(`${HOOK_NAME} hook: Created profile ${newProfileId} for newly created user.`) 38 | 39 | return { 40 | ...payload, 41 | // the following structure is necessary for directus to make the m2m connection 42 | profiles: [ 43 | { 44 | profiles_id: newProfileId as string, 45 | }, 46 | ] 47 | } 48 | 49 | // Handle unknown errors 50 | } catch (error: any) { 51 | logger.error(`${HOOK_NAME} hook: Error: ${error.message}`) 52 | const hookError = createHookErrorConstructor(HOOK_NAME, error.message) 53 | throw new hookError() 54 | } 55 | } 56 | 57 | filter('users.create', handler) 58 | }) 59 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/podcast-transcript/index.ts: -------------------------------------------------------------------------------- 1 | import { defineHook } from '@directus/extensions-sdk' 2 | 3 | import generateTranscriptItem from './generateTranscriptItem.js'; 4 | import processTranscriptItem from './processTranscriptItem.js'; 5 | 6 | const HOOK_NAME = 'podcast-transcript-create' 7 | 8 | export default defineHook(({ action, schedule }, hookContext) => { 9 | const logger = hookContext.logger 10 | const ItemsService = hookContext.services.ItemsService 11 | const getSchema = hookContext.getSchema 12 | const env = hookContext.env 13 | 14 | action('podcasts.items.create', ({ payload, ...metadata }, context) => 15 | generateTranscriptItem(HOOK_NAME, { payload, metadata, context }, {logger, ItemsService}) 16 | ) 17 | 18 | action('podcasts.items.update', ({ payload, ...metadata }, context) => 19 | generateTranscriptItem(HOOK_NAME, { payload, metadata, context }, {logger, ItemsService}) 20 | ) 21 | 22 | schedule('*/5 * * * *', 23 | processTranscriptItem(HOOK_NAME, {logger, ItemsService, getSchema, env}) 24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/publishable/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/extensions-sdk' 2 | import InterfaceComponent from './presentation-publishable.vue' 3 | 4 | export default defineInterface({ 5 | id: 'publishable', 6 | name: 'Publishable', 7 | description: 'Indicates whether an item is ready to be published automatically', 8 | icon: 'info', 9 | component: InterfaceComponent, 10 | types: ['alias'], 11 | localTypes: ['presentation'], 12 | group: 'presentation', 13 | hideLabel: true, 14 | options: null, 15 | }) 16 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/publishable/presentation-publishable.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 76 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/publishable/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/errors.ts: -------------------------------------------------------------------------------- 1 | import { createError } from '@directus/errors' 2 | 3 | export function createHookErrorConstructor(hook: string, message: string) { 4 | return createError(hook, message) 5 | } 6 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/isPublishable.ts: -------------------------------------------------------------------------------- 1 | interface Field { 2 | field: string 3 | schema: { 4 | required: boolean 5 | } 6 | meta: { 7 | conditions: { 8 | required: boolean 9 | rule: Record< 10 | string, 11 | { 12 | status: { 13 | _eq: string 14 | } 15 | }[] 16 | > 17 | }[] 18 | } 19 | } 20 | 21 | // eslint-disable-next-line no-unused-vars 22 | type LoggerFunction = (message: string) => void 23 | 24 | export function isPublishable(item: Record, fields: Field[], logger?: LoggerFunction) { 25 | const requiredFieldsAreSet = fields.every((field) => { 26 | ;(() => logger?.('Controlling field ' + field.field))() 27 | 28 | const hasValue = Boolean(item[field.field]) 29 | const isRequiredInSchema = field.schema && field.schema.required 30 | const hasConditions = Boolean(field.meta.conditions) 31 | 32 | let isRequiredOnPublished = false 33 | if (hasConditions) { 34 | ;(() => logger?.('Has conditions'))() 35 | ;(() => logger?.(JSON.stringify(field.meta.conditions)))() 36 | 37 | isRequiredOnPublished = field.meta.conditions.some((condition) => { 38 | ;(() => logger?.('condition ' + JSON.stringify(condition)))() 39 | return ( 40 | condition.required && 41 | condition.rule && 42 | condition.rule._and && 43 | condition.rule._and.some( 44 | (rule) => rule.status && rule.status._eq && rule.status._eq === 'published' 45 | ) 46 | ) 47 | }) 48 | 49 | ;(() => logger?.('is required on published ' + isRequiredOnPublished))() 50 | } 51 | const isOptional = !isRequiredInSchema && !isRequiredOnPublished 52 | 53 | ;(() => logger?.('Is set: ' + (hasValue || isOptional)))() 54 | 55 | return hasValue || isOptional 56 | }) 57 | 58 | return requiredFieldsAreSet 59 | } 60 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/postSlackMessage.ts: -------------------------------------------------------------------------------- 1 | import { WebClient } from '@slack/web-api' 2 | 3 | // Create Slack web client instance 4 | const webClient = new WebClient(process.env.SLACK_BOT_TOKEN as string) 5 | 6 | /** 7 | * A helper function that posts a Slack message to a specific channel. 8 | * 9 | * @param text The text to send. 10 | */ 11 | export async function postSlackMessage(text: string): Promise { 12 | try { 13 | await webClient.chat.postMessage({ 14 | channel: process.env.SLACK_CHANNEL_ID as string, 15 | text, 16 | }) 17 | } catch (error: any) { 18 | throw new Error(`Slack: ${error.message}`) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /directus-cms/extensions/directus-extension-programmierbar-bundle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020", "DOM"], 5 | "moduleResolution": "nodenext", 6 | "strict": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "noImplicitThis": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noUnusedParameters": true, 15 | "alwaysStrict": true, 16 | "strictNullChecks": true, 17 | "strictFunctionTypes": true, 18 | "strictBindCallApply": true, 19 | "strictPropertyInitialization": true, 20 | "resolveJsonModule": false, 21 | "skipLibCheck": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "allowSyntheticDefaultImports": true, 24 | "isolatedModules": true, 25 | "rootDir": "./src", 26 | "allowImportingTsExtensions": true 27 | }, 28 | "include": ["./src/**/*.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /directus-cms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "directus-cms", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "type": "commonjs", 7 | "scripts": { 8 | "migrate:db": "directus database migrate:latest", 9 | "postbootstrap": "npm run migrate:db", 10 | "bootstrap": "directus bootstrap", 11 | "prestart": "npm run migrate:db", 12 | "start": "directus start", 13 | "snapshot-schema": "directus schema snapshot ./schema.json --format json", 14 | "apply-schema": "directus schema apply ./schema.json" 15 | }, 16 | "dependencies": { 17 | "@slack/web-api": "^6.5.1", 18 | "axios": "^0.24.0", 19 | "directus": "^11.0", 20 | "jsonwebtoken": "^8.5.1", 21 | "typescript": "^5.2.2" 22 | }, 23 | "devDependencies": { 24 | "prettier": "^2.5.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /directus-cms/run-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Use this command to verify the built docker container" 4 | echo "For local development you can also use: 'yarn run start'" 5 | 6 | # Check if the Docker image "programmierbar/cms" is locally available 7 | if [[ "$(docker images -q registry.digitalocean.com/programmierbar/cms 2> /dev/null)" == "" ]]; then 8 | echo "Docker image 'programmierbar/cms' is not locally available." 9 | echo "Please pull the image before running the container." 10 | echo "Or build the image locally from the project root:" 11 | echo "'docker build -t programmierbar/cms -f Dockerfile.directus .'" 12 | exit 1 13 | fi 14 | 15 | docker run -d --rm \ 16 | -v "./.env:/usr/src/app/directus-cms/.env" \ 17 | -v "./database:/usr/src/app/directus-cms/database" \ 18 | -v "./extensions:/usr/src/app/directus-cms/extensions" \ 19 | -p "8055:8055" \ 20 | --platform linux/amd64 \ 21 | --name "programmierbar-website" \ 22 | "registry.digitalocean.com/programmierbar/cms:latest" 23 | 24 | echo "Started container..." 25 | -------------------------------------------------------------------------------- /directus-cms/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/directus-cms/uploads/.gitkeep -------------------------------------------------------------------------------- /directus-cms/utils/transcription.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios').default; 2 | 3 | async function create(title, url, env, logger) { 4 | const payload = { 5 | transcription: { 6 | name: title, 7 | language: 'de-DE', 8 | tmp_url: url, 9 | folder: 'programmierbar/episodes', 10 | is_subtitle: false, 11 | organization_id: '491', 12 | use_vocabulary: true, 13 | }, 14 | }; 15 | // Create or update podcast episode at Buzzsprout 16 | const response = await sendToHappyScribe( 17 | 'POST', 18 | 'transcriptions', 19 | payload, 20 | env 21 | ); 22 | 23 | return response.data.id; 24 | } 25 | 26 | async function isDone(id, env) { 27 | const response = await sendToHappyScribe( 28 | 'GET', 29 | `transcriptions/${id}`, 30 | null, 31 | env 32 | ); 33 | 34 | return response.data.state === 'automatic_done'; 35 | } 36 | 37 | async function createExport(id, env) { 38 | const payload = { 39 | export: { 40 | format: 'txt', 41 | transcription_ids: [id], 42 | }, 43 | }; 44 | const response = await sendToHappyScribe('POST', `exports`, payload, env); 45 | 46 | return response.data.id; 47 | } 48 | 49 | async function getExportUrl(id, env) { 50 | const response = await sendToHappyScribe('GET', `exports/${id}`, null, env); 51 | 52 | if (['expired', 'failed'].includes(response.data.state)) { 53 | throw new Error(`Export failed with the status "${response.data.state}".`); 54 | } 55 | 56 | const { download_link } = response.data; 57 | 58 | if (!download_link) return null; 59 | 60 | const transcriptResponse = await axios({ url: download_link, method: 'GET' }); 61 | 62 | return transcriptResponse.data; 63 | } 64 | 65 | async function sendToHappyScribe(method, path, payload, env) { 66 | const config = { 67 | method, 68 | url: `${env.HAPPYSCRIBE_API_URL}${path}`, 69 | headers: { 70 | Authorization: `Bearer ${env.HAPPYSCRIBE_API_KEY}`, 71 | 'Content-Type': 'application/json', 72 | }, 73 | }; 74 | 75 | if (payload) { 76 | config.data = JSON.stringify(payload); 77 | } 78 | const response = await axios(config); 79 | 80 | // Throw error if the request was not successful 81 | if (response.status !== 200 && response.status !== 201) { 82 | throw new Error( 83 | `HappyScribe replied with the status "${response.status}" and the text "${response.statusText}".` 84 | ); 85 | } 86 | return response; 87 | } 88 | module.exports = { create, isDone, createExport, getExportUrl }; 89 | -------------------------------------------------------------------------------- /nuxt-app/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /nuxt-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | extends: ['@nuxt/eslint-config', 'plugin:nuxt/recommended', 'prettier'], 8 | plugins: [], 9 | // add your custom rules here 10 | rules: { 11 | 'vue/multi-word-component-names': 0, 12 | '@typescript-eslint/consistent-type-imports': 'error', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /nuxt-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Use this for local overwrites during development 2 | .env 3 | 4 | node_modules 5 | *.log* 6 | .nuxt 7 | .nitro 8 | .cache 9 | .output 10 | dist 11 | -------------------------------------------------------------------------------- /nuxt-app/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /nuxt-app/.nvmrc: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /nuxt-app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"], 3 | "printWidth": 120, 4 | "tabWidth": 4, 5 | "useTabs": false, 6 | "semi": false, 7 | "singleQuote": true, 8 | "quoteProps": "as-needed", 9 | "jsxSingleQuote": false, 10 | "trailingComma": "es5", 11 | "bracketSpacing": true, 12 | "jsxBracketSameLine": false, 13 | "arrowParens": "always", 14 | "endOfLine": "lf" 15 | } 16 | -------------------------------------------------------------------------------- /nuxt-app/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | ## Setup 4 | 5 | Make sure to install the dependencies: 6 | 7 | ```bash 8 | # npm 9 | npm install 10 | ``` 11 | 12 | Prepare your IDE as follows: 13 | 14 | - https://prettier.io/docs/en/webstorm 15 | - https://www.jetbrains.com/help/webstorm/eslint.html#ws_js_eslint_automatic_configuration 16 | 17 | ## Development Server 18 | 19 | Start the development server on http://localhost:3000 20 | 21 | ```bash 22 | npm run dev 23 | ``` 24 | 25 | ## Production 26 | 27 | Build the application for production: 28 | 29 | ```bash 30 | npm run build 31 | ``` 32 | 33 | Locally preview production build: 34 | 35 | ```bash 36 | npm run preview 37 | ``` 38 | -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/AzeretMono-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/AzeretMono-Medium.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/AzeretMono-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/AzeretMono-Medium.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/DMMono-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/DMMono-Light.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/DMMono-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/DMMono-Light.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-100-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-100-italic.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-100-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-100-italic.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-100.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-100.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-100.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-100.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-300-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-300-italic.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-300-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-300-italic.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-300.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-300.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-500-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-500-italic.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-500-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-500-italic.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-500.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-500.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-700-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-700-italic.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-700-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-700-italic.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-700.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-700.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-900-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-900-italic.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-900-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-900-italic.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-900.woff -------------------------------------------------------------------------------- /nuxt-app/assets/fonts/museo-sans-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/assets/fonts/museo-sans-900.woff2 -------------------------------------------------------------------------------- /nuxt-app/assets/icons/15-sec-backwards.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 seconds backwards 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/15-sec-forwards.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 seconds forwards 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/angle-down.svg: -------------------------------------------------------------------------------- 1 | 2 | Angle down 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/angle-left.svg: -------------------------------------------------------------------------------- 1 | 2 | Angle left 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/angle-right.svg: -------------------------------------------------------------------------------- 1 | 2 | Angle right 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/angle-up.svg: -------------------------------------------------------------------------------- 1 | 2 | Angle up 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/backwards.svg: -------------------------------------------------------------------------------- 1 | 2 | Backwards 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | Download 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/forwards.svg: -------------------------------------------------------------------------------- 1 | 2 | Forwards 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | Heart 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/leave-site.svg: -------------------------------------------------------------------------------- 1 | 2 | Leave site 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/pause-circle-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | Pause circle 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | Pause 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/play-circle-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | Play circle 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/play-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | Play circle 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | Play 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | Search 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/semicircle.svg: -------------------------------------------------------------------------------- 1 | 2 | Semicircle 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/share.svg: -------------------------------------------------------------------------------- 1 | 2 | Share 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/icons/sound.svg: -------------------------------------------------------------------------------- 1 | 2 | Sound 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/images/brand-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | programmier.bar icon 3 | 4 | 5 | -------------------------------------------------------------------------------- /nuxt-app/assets/images/cheers-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nuxt-app/assets/images/profile-picture-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/apple-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | Apple Calendar 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/apple-podcasts-color.svg: -------------------------------------------------------------------------------- 1 | 2 | Apple Podcasts 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/bluesky-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/bluesky.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/facebook-color.svg: -------------------------------------------------------------------------------- 1 | 2 | Facebook 3 | 4 | 5 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | Facebook 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/google-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | Google Calendar 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/google-maps.svg: -------------------------------------------------------------------------------- 1 | 2 | Google Maps 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/linkedin-color.svg: -------------------------------------------------------------------------------- 1 | 2 | LinkedIn 3 | 4 | 5 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/mastodon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/meetup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/rss-feed-color.svg: -------------------------------------------------------------------------------- 1 | 2 | RSS feed 3 | 4 | 5 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/spotify-color.svg: -------------------------------------------------------------------------------- 1 | 2 | Spotify 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/twitter-color.svg: -------------------------------------------------------------------------------- 1 | 2 | Twitter 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/website-color.svg: -------------------------------------------------------------------------------- 1 | 2 | Website 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/youtube-color.svg: -------------------------------------------------------------------------------- 1 | 2 | YouTube 3 | 4 | 5 | -------------------------------------------------------------------------------- /nuxt-app/assets/logos/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nuxt-app/components/Breadcrumbs.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 38 | -------------------------------------------------------------------------------- /nuxt-app/components/ConferenceAgenda.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 46 | 47 | 49 | -------------------------------------------------------------------------------- /nuxt-app/components/ConferenceCard.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 67 | -------------------------------------------------------------------------------- /nuxt-app/components/ConferenceCover.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 88 | -------------------------------------------------------------------------------- /nuxt-app/components/ConferenceSection.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 55 | -------------------------------------------------------------------------------- /nuxt-app/components/ConferenceSpeaker.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 62 | 63 | 68 | -------------------------------------------------------------------------------- /nuxt-app/components/DirectusImage.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 51 | -------------------------------------------------------------------------------- /nuxt-app/components/EmbeddedVideoPlayer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 35 | 36 | 42 | -------------------------------------------------------------------------------- /nuxt-app/components/EmojiPicker.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 57 | -------------------------------------------------------------------------------- /nuxt-app/components/FaqItem.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /nuxt-app/components/FaqList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /nuxt-app/components/FeedbackSection.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /nuxt-app/components/GenericListItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 59 | -------------------------------------------------------------------------------- /nuxt-app/components/InnerHtml.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 34 | 35 | 69 | -------------------------------------------------------------------------------- /nuxt-app/components/InputField.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 24 | 25 | 36 | -------------------------------------------------------------------------------- /nuxt-app/components/InputFieldWithHeadline.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /nuxt-app/components/LikeButton.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 32 | -------------------------------------------------------------------------------- /nuxt-app/components/LinkButton.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 35 | 36 | 55 | -------------------------------------------------------------------------------- /nuxt-app/components/LoadingScreen.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | 34 | -------------------------------------------------------------------------------- /nuxt-app/components/LoginButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | 40 | -------------------------------------------------------------------------------- /nuxt-app/components/MeetupCard.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 72 | -------------------------------------------------------------------------------- /nuxt-app/components/MeetupSection.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 55 | -------------------------------------------------------------------------------- /nuxt-app/components/MeetupStartAndEnd.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 68 | -------------------------------------------------------------------------------- /nuxt-app/components/NewsTicker.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 41 | 42 | 55 | -------------------------------------------------------------------------------- /nuxt-app/components/PageCoverImage.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /nuxt-app/components/PickOfTheDayList.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 29 | -------------------------------------------------------------------------------- /nuxt-app/components/PickOfTheDayListItem.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 54 | -------------------------------------------------------------------------------- /nuxt-app/components/PrimaryPbButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 38 | -------------------------------------------------------------------------------- /nuxt-app/components/ProfileCreationDetails.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 71 | 72 | 77 | -------------------------------------------------------------------------------- /nuxt-app/components/ProfileCreationDone.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /nuxt-app/components/ProfileCreationEmojis.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 33 | -------------------------------------------------------------------------------- /nuxt-app/components/ProfileCreationMainInfos.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /nuxt-app/components/ProfilePicture.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 45 | -------------------------------------------------------------------------------- /nuxt-app/components/SectionHeading.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | -------------------------------------------------------------------------------- /nuxt-app/components/SimplePodcastTranscript.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 43 | -------------------------------------------------------------------------------- /nuxt-app/components/SpeakerList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | -------------------------------------------------------------------------------- /nuxt-app/components/SpeakerListItem.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 85 | -------------------------------------------------------------------------------- /nuxt-app/components/SsoLoginOption.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /nuxt-app/components/TagList.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 55 | -------------------------------------------------------------------------------- /nuxt-app/components/TalkItem.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 74 | 75 | 77 | -------------------------------------------------------------------------------- /nuxt-app/components/TypedText.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | 34 | 47 | -------------------------------------------------------------------------------- /nuxt-app/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useBodyElement' 2 | export * from './useClipboard' 3 | export * from './useDocument' 4 | export * from './useEventListener' 5 | export * from './useIntersectionObserver' 6 | export * from './useLoadingScreen' 7 | export * from './useLocaleString' 8 | export * from './useMediaQuery' 9 | export * from './useMotionParallax' 10 | export * from './useMutationObserver' 11 | export * from './useNavigator' 12 | export * from './usePageMeta' 13 | export * from './usePodcastPlayer' 14 | export * from './useScrollParallax' 15 | export * from './useShare' 16 | export * from './useTagFilter' 17 | export * from './useWindow' 18 | -------------------------------------------------------------------------------- /nuxt-app/composables/useBodyElement.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, ref } from 'vue' 2 | 3 | /** 4 | * Composable for secure access of the document object. 5 | * 6 | * @returns A reference to the document object. 7 | */ 8 | export function useBodyElement() { 9 | const bodyElement = ref() 10 | 11 | onMounted(() => { 12 | bodyElement.value = document.body 13 | }) 14 | 15 | return bodyElement 16 | } 17 | -------------------------------------------------------------------------------- /nuxt-app/composables/useClipboard.ts: -------------------------------------------------------------------------------- 1 | import { reactive, ref, watch } from 'vue' 2 | import { useNavigator } from './useNavigator' 3 | 4 | /** 5 | * Composable for secure use of the share API. 6 | * 7 | * @returns A reference to the share API. 8 | */ 9 | export function useClipboard() { 10 | const navigator = useNavigator() 11 | const isSupported = ref(false) 12 | const copied = ref(false) 13 | 14 | watch(navigator, () => { 15 | isSupported.value = !!navigator.value?.clipboard 16 | }) 17 | 18 | const copy = (data: string) => { 19 | navigator.value?.clipboard.writeText(data).then(() => { 20 | copied.value = true 21 | setTimeout(() => { 22 | copied.value = false 23 | }, 1500) 24 | }) 25 | } 26 | 27 | return reactive({ 28 | isSupported, 29 | copied, 30 | copy, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /nuxt-app/composables/useDocument.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, ref } from 'vue' 2 | 3 | /** 4 | * Composable for secure access of the document object. 5 | * 6 | * @returns A reference to the document object. 7 | */ 8 | export function useDocument() { 9 | const _document = ref() 10 | 11 | onMounted(() => { 12 | _document.value = document 13 | }) 14 | 15 | return _document 16 | } 17 | -------------------------------------------------------------------------------- /nuxt-app/composables/useEventListener.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-redeclare */ 2 | import type { Ref } from 'vue' 3 | import { onBeforeUnmount, onMounted, watch } from 'vue' 4 | 5 | /** 6 | * Composable to add an event listener to an HTML 7 | * element, the document or window object. 8 | * 9 | * @param target The event target. 10 | * @param type The event type. 11 | * @param listener The event listener. 12 | */ 13 | export function useEventListener( 14 | target: Ref, 15 | type: V, 16 | listener: (this: T, event: U[V]) => any 17 | ): void 18 | export function useEventListener( 19 | target: Ref, 20 | type: V, 21 | listener: (this: T, event: U[V]) => any 22 | ): void 23 | export function useEventListener( 24 | target: Ref, 25 | type: V, 26 | listener: (this: T, event: U[V]) => any 27 | ): void 28 | export function useEventListener( 29 | target: Ref, 30 | type: V, 31 | listener: (this: T, event: U[V]) => any 32 | ): void 33 | export function useEventListener(target: Ref, type: any, listener: any) { 34 | // Add event listener to target when component has been mounted 35 | onMounted(() => { 36 | if (target.value) { 37 | target.value.addEventListener(type, listener) 38 | } 39 | }) 40 | 41 | // Remove event listener from target before component is unmounted 42 | onBeforeUnmount(() => { 43 | if (target.value) { 44 | target.value.removeEventListener(type, listener as EventListenerOrEventListenerObject) 45 | } 46 | }) 47 | 48 | // Update event listener when target changes 49 | watch(target, (newTarget, prevTarget) => { 50 | if (prevTarget) { 51 | prevTarget.removeEventListener(type, listener) 52 | } 53 | if (newTarget) { 54 | newTarget.addEventListener(type, listener) 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /nuxt-app/composables/useIntersectionObserver.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { onBeforeUnmount, onMounted, watch } from 'vue' 3 | 4 | /** 5 | * Composable for observing the visibility of an 6 | * HTML element with a intersection observer. 7 | * 8 | * @param target The observation target. 9 | * @param listener The observation listener. 10 | * @param options The observation options. 11 | */ 12 | export function useIntersectionObserver( 13 | target: Ref, 14 | listener: IntersectionObserverCallback, 15 | options?: IntersectionObserverInit | undefined 16 | ): { disconnect: () => void } { 17 | let intersectionObserver: IntersectionObserver 18 | 19 | onMounted(() => { 20 | if (target.value) { 21 | intersectionObserver = new IntersectionObserver(listener) 22 | intersectionObserver.observe(target.value) 23 | } 24 | }) 25 | 26 | onBeforeUnmount(() => { 27 | intersectionObserver?.disconnect() 28 | }) 29 | 30 | watch(target, () => { 31 | intersectionObserver?.disconnect() 32 | if (target.value) { 33 | intersectionObserver = new IntersectionObserver(listener, options) 34 | intersectionObserver.observe(target.value) 35 | } 36 | }) 37 | 38 | const disconnect = () => { 39 | intersectionObserver?.disconnect() 40 | } 41 | 42 | return { disconnect } 43 | } 44 | -------------------------------------------------------------------------------- /nuxt-app/composables/useLoadingScreen.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { reactive, ref, watch } from 'vue' 3 | 4 | // Global is loading state 5 | const isLoading = ref(false) 6 | 7 | /** 8 | * Composable to set and get the global state of the loading screen. 9 | * 10 | * @param dataList A list of fetched data. 11 | * 12 | * @returns The state of the loading screen. 13 | */ 14 | export function useLoadingScreen(...dataList: Ref[]) { 15 | // Set initial state 16 | isLoading.value = dataList.some((data) => !data.value) 17 | 18 | // Change state on update 19 | watch(dataList, () => { 20 | isLoading.value = dataList.some((data) => !data.value) 21 | }) 22 | 23 | // Return state of loading screen 24 | return reactive({ isLoading }) 25 | } 26 | -------------------------------------------------------------------------------- /nuxt-app/composables/useLocaleString.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { computed } from 'vue' 3 | 4 | /** 5 | * Composable that converts a number to a local string. 6 | * 7 | * @param ref The value reference. 8 | * 9 | * @returns The local string. 10 | */ 11 | export function useLocaleString(ref: Ref) { 12 | return computed(() => ref.value?.toLocaleString('de-DE') || null) 13 | } 14 | -------------------------------------------------------------------------------- /nuxt-app/composables/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | import { useEventListener } from './useEventListener' 3 | import { useWindow } from './useWindow' 4 | 5 | /** 6 | * Composable to check if a media query matches the current device. 7 | * 8 | * @param query The media query string. 9 | * 10 | * @returns Whether the media query matches. 11 | */ 12 | export function useMediaQuery(query: string) { 13 | const window = useWindow() 14 | const mediaQuery = ref(window.value?.matchMedia(query)) 15 | const matches = ref(!!mediaQuery.value?.matches) 16 | 17 | watch(window, () => { 18 | mediaQuery.value = window.value?.matchMedia(query) 19 | matches.value = !!mediaQuery.value?.matches 20 | }) 21 | 22 | const listener = (event: MediaQueryListEvent) => { 23 | matches.value = event.matches 24 | } 25 | 26 | useEventListener(mediaQuery, 'change', listener) 27 | 28 | return matches 29 | } 30 | -------------------------------------------------------------------------------- /nuxt-app/composables/useMotionParallax.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { reactive } from 'vue' 3 | import { useEventListener } from '.' 4 | 5 | /** 6 | * Composable to add parallax effects when 7 | * moving the mouse over an element. 8 | * 9 | * @param target The target element. 10 | * 11 | * @returns The current parallax state. 12 | */ 13 | export function useMotionParallax(target: Ref) { 14 | const motionParallax = reactive({ roll: 0, tilt: 0, isActive: false }) 15 | 16 | const updatePosition = (event: MouseEvent) => { 17 | if (target.value) { 18 | const { offsetX, offsetY } = event 19 | const { clientWidth, clientHeight } = target.value 20 | motionParallax.tilt = (offsetX - clientWidth / 2) / clientWidth 21 | motionParallax.roll = (offsetY - clientHeight / 2) / clientHeight 22 | } 23 | } 24 | 25 | useEventListener(target, 'mousemove', updatePosition) 26 | 27 | useEventListener(target, 'mouseenter', () => { 28 | motionParallax.isActive = true 29 | }) 30 | 31 | useEventListener(target, 'mouseleave', () => { 32 | motionParallax.isActive = false 33 | }) 34 | 35 | return motionParallax 36 | } 37 | -------------------------------------------------------------------------------- /nuxt-app/composables/useMutationObserver.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { onBeforeUnmount, onMounted, watch } from 'vue' 3 | 4 | /** 5 | * Composable for observing DOM changes of an 6 | * HTML element with a mutation observer. 7 | * 8 | * @param target The observation target. 9 | * @param listener The observation listener. 10 | * @param options The observation options. 11 | */ 12 | export function useMutationObserver( 13 | target: Ref, 14 | listener: MutationCallback, 15 | options?: MutationObserverInit | undefined 16 | ) { 17 | let mutationObserver: MutationObserver 18 | 19 | onMounted(() => { 20 | if (target.value) { 21 | mutationObserver = new MutationObserver(listener) 22 | mutationObserver.observe(target.value, options) 23 | } 24 | }) 25 | 26 | onBeforeUnmount(() => { 27 | mutationObserver?.disconnect() 28 | }) 29 | 30 | watch(target, () => { 31 | mutationObserver?.disconnect() 32 | if (target.value) { 33 | mutationObserver = new MutationObserver(listener) 34 | mutationObserver.observe(target.value, options) 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /nuxt-app/composables/useNavigator.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, ref } from 'vue' 2 | 3 | /** 4 | * Composable for secure access of the navigator object. 5 | * 6 | * @returns A reference to the navigator object. 7 | */ 8 | export function useNavigator() { 9 | const _navigator = ref() 10 | 11 | onMounted(() => { 12 | _navigator.value = navigator 13 | }) 14 | 15 | return _navigator 16 | } 17 | -------------------------------------------------------------------------------- /nuxt-app/composables/usePageMeta.ts: -------------------------------------------------------------------------------- 1 | import { useHead, useRoute } from '#app' 2 | import type { Ref } from 'vue' 3 | import { getMetaInfo } from '../helpers' 4 | import type { FileItem } from '../types' 5 | 6 | interface PageMeta { 7 | meta_title: string 8 | meta_description: string 9 | cover_image?: FileItem 10 | } 11 | 12 | /** 13 | * Composable to set the meta data of a page. 14 | * 15 | * @param page A page from our CMS. 16 | */ 17 | export function usePageMeta(page: Ref) { 18 | const route = useRoute() 19 | useHead(() => 20 | page.value 21 | ? getMetaInfo({ 22 | type: 'website', 23 | path: route.path, 24 | title: page.value.meta_title, 25 | description: page.value.meta_description, 26 | image: page.value.cover_image, 27 | }) 28 | : {} 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /nuxt-app/composables/useProfileCreationStore.ts: -------------------------------------------------------------------------------- 1 | import type { Tag } from '~/composables/useDirectus' 2 | import { defineStore } from 'pinia' 3 | 4 | interface MainInfos { 5 | firstName: string 6 | lastName: string 7 | } 8 | 9 | interface Details { 10 | role: string 11 | company: string 12 | location: string 13 | aboutMe: string 14 | } 15 | 16 | export const useProfileCreationStore = defineStore('profileCreation', { 17 | state: (): { 18 | mainInfos: MainInfos 19 | selectedEmojis: string[] 20 | selectedInterests: Tag[] 21 | details: Details 22 | profilePicture: { 23 | file: File | null 24 | previewUrl: string | null 25 | } 26 | } => ({ 27 | mainInfos: {} as MainInfos, 28 | selectedEmojis: [], 29 | selectedInterests: [], 30 | details: {} as Details, 31 | profilePicture: { 32 | file: null, 33 | previewUrl: null, 34 | }, 35 | }), 36 | actions: { 37 | updateMainInfos(data: MainInfos) { 38 | this.mainInfos = data 39 | }, 40 | updateSelectedEmojis(emojis: string[]) { 41 | this.selectedEmojis = emojis 42 | }, 43 | updateSelectedInterests(interests: Tag[]) { 44 | this.selectedInterests = interests 45 | }, 46 | updateDetails(data: any) { 47 | this.details = data 48 | }, 49 | updateProfilePicture(file: File, previewUrl: string) { 50 | this.profilePicture.file = file 51 | this.profilePicture.previewUrl = previewUrl 52 | }, 53 | }, 54 | }) 55 | -------------------------------------------------------------------------------- /nuxt-app/composables/useScrollParallax.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { reactive } from 'vue' 3 | import { useEventListener, useWindow } from '.' 4 | 5 | /** 6 | * Composable to add parallax effects when scrolling within an element. 7 | * 8 | * @param target The target element. 9 | * 10 | * @returns The current parallax state. 11 | */ 12 | export function useScrollParallax(target: Ref) { 13 | const scrollParallax = reactive({ offset: 0, isVisible: false }) 14 | 15 | const updatePosition = () => { 16 | if (target.value) { 17 | const { innerHeight } = window 18 | const { top, bottom, height } = target.value.getBoundingClientRect() 19 | scrollParallax.offset = innerHeight / 2 - (top + height / 2) 20 | scrollParallax.isVisible = bottom > 0 && top < innerHeight 21 | } 22 | } 23 | 24 | useEventListener(useWindow(), 'scroll', updatePosition) 25 | 26 | return scrollParallax 27 | } 28 | -------------------------------------------------------------------------------- /nuxt-app/composables/useShare.ts: -------------------------------------------------------------------------------- 1 | import { reactive, ref, watch } from 'vue' 2 | import { useNavigator } from './useNavigator' 3 | 4 | /** 5 | * Composable for secure use of the share API. 6 | * 7 | * @returns A reference to the share API. 8 | */ 9 | export function useShare() { 10 | const navigator = useNavigator() 11 | const isSupported = ref(false) 12 | 13 | watch(navigator, () => { 14 | isSupported.value = !!navigator.value?.share 15 | }) 16 | 17 | const share = (data: ShareData) => { 18 | navigator.value?.share(data) 19 | } 20 | 21 | return reactive({ 22 | isSupported, 23 | share, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /nuxt-app/composables/useTagFilterNew.ts: -------------------------------------------------------------------------------- 1 | import type { DirectusTag, Tag } from '~/composables/useDirectus' 2 | import { ADD_TAG_FILTER_EVENT_ID, REMOVE_TAG_FILTER_EVENT_ID } from '~/config' 3 | import { trackGoal } from '~/helpers' 4 | import type { Ref } from 'vue' 5 | import { computed, reactive, ref, watch } from 'vue' 6 | 7 | interface LocalTag extends Tag { 8 | is_active: boolean 9 | } 10 | 11 | /** 12 | * Composable that provide the functionality of the tag filter. 13 | * 14 | * @param items The data to be filtered. Usually the result of a directus request from the composable useDirectus. 15 | * @param tags The tags to be used for filtering. 16 | * 17 | * @returns State and methods to use the tag filter. 18 | */ 19 | export function useTagFilterNew(items: Ref, tags: Ref) { 20 | const reactiveTags = ref([]) 21 | 22 | // Track analytic events 23 | watch(reactiveTags, (nextTags, prevTags) => { 24 | if (prevTags.length === 0 && nextTags.length > 0) { 25 | trackGoal(ADD_TAG_FILTER_EVENT_ID) 26 | } else if (prevTags.length > 0 && nextTags.length === 0) { 27 | trackGoal(REMOVE_TAG_FILTER_EVENT_ID) 28 | } 29 | }) 30 | 31 | // Update tags when input changes 32 | watch(items, () => { 33 | if (tags.value) { 34 | reactiveTags.value = tags.value.map((tag) => ({ ...tag, is_active: false })) 35 | } 36 | }) 37 | 38 | const toggleTag = (_: any, index: number) => { 39 | reactiveTags.value.splice(index, 1, { 40 | ...reactiveTags.value[index], 41 | is_active: !reactiveTags.value[index].is_active, 42 | }) 43 | } 44 | 45 | const output = computed(() => { 46 | const activeTags = reactiveTags.value.filter((tag) => tag.is_active) 47 | 48 | // Filter entity if at least one tag from an entity has the same name as an active tag 49 | if (activeTags.length) { 50 | return ( 51 | items.value?.filter((entityItem) => { 52 | return entityItem.tags?.some((entityTag: DirectusTag) => { 53 | return activeTags.some((tag) => tag.name === entityTag.tag?.name) 54 | }) 55 | }) || [] 56 | ) 57 | } 58 | 59 | // Otherwise return entity 60 | return items.value || [] 61 | }) 62 | 63 | return reactive({ 64 | tags: reactiveTags, 65 | toggleTag, 66 | output, 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /nuxt-app/composables/useWindow.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, ref } from 'vue' 2 | 3 | /** 4 | * Composable for secure access of the window object. 5 | * 6 | * @returns A reference to the window object. 7 | */ 8 | export function useWindow() { 9 | const _window = ref() 10 | 11 | onMounted(() => { 12 | _window.value = window 13 | }) 14 | 15 | return _window 16 | } 17 | -------------------------------------------------------------------------------- /nuxt-app/helpers/getCookie.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to get the value of a browser cookie. 3 | * 4 | * @param name The name of the cookie. 5 | * 6 | * @returns The value of the cookie. 7 | */ 8 | export function getCookie(name: string) { 9 | return document.cookie 10 | .split('; ') 11 | .find((cookie) => name === cookie.split('=')[0]) 12 | ?.split('=')[1] 13 | } 14 | -------------------------------------------------------------------------------- /nuxt-app/helpers/getHashCode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to get a hash code from a string. 3 | * 4 | * @param source The source data of the hash code. 5 | * 6 | * @returns A hash code. 7 | */ 8 | export function getHashCode(source: string | undefined) { 9 | const string = JSON.stringify(source) ?? '' 10 | let hash = 0 11 | let char: number 12 | for (let i = 0; i < string.length; i += 1) { 13 | char = string.charCodeAt(i) 14 | hash = (hash << 5) - hash + char 15 | hash |= 0 16 | } 17 | return hash 18 | } 19 | -------------------------------------------------------------------------------- /nuxt-app/helpers/getTrimmedString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A helper function that trims a string to a maximal length. 3 | * 4 | * @param string The string to be trimmed. 5 | * @param maxLength The maximal length. 6 | * 7 | * @returns A trimmed string. 8 | */ 9 | export function getTrimmedString(string: string, maxLength: number) { 10 | if (maxLength < string.length) { 11 | const trimmedString = string.slice(0, maxLength).trim() 12 | return trimmedString + (trimmedString.charAt(trimmedString.length - 1) === '.' ? '..' : '...') 13 | } 14 | return string 15 | } 16 | -------------------------------------------------------------------------------- /nuxt-app/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getCookie' 2 | export * from './getHashCode' 3 | export * from './getMetaInfo' 4 | export * from './getTrimmedString' 5 | export * from './setCookie' 6 | export * from './sleep' 7 | export * from './trackGoal' 8 | -------------------------------------------------------------------------------- /nuxt-app/helpers/prepareTranscript.ts: -------------------------------------------------------------------------------- 1 | import type { DirectusTranscriptItem } from '~/types' 2 | import { DirectusTranscriptItemServices } from '~/types' 3 | 4 | interface WordTimestamp { 5 | word: string 6 | time: number 7 | } 8 | 9 | interface SequentialParagraph { 10 | speaker: string 11 | wordlist: WordTimestamp[] 12 | } 13 | 14 | function getNameForSpeaker(speakerIdentifier: string, transcript: DirectusTranscriptItem) { 15 | const speaker = transcript.speakers?.find((speaker) => String(speaker.identifier) === String(speakerIdentifier)) 16 | return speaker ? speaker.name : '???' 17 | } 18 | 19 | function transformProgrammierbar(word: string): string { 20 | const lowerWord = word.toLowerCase() 21 | if (lowerWord.includes('programmierbar')) { 22 | return word.replace(/programmierbar/i, 'programmier.bar') 23 | } 24 | return word 25 | } 26 | 27 | function prepareTranscriptFromDeepgram(transcript: DirectusTranscriptItem): SequentialParagraph[] { 28 | let sequentialParagraphs: SequentialParagraph[] = [] 29 | let currentSpeaker = '' 30 | let currentWordList: WordTimestamp[] = [] 31 | 32 | transcript.raw_response?.results.utterances.forEach((utterance) => { 33 | utterance.words.forEach((word) => { 34 | const transformedWord = transformProgrammierbar(word.punctuated_word) 35 | 36 | if (currentSpeaker === '' || currentSpeaker === word.speaker) { 37 | currentWordList.push({ word: transformedWord, time: word.start }) 38 | currentSpeaker = word.speaker 39 | } else { 40 | sequentialParagraphs.push({ speaker: currentSpeaker, wordlist: currentWordList }) 41 | currentWordList = [] 42 | currentWordList.push({ word: transformedWord, time: word.start }) 43 | currentSpeaker = word.speaker 44 | } 45 | }) 46 | }) 47 | 48 | sequentialParagraphs.push({ speaker: currentSpeaker, wordlist: currentWordList }) 49 | 50 | sequentialParagraphs = sequentialParagraphs.map((paragraph) => { 51 | paragraph.speaker = getNameForSpeaker(paragraph.speaker, transcript) 52 | 53 | return paragraph 54 | }) 55 | 56 | return sequentialParagraphs 57 | } 58 | 59 | export function prepareTranscript(transcript: DirectusTranscriptItem): SequentialParagraph[] { 60 | switch (transcript.service) { 61 | case DirectusTranscriptItemServices.Deepgram: 62 | return prepareTranscriptFromDeepgram(transcript) 63 | default: 64 | throw new Error(`Unknown transcript service "${transcript.service}". Cannot prepare transcript.`) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /nuxt-app/helpers/setCookie.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to set a new browser cookie. 3 | * 4 | * @param name The name of the cookie. 5 | * @param value The value of the cookie. 6 | * @param days The lifetime of the cookie in days. 7 | */ 8 | export function setCookie(name: string, value: string, days: number) { 9 | const maxAge = 60 * 60 * 24 * days 10 | const domain = window.location.hostname 11 | document.cookie = `${name}=${value}; max-age=${maxAge}; domain=${domain}; path=/; samesite=strict; secure` 12 | } 13 | -------------------------------------------------------------------------------- /nuxt-app/helpers/sleep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * It stops the execution of the code for a certain time. 3 | * 4 | * @param time The time to wait. 5 | * 6 | * @returns A promise object. 7 | */ 8 | export function sleep(time: number) { 9 | return new Promise((resolve) => setTimeout(resolve, time)) 10 | } 11 | -------------------------------------------------------------------------------- /nuxt-app/helpers/trackGoal.ts: -------------------------------------------------------------------------------- 1 | import * as fathom from 'fathom-client'; 2 | 3 | /** 4 | * A helper function that tracks goals with Fathom Analytics. 5 | * 6 | * @param eventId The ID of the event. 7 | * @param value The value of the event. 8 | */ 9 | export function trackGoal(eventId: string, value?: number): void { 10 | fathom.trackEvent(eventId, { 11 | _value: value || 0 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /nuxt-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare", 11 | "eslint": "eslint --fix .", 12 | "prettier": "prettier . --write" 13 | }, 14 | "dependencies": { 15 | "@directus/sdk": "^17.0.0", 16 | "@nuxtjs/tailwindcss": "^6.11.4", 17 | "@pinia/nuxt": "^0.5.1", 18 | "@types/google.maps": "^3.55.3", 19 | "@types/smoothscroll-polyfill": "^0.3.4", 20 | "@ubclaunchpad/vue-fathom": "^2.0.0", 21 | "@vue/compiler-sfc": "^3.4.19", 22 | "core-js": "^3.36.0", 23 | "h3-zod": "^0.5.3", 24 | "isomorphic-dompurify": "^2.12.0", 25 | "nodemailer": "^6.9.9", 26 | "nuxt-jsonld": "^2.0.8", 27 | "pinia": "^2.1.7", 28 | "smoothscroll-polyfill": "^0.4.4", 29 | "vite-svg-loader": "^5.1.0", 30 | "zod": "^3.22.4" 31 | }, 32 | "devDependencies": { 33 | "@ianvs/prettier-plugin-sort-imports": "^4.1.1", 34 | "@nuxt/eslint-config": "^0.2.0", 35 | "@nuxt/image-edge": "^1.3.0-28468005.8ad772e", 36 | "@nuxtjs/algolia": "^1.10.2", 37 | "@types/nodemailer": "^6.4.14", 38 | "eslint": "^8.56.0", 39 | "eslint-config-prettier": "^9.1.0", 40 | "eslint-plugin-nuxt": "^4.0.0", 41 | "eslint-plugin-vue": "^9.21.1", 42 | "nuxt": "^3.15.2", 43 | "postcss": "^8.4.35", 44 | "prettier": "^3.2.5", 45 | "prettier-plugin-tailwindcss": "^0.5.11" 46 | }, 47 | "overrides": { 48 | "vue": "latest" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /nuxt-app/pages/aufnahmen.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 47 | -------------------------------------------------------------------------------- /nuxt-app/pages/datenschutz.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 44 | -------------------------------------------------------------------------------- /nuxt-app/pages/gewinnspiel.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 49 | -------------------------------------------------------------------------------- /nuxt-app/pages/impressum.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 44 | -------------------------------------------------------------------------------- /nuxt-app/pages/konferenzen/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 64 | -------------------------------------------------------------------------------- /nuxt-app/pages/kontakt.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 65 | -------------------------------------------------------------------------------- /nuxt-app/pages/login-callback.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 60 | -------------------------------------------------------------------------------- /nuxt-app/pages/profiles/[slug].vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 82 | -------------------------------------------------------------------------------- /nuxt-app/pages/verhaltensregeln.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 49 | -------------------------------------------------------------------------------- /nuxt-app/plugins/fathom.client.ts: -------------------------------------------------------------------------------- 1 | import VueFathom from '@ubclaunchpad/vue-fathom' 2 | import { defineNuxtPlugin } from '#app' 3 | 4 | export default defineNuxtPlugin((nuxtApp) => { 5 | nuxtApp.vueApp.use(VueFathom, { 6 | siteID: 'XSJTTACD', 7 | 8 | settings: { 9 | url: 'https://ziggy-stardust-six.programmier.bar/script.js', 10 | spa: 'history', 11 | honorDNT: false, 12 | canonical: true, 13 | }, 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /nuxt-app/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /nuxt-app/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /nuxt-app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /nuxt-app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/public/favicon-16x16.png -------------------------------------------------------------------------------- /nuxt-app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/public/favicon-32x32.png -------------------------------------------------------------------------------- /nuxt-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/public/favicon.ico -------------------------------------------------------------------------------- /nuxt-app/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programmierbar/website/0ccb0f25524c83980537f4745067b740e0ee55a5/nuxt-app/public/icon.png -------------------------------------------------------------------------------- /nuxt-app/public/sw.js: -------------------------------------------------------------------------------- 1 | // THIS FILE SHOULD NOT BE VERSION CONTROLLED 2 | 3 | // https://github.com/NekR/self-destroying-sw 4 | 5 | self.addEventListener('install', function (e) { 6 | self.skipWaiting() 7 | }) 8 | 9 | self.addEventListener('activate', function (e) { 10 | self.registration 11 | .unregister() 12 | .then(function () { 13 | return self.clients.matchAll() 14 | }) 15 | .then(function (clients) { 16 | clients.forEach((client) => client.navigate(client.url)) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /nuxt-app/server/api/cocktails.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event } from 'h3' 2 | 3 | export default function (event: H3Event) { 4 | const data = { 5 | cocktails: [ 6 | { 7 | name: 'Gin Tonic', 8 | ingredients: ['gin', 'tonic water', 'ice'], 9 | contains_alcohol: true, 10 | alcohol_free_variant_available: true, 11 | }, 12 | { 13 | name: 'Ipanema', 14 | ingredients: ['ginger ale', 'juice', 'lime', 'brown sugar', 'ice'], 15 | contains_alcohol: false, 16 | }, 17 | { 18 | name: 'Crodino Spritz', 19 | ingredients: ['crodino', 'orange', 'mint', 'ice'], 20 | contains_alcohol: false, 21 | }, 22 | ], 23 | } 24 | 25 | event.node.res.setHeader('Content-Type', 'application/json') 26 | event.node.res.end(JSON.stringify(data)) 27 | } 28 | -------------------------------------------------------------------------------- /nuxt-app/server/middleware/discord.ts: -------------------------------------------------------------------------------- 1 | import { DISCORD_INVITE_LINK } from '~/config'; 2 | 3 | export default eventHandler(function(event) { 4 | const hostToMatch = 'discord.programmier.bar'; 5 | const pathToMatch = '/discord'; 6 | const requestHost = event.headers.get('host') || ''; 7 | const requestPath = event.path; 8 | 9 | console.log(requestPath); 10 | 11 | if (!requestHost.startsWith(hostToMatch) && !(requestPath === pathToMatch)) { 12 | return; 13 | } 14 | 15 | const redirectUrl = `${DISCORD_INVITE_LINK}`; 16 | 17 | // Set the response status and location header for redirection 18 | // And end the response to complete the redirection 19 | event.node.res.writeHead(302, { 20 | Location: redirectUrl, 21 | }); 22 | 23 | event.node.res.end(); 24 | }); 25 | -------------------------------------------------------------------------------- /nuxt-app/server/middleware/flutterday.ts: -------------------------------------------------------------------------------- 1 | export default eventHandler(function(event) { 2 | const toMatch = 'flutterday.programmier.bar'; 3 | const requestHost = event.headers.get('host') || ''; 4 | 5 | if (!requestHost.startsWith(toMatch)) { 6 | return; 7 | } 8 | 9 | const host = 'https://programmier.bar'; 10 | const path = '/meetup/flutter-day-2024'; 11 | 12 | const redirectUrl = `${host}${path}`; 13 | 14 | // Set the response status and location header for redirection 15 | // And end the response to complete the redirection 16 | event.node.res.writeHead(302, { 17 | Location: redirectUrl, 18 | }); 19 | 20 | event.node.res.end(); 21 | }); 22 | -------------------------------------------------------------------------------- /nuxt-app/server/middleware/wellKnownWebfinger.ts: -------------------------------------------------------------------------------- 1 | export default eventHandler(function (context) { 2 | const incomingPath = context.path 3 | const toMatch = '/.well-known/webfinger' 4 | 5 | if (!incomingPath.startsWith(toMatch)) { 6 | return 7 | } 8 | 9 | const externalHost = 'https://social.programmier.bar' 10 | const redirectUrl = `${externalHost}${incomingPath}` 11 | 12 | // Set the response status and location header for redirection 13 | // And end the response to complete the redirection 14 | context.node.res.writeHead(302, { 15 | Location: redirectUrl, 16 | }) 17 | context.node.res.end() 18 | }) 19 | -------------------------------------------------------------------------------- /nuxt-app/server/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sendEmail' 2 | export * from './schema' 3 | -------------------------------------------------------------------------------- /nuxt-app/server/utils/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const EmailSchema = z.object({ 4 | name: z 5 | .string() 6 | .min(1, 'Bitte trage deinen Namen ein.') 7 | .max(50, 'Dein Name darf nicht länger als 50 Zeichen lang sein.'), 8 | email: z 9 | .string() 10 | .email('Deine E-Mail-Adresse scheint ungültig zu sein.') 11 | .max(50, 'Deine E-Mail-Adresse darf nicht länger als 50 Zeichen lang sein.'), 12 | message: z 13 | .string() 14 | .min(1, 'Bitte trage deine Nachricht ein.') 15 | .max(5000, 'Dein Nachricht darf nicht länger als 5.000 Zeichen lang sein.'), 16 | }) 17 | -------------------------------------------------------------------------------- /nuxt-app/server/utils/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer' 2 | 3 | // Create nodemailer transporter 4 | const transporter = nodemailer.createTransport({ 5 | host: 'smtp.gmail.com', 6 | port: 465, 7 | secure: true, 8 | auth: { 9 | user: 'noreply@programmier.bar', 10 | pass: useRuntimeConfig().emailPassword, 11 | }, 12 | }) 13 | 14 | type EmailData = { 15 | to: string 16 | subject: string 17 | html: string 18 | } 19 | 20 | /** 21 | * Helper function that sends an email via nodemailer. 22 | * 23 | * @param emailData The email data. 24 | */ 25 | export async function sendEmail(emailData: EmailData): Promise { 26 | return new Promise((resolve, reject) => { 27 | transporter.sendMail( 28 | { 29 | ...emailData, 30 | from: 'programmier.bar ', 31 | }, 32 | (error) => { 33 | if (error) { 34 | reject(error) 35 | } 36 | resolve() 37 | } 38 | ) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /nuxt-app/services/directus.ts: -------------------------------------------------------------------------------- 1 | import { authentication, createDirectus, rest } from '@directus/sdk' 2 | import { DIRECTUS_CMS_URL } from '../config' 3 | import type { 4 | DirectusAboutPage, 5 | DirectusCocPage, 6 | DirectusConferencePage, 7 | DirectusContactPage, 8 | DirectusHallOfFamePage, 9 | DirectusHomePage, 10 | DirectusImprintPage, 11 | DirectusLoginPage, 12 | DirectusMeetupItem, 13 | DirectusConferenceItem, 14 | DirectusMeetupPage, 15 | DirectusMemberItem, 16 | DirectusPickOfTheDayItem, 17 | DirectusPickOfTheDayPage, 18 | DirectusPodcastItem, 19 | DirectusPodcastPage, 20 | DirectusPrivacyPage, 21 | DirectusProfileCreationPage, 22 | DirectusRafflePage, 23 | DirectusRecordingsPage, 24 | DirectusSpeakerItem, 25 | DirectusTagItem, 26 | DirectusProfileItem, 27 | DirectusTranscriptItem 28 | } from '../types' 29 | 30 | export type Collections = { 31 | home_page: DirectusHomePage 32 | podcast_page: DirectusPodcastPage 33 | meetup_page: DirectusMeetupPage 34 | conference_page: DirectusConferencePage 35 | hall_of_fame_page: DirectusHallOfFamePage 36 | pick_of_the_day_page: DirectusPickOfTheDayPage 37 | about_page: DirectusAboutPage 38 | contact_page: DirectusContactPage 39 | imprint_page: DirectusImprintPage 40 | privacy_page: DirectusPrivacyPage 41 | raffle_page: DirectusRafflePage 42 | login_page: DirectusLoginPage 43 | profile_creation_page: DirectusProfileCreationPage 44 | coc_page: DirectusCocPage 45 | recordings_page: DirectusRecordingsPage 46 | podcasts: DirectusPodcastItem[] 47 | meetups: DirectusMeetupItem[] 48 | conferences: DirectusConferenceItem[] 49 | members: DirectusMemberItem[] 50 | speakers: DirectusSpeakerItem[] 51 | picks_of_the_day: DirectusPickOfTheDayItem[] 52 | profiles: DirectusProfileItem[], 53 | tags: DirectusTagItem[] 54 | transcripts: DirectusTranscriptItem[] 55 | } 56 | 57 | export const directus = createDirectus(DIRECTUS_CMS_URL) 58 | .with(authentication('session', { credentials: 'include' })) 59 | .with(rest()) 60 | -------------------------------------------------------------------------------- /nuxt-app/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './directus' 2 | -------------------------------------------------------------------------------- /nuxt-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { 5 | "allowArbitraryExtensions": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /nuxt-app/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './directus' 2 | export * from './items' 3 | export * from './search' 4 | -------------------------------------------------------------------------------- /nuxt-app/types/search.ts: -------------------------------------------------------------------------------- 1 | import type { MeetupItem, PickOfTheDayItem, PodcastItem, SpeakerItem } from './items' 2 | 3 | export interface PodcastSearchItem 4 | extends Pick< 5 | PodcastItem, 6 | 'id' | 'slug' | 'published_on' | 'type' | 'number' | 'title' | 'description' | 'cover_image' | 'tags' 7 | > { 8 | item_type: 'podcast' 9 | } 10 | 11 | export interface MeetupSearchItem 12 | extends Pick { 13 | item_type: 'meetup' 14 | } 15 | 16 | export interface PickOfTheDaySearchItem 17 | extends Pick { 18 | item_type: 'pick_of_the_day' 19 | } 20 | 21 | export interface SpeakerSearchItem 22 | extends Pick< 23 | SpeakerItem, 24 | | 'id' 25 | | 'slug' 26 | | 'published_on' 27 | | 'academic_title' 28 | | 'first_name' 29 | | 'last_name' 30 | | 'description' 31 | | 'profile_image' 32 | | 'tags' 33 | > { 34 | item_type: 'speaker' 35 | } 36 | 37 | export type SearchItem = PodcastSearchItem | MeetupSearchItem | PickOfTheDaySearchItem | SpeakerSearchItem 38 | -------------------------------------------------------------------------------- /nuxt-app/types/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /shared-code/helpers/getFullPodcastTitle.ts: -------------------------------------------------------------------------------- 1 | import { getPodcastTypeAndNumber } from './getPodcastTypeAndNumber'; 2 | import { getPodcastTitleDivider } from './getPodcastTitleDivider'; 3 | 4 | interface Podcast { 5 | type: string; 6 | number: string; 7 | title: string; 8 | } 9 | 10 | /** 11 | * A helper function that returns the full title of a podcast episode. 12 | * 13 | * @param podcast A podcast object. 14 | * 15 | * @returns The full podcast episode title. 16 | */ 17 | export function getFullPodcastTitle(podcast: Podcast) { 18 | return ( 19 | getPodcastTypeAndNumber(podcast) + 20 | getPodcastTitleDivider(podcast) + 21 | podcast.title 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /shared-code/helpers/getFullSpeakerName.ts: -------------------------------------------------------------------------------- 1 | interface Speaker { 2 | academic_title?: string | null; 3 | first_name: string; 4 | last_name: string; 5 | } 6 | 7 | /** 8 | * A helper function that returns the full name of a speaker. 9 | * 10 | * @param podcast A speaker object. 11 | * 12 | * @returns The full name of the speaker. 13 | */ 14 | export function getFullSpeakerName(speaker: Speaker) { 15 | return ( 16 | (speaker.academic_title || '') + 17 | ' ' + 18 | speaker.first_name + 19 | ' ' + 20 | speaker.last_name 21 | ).trim(); 22 | } 23 | -------------------------------------------------------------------------------- /shared-code/helpers/getPodcastTitleDivider.ts: -------------------------------------------------------------------------------- 1 | interface Podcast { 2 | type: string; 3 | } 4 | 5 | /** 6 | * A helper function that returns the title divider based on the podcast type. 7 | * 8 | * @param podcast A podcast object. 9 | * 10 | * @returns The podcast title divider. 11 | */ 12 | export function getPodcastTitleDivider(podcast: Podcast) { 13 | switch (podcast.type) { 14 | case 'deep_dive': 15 | return ' – '; 16 | case 'cto_special': 17 | case 'news': 18 | default: 19 | return ': '; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /shared-code/helpers/getPodcastType.ts: -------------------------------------------------------------------------------- 1 | interface Podcast { 2 | type: string; 3 | } 4 | 5 | /** 6 | * A helper function that converts the podcast type to a readable string. 7 | * 8 | * @param podcast A podcast object. 9 | * 10 | * @returns The podcast type. 11 | */ 12 | export function getPodcastType(podcast: Podcast) { 13 | switch (podcast.type) { 14 | case 'deep_dive': 15 | return 'Deep Dive'; 16 | case 'cto_special': 17 | return 'CTO-Special'; 18 | case 'news': 19 | return 'News'; 20 | default: 21 | return 'Spezialfolge'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /shared-code/helpers/getPodcastTypeAndNumber.ts: -------------------------------------------------------------------------------- 1 | import { getPodcastType } from './getPodcastType'; 2 | 3 | interface Podcast { 4 | type: string; 5 | number: string; 6 | } 7 | 8 | /** 9 | * A helper function that returns the type and number 10 | * of a podcast episode in a readable string. 11 | * 12 | * @param podcast A podcast object. 13 | * 14 | * @returns The podcast type and number. 15 | */ 16 | export function getPodcastTypeAndNumber(podcast: Podcast) { 17 | return getPodcastType(podcast) + ' ' + podcast.number; 18 | } 19 | -------------------------------------------------------------------------------- /shared-code/helpers/getUrlSlug.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to convert a text into a URL slug. 3 | * 4 | * @param text The text to be converted. 5 | * 6 | * @returns A URL slug. 7 | */ 8 | export function getUrlSlug(text: string) { 9 | return ( 10 | text 11 | // Convert to lower case 12 | .toLowerCase() 13 | 14 | // Convert special german characters 15 | .replace(/ä/g, 'ae') 16 | .replace(/ö/g, 'oe') 17 | .replace(/ü/g, 'ue') 18 | .replace(/ß/g, 'ss') 19 | 20 | // Remove accents 21 | .normalize('NFD') 22 | .replace(/[\u0300-\u036F]/g, '') 23 | 24 | // Remove invalid characters 25 | .replace(/(^[^a-z0-9]+|[^a-z0-9/.\- ]+|[^a-z0-9]+$)/g, '') 26 | 27 | // Replace certain characters with a hyphen 28 | .replace(/( |\/|\.|-)+/g, '-') 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /shared-code/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getFullPodcastTitle'; 2 | export * from './getFullSpeakerName'; 3 | export * from './getPodcastTitleDivider'; 4 | export * from './getPodcastType'; 5 | export * from './getPodcastTypeAndNumber'; 6 | export * from './getUrlSlug'; 7 | -------------------------------------------------------------------------------- /shared-code/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers'; 2 | -------------------------------------------------------------------------------- /website.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [{ "path": "." }], 3 | "settings": { 4 | "editor.formatOnSave": true, 5 | "eslint.workingDirectories": [ 6 | "./cloud-functions", 7 | "./directus-cms", 8 | "./nuxt-app", 9 | "./shared-code" 10 | ] 11 | } 12 | } 13 | --------------------------------------------------------------------------------