├── dev ├── .gitignore ├── vault │ ├── bin │ │ ├── seal.sh │ │ ├── read.sh │ │ ├── unseal.sh │ │ └── copy.sh │ ├── .gitignore │ ├── Dockerfile │ └── vault.config.json └── workspace │ ├── .gitignore │ ├── env.list.dist │ ├── .zshrc │ └── Dockerfile ├── app ├── .node-version ├── functions │ ├── .node-version │ ├── http │ │ ├── index.js │ │ ├── image.onRequest.spec.js │ │ └── image.onRequest.js │ ├── firestore │ │ ├── index.js │ │ ├── algolia.onWrite.js │ │ └── algolia.onWrite.spec.js │ ├── storage │ │ ├── index.js │ │ ├── uploads.onChange.spec.js │ │ └── uploads.onChange.js │ ├── auth │ │ ├── index.js │ │ ├── customClaim.onCreate.js │ │ └── customClaim.onCreate.spec.js │ ├── utils │ │ ├── collections.util.js │ │ ├── index.js │ │ ├── admin.util.js │ │ ├── algolia.util.js │ │ ├── environment.util.spec.js │ │ └── environment.util.js │ ├── config.json.dist │ ├── index.js │ └── package.json ├── src │ ├── utils │ │ ├── index.js │ │ └── batch.util.js │ ├── favicon.ico │ ├── assets │ │ ├── icon.png │ │ ├── favicon.ico │ │ ├── logo-built_white.png │ │ ├── svg │ │ │ ├── add.svg │ │ │ ├── menu.svg │ │ │ ├── skip-next.svg │ │ │ ├── view-carousel.svg │ │ │ ├── delete.svg │ │ │ ├── clear.svg │ │ │ ├── open-with.svg │ │ │ ├── check-circle.svg │ │ │ ├── content-copy.svg │ │ │ ├── search.svg │ │ │ ├── share.svg │ │ │ ├── three-dots.svg │ │ │ └── spinner.svg │ │ └── algolia.svg │ ├── observers │ │ ├── index.js │ │ ├── images.observer.js │ │ └── search.observer.js │ ├── datastore │ │ ├── actions │ │ │ ├── setImages.action.js │ │ │ ├── setSearch.action.js │ │ │ ├── clearSelection.action.js │ │ │ ├── toggleMenu.action.js │ │ │ ├── setImagesWidth.action.js │ │ │ ├── setToken.action.js │ │ │ ├── setAllImagesLoaded.action.js │ │ │ ├── setPath.action.js │ │ │ ├── setImagesAllLoaded.action.js │ │ │ ├── setSelecting.action.js │ │ │ ├── setCurrentUser.action.js │ │ │ ├── setImage.action.js │ │ │ ├── setSearchResults.action.js │ │ │ ├── addImage.action.js │ │ │ ├── removeSelection.action.js │ │ │ ├── addSelection.action.js │ │ │ ├── addImages.action.js │ │ │ ├── setControlSelect.action.js │ │ │ ├── setShiftSelect.action.js │ │ │ ├── setSearching.action.js │ │ │ ├── updateImage.action.js │ │ │ ├── addImageVersion.action.js │ │ │ ├── index.js │ │ │ └── deleteSelection.action.js │ │ └── index.js │ ├── components │ │ ├── views │ │ │ ├── index.js │ │ │ ├── tags.scss │ │ │ ├── tags.view.scss │ │ │ ├── embed.view.scss │ │ │ ├── images.view.scss │ │ │ ├── galleries.view.scss │ │ │ ├── tags.view.js │ │ │ ├── embed.view.js │ │ │ ├── galleries.view.js │ │ │ ├── images.view.js │ │ │ ├── gallery.view.scss │ │ │ └── gallery.view.js │ │ ├── drawer │ │ │ ├── drawer.scss │ │ │ └── drawer.component.js │ │ ├── nav │ │ │ ├── nav.scss │ │ │ └── nav.component.js │ │ ├── guard │ │ │ └── guard.component.js │ │ ├── search │ │ │ ├── search.scss │ │ │ └── search.component.js │ │ ├── tags │ │ │ ├── tags.scss │ │ │ └── tags.component.js │ │ ├── image-detail │ │ │ ├── imageDetail.scss │ │ │ └── imageDetail.component.js │ │ └── images │ │ │ ├── images.scss │ │ │ └── images.component.js │ ├── queries │ │ ├── index.js │ │ ├── deleteImage.query.js │ │ ├── image.query.js │ │ ├── updateImage.query.js │ │ ├── imagesByTag.query.js │ │ ├── updateTags.query.js │ │ ├── images.query.js │ │ └── imageVersion.query.js │ ├── manifest.json │ ├── style.css │ ├── environment.js.dist │ └── index.js ├── data │ ├── jpg.jpg │ ├── image-book.lnk │ ├── test-MjQyNjY5M2NiNTYxM2M4MTkwZmY0YjNmZDdjM2E3NzI=.json │ └── sample-storage-event.json ├── favicon.ico ├── assets │ ├── icon.png │ └── favicon.ico ├── tsconfig.json ├── database.rules.bolt ├── .gitignore ├── storage.rules ├── firestore.indexes.json ├── firestore.rules ├── firebase.json ├── preact.config.js ├── LICENSE ├── package.json ├── template.html └── README.md ├── .gitattributes ├── bin ├── vault │ ├── encode-base-64.sh │ ├── decode-base-64.sh │ ├── interactive-vault.sh │ ├── run-vault.sh │ ├── copy-vault-keys.ps1 │ ├── copy-vault-keys.sh │ └── expand-secrets.js ├── cloudbuild-submit.ps1 ├── watch.ps1 └── docker-login.ps1 ├── .dockerignore ├── .vscode └── settings.json ├── Dockerfile ├── .devcontainer.json ├── cloudbuild.yaml ├── functions └── utils │ └── environment.util.spec.js ├── docker-compose.yaml ├── package.json ├── preact.config.js └── README.md /dev/.gitignore: -------------------------------------------------------------------------------- 1 | env.list -------------------------------------------------------------------------------- /app/.node-version: -------------------------------------------------------------------------------- 1 | lts 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | core.autocrlf false -------------------------------------------------------------------------------- /app/functions/.node-version: -------------------------------------------------------------------------------- 1 | 8.15.1 2 | -------------------------------------------------------------------------------- /bin/vault/encode-base-64.sh: -------------------------------------------------------------------------------- 1 | cat $1 | base64 -------------------------------------------------------------------------------- /dev/vault/bin/seal.sh: -------------------------------------------------------------------------------- 1 | vault operator seal -------------------------------------------------------------------------------- /dev/workspace/.gitignore: -------------------------------------------------------------------------------- 1 | service-account.json -------------------------------------------------------------------------------- /bin/vault/decode-base-64.sh: -------------------------------------------------------------------------------- 1 | echo $1 | base64 --decode -------------------------------------------------------------------------------- /dev/workspace/env.list.dist: -------------------------------------------------------------------------------- 1 | FIREBASE_TOKEN=ABCDEFG -------------------------------------------------------------------------------- /app/src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './batch.util'; 2 | -------------------------------------------------------------------------------- /dev/vault/.gitignore: -------------------------------------------------------------------------------- 1 | env.list 2 | service-account.json 3 | -------------------------------------------------------------------------------- /dev/vault/bin/read.sh: -------------------------------------------------------------------------------- 1 | vault read $VAULT_PATH -format="json" -------------------------------------------------------------------------------- /dev/vault/bin/unseal.sh: -------------------------------------------------------------------------------- 1 | vault operator unseal $VAULT_KEY_1 -------------------------------------------------------------------------------- /bin/vault/interactive-vault.sh: -------------------------------------------------------------------------------- 1 | docker-compose run --service-ports vault sh -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | bin 3 | dev 4 | 5 | app/dist 6 | app/node_modules -------------------------------------------------------------------------------- /app/data/jpg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/fogo/HEAD/app/data/jpg.jpg -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/fogo/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /dev/vault/bin/copy.sh: -------------------------------------------------------------------------------- 1 | vault read $VAULT_PATH -format="json" > /app/vault/secrets.json -------------------------------------------------------------------------------- /app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/fogo/HEAD/app/assets/icon.png -------------------------------------------------------------------------------- /app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/fogo/HEAD/app/src/favicon.ico -------------------------------------------------------------------------------- /app/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/fogo/HEAD/app/assets/favicon.ico -------------------------------------------------------------------------------- /app/data/image-book.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/fogo/HEAD/app/data/image-book.lnk -------------------------------------------------------------------------------- /app/src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/fogo/HEAD/app/src/assets/icon.png -------------------------------------------------------------------------------- /bin/cloudbuild-submit.ps1: -------------------------------------------------------------------------------- 1 | gcloud builds submit --tag gcr.io/fir-consulting-2019/firebase-consulting . -------------------------------------------------------------------------------- /app/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/fogo/HEAD/app/src/assets/favicon.ico -------------------------------------------------------------------------------- /app/src/observers/index.js: -------------------------------------------------------------------------------- 1 | export * from './images.observer'; 2 | export * from './search.observer'; 3 | -------------------------------------------------------------------------------- /bin/vault/run-vault.sh: -------------------------------------------------------------------------------- 1 | docker-compose run --service-ports vault server -config /dev/vault/vault.config.json -------------------------------------------------------------------------------- /app/functions/http/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | imageOnRequest: require('./image.onRequest'), 3 | }; 4 | -------------------------------------------------------------------------------- /app/functions/firestore/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | algoliaOnWrite: require('./algolia.onWrite'), 3 | }; 4 | -------------------------------------------------------------------------------- /app/functions/storage/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | uploadsOnChange: require('./uploads.onChange'), 3 | }; 4 | -------------------------------------------------------------------------------- /app/functions/auth/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | customClaimOnCreate: require('./customClaim.onCreate'), 3 | }; 4 | -------------------------------------------------------------------------------- /app/src/assets/logo-built_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/how-to-firebase/fogo/HEAD/app/src/assets/logo-built_white.png -------------------------------------------------------------------------------- /app/src/datastore/actions/setImages.action.js: -------------------------------------------------------------------------------- 1 | export function setImages(state, images) { 2 | return { images }; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setSearch.action.js: -------------------------------------------------------------------------------- 1 | export function setSearch(state, search = '') { 2 | return { search }; 3 | } 4 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "allowJs": true 5 | } 6 | } -------------------------------------------------------------------------------- /app/src/datastore/actions/clearSelection.action.js: -------------------------------------------------------------------------------- 1 | export function clearSelection() { 2 | return { selection: new Set() }; 3 | } 4 | -------------------------------------------------------------------------------- /bin/watch.ps1: -------------------------------------------------------------------------------- 1 | invoke-expression 'cmd /c start powershell -windowstyle minimized -Command { docker-volume-watcher -e "*node_modules*" }' -------------------------------------------------------------------------------- /app/src/datastore/actions/toggleMenu.action.js: -------------------------------------------------------------------------------- 1 | export function toggleMenu({ showMenu }) { 2 | return { showMenu: !showMenu }; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setImagesWidth.action.js: -------------------------------------------------------------------------------- 1 | export function setImagesWidth(state, imagesWidth) { 2 | return { imagesWidth }; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setToken.action.js: -------------------------------------------------------------------------------- 1 | export function setToken(state, token) { 2 | return { token, isAdmin: token.admin }; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setAllImagesLoaded.action.js: -------------------------------------------------------------------------------- 1 | export function setImages(state, imagesAllLoaded) { 2 | return { imagesAllLoaded }; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setPath.action.js: -------------------------------------------------------------------------------- 1 | export function setPath({ path: laggedPath, showNav }, path) { 2 | return { laggedPath, path }; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setImagesAllLoaded.action.js: -------------------------------------------------------------------------------- 1 | export function setImagesAllLoaded(state, imagesAllLoaded) { 2 | return { imagesAllLoaded }; 3 | } 4 | -------------------------------------------------------------------------------- /app/database.rules.bolt: -------------------------------------------------------------------------------- 1 | path /notifications/{uid} { 2 | read() { isCurrentUser(uid) } 3 | } 4 | 5 | isCurrentUser(uid) { 6 | auth != null && auth.uid == uid 7 | } 8 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setSelecting.action.js: -------------------------------------------------------------------------------- 1 | export function setSelecting({ searching }, selecting) { 2 | return { selecting: !!selecting || searching }; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setCurrentUser.action.js: -------------------------------------------------------------------------------- 1 | export function setCurrentUser({ currentUser: laggedCurrentUser }, currentUser) { 2 | return { laggedCurrentUser, currentUser }; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setImage.action.js: -------------------------------------------------------------------------------- 1 | export function setImage(state, { ...image }) { 2 | if (!Object.keys(image).length) { 3 | image = null; 4 | } 5 | return { image }; 6 | } 7 | -------------------------------------------------------------------------------- /bin/docker-login.ps1: -------------------------------------------------------------------------------- 1 | # See https://cloud.google.com/container-registry/docs/advanced-authentication#gcloud_docker 2 | # gcloud components install docker-credential-gcr 3 | docker-credential-gcr configure-docker -------------------------------------------------------------------------------- /app/src/components/views/index.js: -------------------------------------------------------------------------------- 1 | export * from './embed.view'; 2 | export * from './galleries.view'; 3 | export * from './gallery.view'; 4 | export * from './images.view'; 5 | export * from './tags.view'; 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#122F4E", 4 | "titleBar.activeBackground": "#19416E", 5 | "titleBar.activeForeground": "#F7FAFD" 6 | } 7 | } -------------------------------------------------------------------------------- /app/functions/utils/collections.util.js: -------------------------------------------------------------------------------- 1 | module.exports = environment => { 2 | return new Map(Object.keys(environment.collections).map(key => [ 3 | key, 4 | environment.collections[key], 5 | ])); 6 | }; 7 | -------------------------------------------------------------------------------- /dev/vault/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM vault 2 | 3 | ENV GOOGLE_APPLICATION_CREDENTIALS="/dev/vault/service-account.json" 4 | ENV VAULT_ADDR='http://0.0.0.0:8200' 5 | 6 | CMD ["vault", "server", "-config", "/dev/vault/vault.config.json"] -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | /*.log 4 | 5 | functions/tmp/* 6 | 7 | service-account.json 8 | config.json 9 | environment.js 10 | 11 | .vscode 12 | 13 | *.log 14 | bin 15 | 16 | .firebaserc 17 | .firebase 18 | -------------------------------------------------------------------------------- /app/src/assets/svg/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setSearchResults.action.js: -------------------------------------------------------------------------------- 1 | export function setSearchResults({ selecting }, searchResults) { 2 | if (searchResults) { 3 | selecting = true; 4 | } 5 | return { searchResults, selecting }; 6 | } 7 | -------------------------------------------------------------------------------- /app/src/components/views/tags.scss: -------------------------------------------------------------------------------- 1 | .tagsView { 2 | min-height: calc(100vh - 5rem); 3 | } 4 | 5 | .header { 6 | font-size: 1.5rem; 7 | font-style: italic; 8 | font-weight: 800; 9 | text-align: left; 10 | } 11 | -------------------------------------------------------------------------------- /bin/vault/copy-vault-keys.ps1: -------------------------------------------------------------------------------- 1 | docker-compose exec vault sh /dev/vault/bin/unseal.sh 2 | docker-compose exec vault sh /dev/vault/bin/copy.sh 3 | docker-compose exec vault sh /dev/vault/bin/seal.sh 4 | 5 | node "$PWD\bin\vault\expand-secrets.js" -------------------------------------------------------------------------------- /app/src/assets/svg/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/assets/svg/skip-next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:10 2 | 3 | WORKDIR /app 4 | 5 | COPY ./app/package.json package.json 6 | COPY ./app/yarn.lock yarn.lock 7 | RUN yarn --pure-lockfile 8 | 9 | ADD ./app /app 10 | 11 | RUN yarn && yarn build 12 | -------------------------------------------------------------------------------- /.devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/vscode-remote/devcontainer.json for format details. 2 | { 3 | "dockerComposeFile": "docker-compose.yaml", 4 | "service": "workspace", 5 | "workspaceFolder": "/app", 6 | "extensions": ["esbenp.prettier-vscode"] 7 | } -------------------------------------------------------------------------------- /app/functions/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | adminUtil: require('./admin.util'), 3 | algoliaUtil: require('./algolia.util'), 4 | collectionsUtil: require('./collections.util'), 5 | environmentUtil: require('./environment.util'), 6 | }; 7 | -------------------------------------------------------------------------------- /app/src/assets/svg/view-carousel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/addImage.action.js: -------------------------------------------------------------------------------- 1 | export function addImage({ images, tags }, image) { 2 | images = [image, ...images]; 3 | if (image.tags) { 4 | image.tags.forEach(tag => tags.add(tag)); 5 | } 6 | 7 | return { images, tags }; 8 | } 9 | -------------------------------------------------------------------------------- /app/functions/utils/admin.util.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | let singleton; 3 | 4 | module.exports = ({ firebase }) => { 5 | if (!singleton) { 6 | singleton = admin.initializeApp(firebase) 7 | } 8 | return singleton; 9 | }; 10 | -------------------------------------------------------------------------------- /app/storage.rules: -------------------------------------------------------------------------------- 1 | service firebase.storage { 2 | match /b/{bucket}/o { 3 | match /{environment} { 4 | match /uploads/{filename} { 5 | allow write: if request.auth.token.admin == true; 6 | allow read; 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /dev/workspace/.zshrc: -------------------------------------------------------------------------------- 1 | # Lines to append to .zshrc from deltaepsilon/dotfiles 2 | # Don't leave blank lines... it copies poorly 3 | export NVM_DIR="$HOME/.nvm" 4 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm 5 | 6 | # Aliases 7 | alias ll="ls -al" -------------------------------------------------------------------------------- /app/src/assets/svg/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/components/views/tags.view.scss: -------------------------------------------------------------------------------- 1 | .tagsView { 2 | min-height: calc(100vh - 5rem); 3 | width: calc(100vw - 3rem); 4 | } 5 | 6 | .header { 7 | font-size: 1.5rem; 8 | font-style: italic; 9 | font-weight: 800; 10 | text-align: left; 11 | } 12 | -------------------------------------------------------------------------------- /app/src/datastore/actions/removeSelection.action.js: -------------------------------------------------------------------------------- 1 | export function removeSelection({ selection, selecting }, id) { 2 | selection.delete(id); 3 | if (!selection.size) { 4 | selecting = false; 5 | } 6 | return { selection: new Set(selection), selecting }; 7 | } 8 | -------------------------------------------------------------------------------- /app/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionId": "uploads", 5 | "fields": [ 6 | { "fieldPath": "isProduction", "mode": "ASCENDING" }, 7 | { "fieldPath": "created", "mode": "DESCENDING" } 8 | ] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /app/src/assets/svg/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/datastore/actions/addSelection.action.js: -------------------------------------------------------------------------------- 1 | export function addSelection({ selection }, ids) { 2 | if (typeof ids == 'string') { 3 | ids = [ids]; 4 | } 5 | 6 | ids.forEach(id => id && selection.add(id)); 7 | return { selection: new Set(selection), selecting: true }; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/queries/index.js: -------------------------------------------------------------------------------- 1 | export * from './deleteImage.query'; 2 | export * from './image.query'; 3 | export * from './images.query'; 4 | export * from './imagesByTag.query'; 5 | export * from './imageVersion.query'; 6 | export * from './updateImage.query'; 7 | export * from './updateTags.query'; 8 | -------------------------------------------------------------------------------- /app/src/assets/svg/open-with.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/components/views/embed.view.scss: -------------------------------------------------------------------------------- 1 | .embedView { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | 6 | textarea { 7 | width: calc(100vw - 8rem); 8 | height: 2rem; 9 | } 10 | 11 | iframe, textarea { 12 | margin: 1rem; 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/datastore/actions/addImages.action.js: -------------------------------------------------------------------------------- 1 | export function addImages({ images }, newImages) { 2 | images = [...images, ...newImages]; 3 | 4 | const tags = new Set(); 5 | images.forEach(image => image.tags && image.tags.forEach(tag => tags.add(tag))); 6 | return { images, tags }; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setControlSelect.action.js: -------------------------------------------------------------------------------- 1 | export function setControlSelect({ controlSelect, shiftSelect }, value) { 2 | if (!!value) { 3 | shiftSelect = false; 4 | } 5 | 6 | if (value != controlSelect) { 7 | return { controlSelect: !!value, shiftSelect }; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setShiftSelect.action.js: -------------------------------------------------------------------------------- 1 | export function setShiftSelect({ shiftSelect, controlSelect }, value) { 2 | if (!!value) { 3 | controlSelect = false; 4 | } 5 | 6 | if (value != shiftSelect) { 7 | return { shiftSelect: !!value, controlSelect }; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /dev/vault/vault.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "storage": { 3 | "gcs": { 4 | "bucket": "quiver-vault" 5 | } 6 | }, 7 | "listener": { 8 | "tcp": { 9 | "address": "0.0.0.0:8200", 10 | "tls_disable": true 11 | } 12 | }, 13 | "disable_mlock": true, 14 | "ui": true 15 | } 16 | -------------------------------------------------------------------------------- /app/src/assets/svg/check-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/queries/deleteImage.query.js: -------------------------------------------------------------------------------- 1 | export async function deleteImageQuery({ environment, id }) { 2 | const uploads = environment.collections.uploads; 3 | const doc = window.firebase 4 | .firestore() 5 | .collection(uploads) 6 | .doc(id); 7 | 8 | await doc.update({ deleted: true }); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/assets/svg/content-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/functions/utils/algolia.util.js: -------------------------------------------------------------------------------- 1 | const algoliasearch = require('algoliasearch'); 2 | let singleton; 3 | 4 | module.exports = ({ algolia }) => { 5 | if (!singleton) { 6 | const { applicationId, apiKey } = algolia; 7 | singleton = algoliasearch(applicationId, apiKey); 8 | } 9 | return singleton; 10 | }; 11 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/cloud-builders/docker' 3 | args: ['build', '-t', 'us.gcr.io/$PROJECT_ID/fogo:latest', '.'] 4 | - name: 'us.gcr.io/$PROJECT_ID/fogo:latest' 5 | dir: '/app' 6 | args: ['yarn', 'ci:deploy', '--token', '$_FIREBASE_TOKEN'] 7 | images: ['us.gcr.io/$PROJECT_ID/fogo:latest'] 8 | timeout: 3600s 9 | -------------------------------------------------------------------------------- /app/firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | match /uploads/{document=**} { 4 | allow write: if request.auth.token.admin == true ; 5 | allow read; 6 | } 7 | 8 | match /users/{document=**} { 9 | allow read, write: if request.auth.token.admin == true ; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/components/drawer/drawer.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: relative; 3 | z-index: 100; 4 | } 5 | 6 | .drawer { 7 | .list { 8 | margin-top: 4rem; 9 | } 10 | 11 | .item { 12 | font-family: 'mr-eaves-modern' !important; 13 | font-size: 2rem !important; 14 | font-weight: 800; 15 | font-style: italic; 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/components/views/images.view.scss: -------------------------------------------------------------------------------- 1 | .imagesView { 2 | min-height: calc(100vh - 5rem); 3 | } 4 | 5 | .header { 6 | font-size: 1.5rem; 7 | font-style: italic; 8 | font-weight: 800; 9 | text-align: left; 10 | } 11 | 12 | .fab { 13 | position: fixed !important; 14 | right: 2rem; 15 | bottom: 2rem; 16 | z-index: 9; 17 | } 18 | -------------------------------------------------------------------------------- /app/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Fogo", 3 | "short_name": "Fogo", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#fff", 8 | "theme_color": "#29b6f6", 9 | "icons": [ 10 | { 11 | "src": "/assets/icon.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /bin/vault/copy-vault-keys.sh: -------------------------------------------------------------------------------- 1 | CURRENT_PATH="`dirname \"$0\"`" 2 | CURRENT_PATH="`( cd \"$CURRENT_PATH\" && pwd )`" 3 | if [ -z "$CURRENT_PATH" ] ; then 4 | exit 1 5 | fi 6 | 7 | docker-compose exec vault sh /dev/vault/bin/unseal.sh 8 | docker-compose exec vault sh /dev/vault/bin/copy.sh 9 | docker-compose exec vault sh /dev/vault/bin/seal.sh 10 | node $CURRENT_PATH/expand-secrets.js -------------------------------------------------------------------------------- /app/functions/utils/environment.util.spec.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const environmentUtil = require('./environment.util'); 3 | 4 | describe('environmentUtil', () => { 5 | it('should read development environment', () => { 6 | const environment = environmentUtil(functions); 7 | expect(typeof environment.firebase).toEqual('object'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /functions/utils/environment.util.spec.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const environmentUtil = require('./environment.util'); 3 | 4 | describe('environmentUtil', () => { 5 | it('should read development environment', () => { 6 | const environment = environmentUtil(functions); 7 | expect(typeof environment.firebase).toEqual('object'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /app/src/datastore/actions/setSearching.action.js: -------------------------------------------------------------------------------- 1 | export function setSearching({ searchResults, selection, selecting }, searching) { 2 | if (searching) { 3 | selection = new Set(); 4 | 5 | if (searchResults) { 6 | selecting = true; 7 | } 8 | } else { 9 | searchResults = null 10 | } 11 | return { searching: !!searching, search: '', searchResults, selection, selecting }; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/assets/svg/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/components/views/galleries.view.scss: -------------------------------------------------------------------------------- 1 | .galleriesView { 2 | min-height: calc(100vh - 8rem); 3 | width: calc(100vw - 3rem); 4 | 5 | h3 { 6 | font-family: 'mr-eaves-modern' !important; 7 | font-size: 2rem !important; 8 | font-weight: 800; 9 | font-style: italic; 10 | text-transform: uppercase; 11 | } 12 | } 13 | 14 | .row { 15 | display: flex; 16 | align-items: center; 17 | 18 | a { 19 | margin-right: 1rem; 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/queries/image.query.js: -------------------------------------------------------------------------------- 1 | export async function imageQuery(environment, id) { 2 | const uploads = environment.collections.uploads; 3 | const ref = window.firebase 4 | .firestore() 5 | .collection(uploads) 6 | .doc(id); 7 | const doc = await ref.get(); 8 | const image = { 9 | __id: doc.id, 10 | ...doc.data(), 11 | }; 12 | 13 | if (image.tags) { 14 | image.tags = Object.keys(image.tags); 15 | } 16 | 17 | return image; 18 | } 19 | -------------------------------------------------------------------------------- /app/src/components/views/tags.view.js: -------------------------------------------------------------------------------- 1 | import style from './tags.view.scss'; 2 | import { connect } from 'unistore'; 3 | import { actions } from '../../datastore'; 4 | 5 | import Tags from '../tags/tags.component'; 6 | 7 | const TagsView = connect('selection', actions)(({ selection }) => ( 8 |
9 |

Tags

10 | 11 |
12 | )); 13 | 14 | export { TagsView }; 15 | -------------------------------------------------------------------------------- /app/src/assets/svg/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.bolt" 4 | }, 5 | "firestore": { 6 | "rules": "firestore.rules", 7 | "indexes": "firestore.indexes.json" 8 | }, 9 | "hosting": { 10 | "public": "build", 11 | "ignore": [ 12 | "firebase.json", 13 | "**/.*", 14 | "**/node_modules/**" 15 | ], 16 | "rewrites": [ 17 | { 18 | "source": "**", 19 | "destination": "/index.html" 20 | } 21 | ] 22 | }, 23 | "storage": { 24 | "rules": "storage.rules" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/datastore/actions/updateImage.action.js: -------------------------------------------------------------------------------- 1 | export function updateImage({ environment, images, image }, updatedImage) { 2 | const { __id: id } = updatedImage; 3 | const imageIndex = images.findIndex(image => image.__id == id); 4 | 5 | images = images.slice(0); 6 | 7 | if (image && image.__id == id) { 8 | image = updatedImage; 9 | } 10 | 11 | images[imageIndex] = updatedImage; 12 | 13 | const tags = new Set(); 14 | images.forEach(image => image.tags && image.tags.forEach(tag => tags.add(tag))); 15 | 16 | return { images, image, tags }; 17 | } 18 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | workspace: 4 | build: ./dev/workspace 5 | env_file: ./dev/workspace/env.list 6 | ports: 7 | - '3000:3000' 8 | - '8080:8080' 9 | - '41000:41000' 10 | volumes: 11 | - './app:/app' 12 | vault: 13 | container_name: vault 14 | build: ./dev/vault 15 | env_file: ./dev/vault/env.list 16 | volumes: 17 | - ./dev/vault:/dev/vault 18 | - ./app/vault:/app/vault 19 | ports: 20 | - 8200:8200 21 | cap_add: 22 | - IPC_LOCK 23 | 24 | -------------------------------------------------------------------------------- /app/src/queries/updateImage.query.js: -------------------------------------------------------------------------------- 1 | import { imageQuery } from './image.query'; 2 | import { mappedActions } from '../datastore'; 3 | const { updateImage } = mappedActions; 4 | 5 | export async function updateImageQuery({ environment, image }) { 6 | const { __id: id, description } = image; 7 | const uploads = environment.collections.uploads; 8 | const doc = window.firebase 9 | .firestore() 10 | .collection(uploads) 11 | .doc(id); 12 | 13 | await doc.set({ description }, { merge: true }); 14 | const updatedImage = await imageQuery(environment, id); 15 | updateImage(updatedImage); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/components/nav/nav.scss: -------------------------------------------------------------------------------- 1 | .mdc-toolbar { 2 | opacity: 0.9; 3 | position: fixed !important; 4 | z-index: 10; 5 | 6 | .mdc-toolbar__section { 7 | align-items: center; 8 | } 9 | 10 | .mdc-toolbar__title { 11 | font-family: 'mr-eaves-modern' !important; 12 | font-size: 2rem !important; 13 | font-weight: 800; 14 | font-style: italic; 15 | } 16 | } 17 | 18 | .toolbarIcon { 19 | cursor: pointer; 20 | } 21 | 22 | .actions { 23 | display: flex; 24 | align-items: center; 25 | padding: 0 2rem; 26 | flex-grow: 1; 27 | } 28 | 29 | .icon { 30 | cursor: pointer; 31 | margin: 0 1rem; 32 | } 33 | 34 | .toolbarIcon { 35 | margin-left: 1rem; 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@quiver/fogo", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/how-to-firebase/fogo.git", 6 | "author": "Chris Esplin ", 7 | "private": true, 8 | "scripts": { 9 | "build": "docker-compose build", 10 | "dev": "docker-compose build workspace && docker-compose run --service-ports --rm workspace zsh", 11 | "ci:login": "npx firebase login:ci --no-localhost", 12 | "ci:build": "docker build --tag=fogo .", 13 | "ci:interactive": "docker run -it --rm fogo sh", 14 | "ci:latest": "docker run -it --rm gcr.io/fir-consulting-2019/fogo:latest sh", 15 | "windows:watch": "powershell ./bin/watch.ps1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/datastore/actions/addImageVersion.action.js: -------------------------------------------------------------------------------- 1 | export function addImageVersion({ images, image, environment }, { versionName, url }) { 2 | const imageIndex = images.findIndex(image => image.__id == record); 3 | const clone = { ...images[imageIndex] }; 4 | 5 | images = images.slice(0); 6 | 7 | clone.versions = { ...clone.versions, [versionName]: { url } }; 8 | 9 | if (!clone.versions[versionName].url) { 10 | console.error('url missing!', url); 11 | clone.versions[versionName].url = null; 12 | } 13 | 14 | if (image && image.__id == record) { 15 | image = { ...clone }; 16 | } 17 | 18 | images[imageIndex] = clone; 19 | 20 | return { images, image, timestamp: Date.now() }; 21 | } 22 | -------------------------------------------------------------------------------- /app/src/queries/imagesByTag.query.js: -------------------------------------------------------------------------------- 1 | export async function imagesByTagQuery({ environment, tag }) { 2 | const uploads = environment.collections.uploads; 3 | const query = window.firebase 4 | .firestore() 5 | .collection(uploads) 6 | .where('environment', '==', environment.environment) 7 | .where(`tags.${tag}`, '==', true); 8 | 9 | // Don't use an orderBy here because it requires a new index for every tag... which is insane! 10 | 11 | const snapshot = await query.get(); 12 | const results = snapshot.docs 13 | .map(doc => ({ 14 | __id: doc.id, 15 | ...doc.data(), 16 | })) 17 | .sort((a, b) => { 18 | return a.filename > b.filename ? 1 : -1; 19 | }); 20 | 21 | return results; 22 | } 23 | -------------------------------------------------------------------------------- /app/src/components/views/embed.view.js: -------------------------------------------------------------------------------- 1 | import style from './embed.view.scss'; 2 | import { connect } from 'unistore'; 3 | import { actions } from '../../datastore'; 4 | 5 | export function EmbedView({ tag }) { 6 | return ( 7 |
8 | 11 |