├── .gitignore ├── .prettierrc.js ├── README.md ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── functions ├── .eslintrc.json ├── .gitignore ├── index.js ├── package-lock.json └── package.json ├── package.json ├── public ├── 404.html ├── favicon.png ├── global.css ├── images │ ├── almond.jpg │ ├── ebro_river.jpg │ ├── firebase-logo.png │ ├── materialize.svg │ ├── svelte-logo.svg │ ├── svelte-router-logo.png │ ├── validate-js-logo.png │ └── waterfall.jpg ├── index.html ├── plugins.css └── stylesheets │ └── custom.css ├── rollup.config.js ├── src ├── App.svelte ├── config │ ├── firebase.js │ └── settings.js ├── lib │ ├── diacritics.js │ ├── filter_results.js │ └── routes │ │ ├── protected.js │ │ └── public.js ├── main.js ├── middleware │ ├── database │ │ ├── employees.js │ │ ├── firebase_results.js │ │ ├── index.js │ │ └── teams.js │ ├── employees │ │ └── crud.js │ └── users │ │ └── auth.js ├── routes.js ├── stores │ ├── current_user.js │ └── notification_message.js └── views │ ├── 404.svelte │ ├── admin │ ├── dashboard │ │ └── index.svelte │ ├── employees │ │ ├── edit.svelte │ │ ├── form.svelte │ │ ├── header │ │ │ ├── index.svelte │ │ │ └── search.svelte │ │ ├── index.svelte │ │ ├── item.svelte │ │ ├── layout.svelte │ │ ├── list.svelte │ │ ├── new.svelte │ │ └── show.svelte │ ├── layout │ │ ├── header.svelte │ │ ├── index.svelte │ │ └── sidebar │ │ │ ├── index.svelte │ │ │ ├── item.svelte │ │ │ └── menu.svelte │ └── teams │ │ ├── form.svelte │ │ ├── index.svelte │ │ ├── item.svelte │ │ ├── list.svelte │ │ └── show.svelte │ ├── components │ ├── forms │ │ ├── buttons.svelte │ │ ├── check_box.svelte │ │ ├── date_input.svelte │ │ ├── date_range.svelte │ │ ├── email_input.svelte │ │ ├── number_input.svelte │ │ ├── password_input.svelte │ │ ├── radio_input.svelte │ │ ├── select.svelte │ │ ├── text_input.svelte │ │ ├── textarea.svelte │ │ ├── time_input.svelte │ │ └── time_range.svelte │ ├── loading.svelte │ ├── modals │ │ ├── buttons.svelte │ │ └── modal.svelte │ ├── notification.svelte │ └── number_pad │ │ ├── button.svelte │ │ └── index.svelte │ └── public │ ├── home │ ├── hero.svelte │ ├── how_to_use_it.svelte │ ├── index.svelte │ ├── main_features.svelte │ ├── menu.svelte │ └── technology.svelte │ ├── layout │ ├── footer.svelte │ └── index.svelte │ ├── login │ ├── form.svelte │ └── index.svelte │ └── signup │ ├── form.svelte │ └── index.svelte ├── storage.rules └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | functions/node_modules 4 | public/bundle.* 5 | src/config/settings.js 6 | /dist 7 | src/config 8 | 9 | /tests/e2e/videos/ 10 | /tests/e2e/screenshots/ 11 | 12 | # local env files 13 | .env.local 14 | .env.*.local 15 | 16 | # Log files 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # Editor directories and files 22 | .idea 23 | .vscode 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw* 29 | 30 | .firebase/* 31 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: false, 4 | printWidth: 120 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte - Firebase 2 | 3 | A free template that you can use to create new applications using Svelte and Firebase. 4 | 5 | You can see a live demo here: [https://svelte-firebase-template.web.app/](https://svelte-firebase-template.web.app/) 6 | 7 | ## Features 8 | 9 | - Powerfull routing system with nested layouts. 10 | - Public and Private sections 11 | - Form validation 12 | - Preconfigured pages for Home, Login, Signup and more... 13 | - Secure your database with Firebase rules 14 | - Fully resposive theme 15 | - And many more... 16 | 17 | ## Usage 18 | 19 | Grab a copy of the template and install the dependencies: 20 | 21 | ```bash 22 | git clone https://github.com/jorgegorka/svelte-firebase my-app-name 23 | cd my-app-name && yarn install 24 | ``` 25 | 26 | Add your Firebase configuration info to 27 | _src/config/settings.js_ 28 | 29 | If you don't have a Firebase project you can create one in the 30 | [Firebase website](https://firebase.google.com/) 31 | 32 | Activate cloud firestore, storage and hosting in the Firebase console 33 | 34 | ```javascript 35 | const config = { 36 | apiKey: '', 37 | authDomain: '', 38 | databaseURL: '', 39 | projectId: '', 40 | storageBucket: '', 41 | messagingSenderId: '' 42 | } 43 | ``` 44 | 45 | **Update .firebaserc with your project ID** 46 | 47 | Install all the dependencies required by functions. 48 | 49 | ```bash 50 | cd functions 51 | npm i 52 | ``` 53 | 54 | Now we want to deploy all the rules, indexes and cloud functions to Firebase. 55 | 56 | 57 | ```bash 58 | yarn deploy 59 | ``` 60 | 61 | This first first deployment will setup Firebase so everything is ready for development. 62 | 63 | ### Development 64 | 65 | Launch the development server 66 | 67 | ```bash 68 | yarn dev 69 | ``` 70 | 71 | Visit [http://localhost:5000](http://localhost:5000) 72 | 73 | To add new pages edit the routes files at _src/lib/routes_ 74 | 75 | There are public and protected routes. Protected routes require the visitor to be authenticated before accesing them. 76 | 77 | There are two complete CRUD examples: Teams and Employees. 78 | 79 | ### Deployment 80 | 81 | Rembember to activate cloud firestore, storage and hosting in the Firebase console before deploying for the first time. 82 | 83 | ```bash 84 | yarn deploy 85 | ``` 86 | 87 | Enjoy 88 | 89 | ## Contribute 90 | 91 | Your comments, suggestions and improvements are [very welcome](https://github.com/jorgegorka/svelte-firebase/issues). 92 | 93 | ## Credits 94 | 95 | Svelte-Firebase has been created by [Jorge Alvarez](https://www.alvareznavarro.es). 96 | 97 | ## License 98 | 99 | [Released under MIT license](http://www.opensource.org/licenses/MIT) 100 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "functions": { 7 | "predeploy": [ 8 | "npm --prefix \"$RESOURCE_DIR\" run lint" 9 | ], 10 | "source": "functions" 11 | }, 12 | "hosting": { 13 | "public": "public", 14 | "ignore": [ 15 | "firebase.json", 16 | "**/.*", 17 | "**/node_modules/**" 18 | ], 19 | "rewrites": [ 20 | { 21 | "source": "**", 22 | "destination": "/index.html" 23 | } 24 | ] 25 | }, 26 | "storage": { 27 | "rules": "storage.rules" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "teams", 5 | "queryScope": "COLLECTION", 6 | "fields": [ 7 | { 8 | "fieldPath": "companyId", 9 | "order": "ASCENDING" 10 | }, 11 | { 12 | "fieldPath": "name", 13 | "order": "ASCENDING" 14 | } 15 | ] 16 | }, 17 | { 18 | "collectionGroup": "employees", 19 | "queryScope": "COLLECTION", 20 | "fields": [ 21 | { 22 | "fieldPath": "companyId", 23 | "order": "ASCENDING" 24 | }, 25 | { 26 | "fieldPath": "name", 27 | "order": "ASCENDING" 28 | } 29 | ] 30 | }, 31 | { 32 | "collectionGroup": "employees", 33 | "queryScope": "COLLECTION", 34 | "fields": [ 35 | { 36 | "fieldPath": "active", 37 | "order": "ASCENDING" 38 | }, 39 | { 40 | "fieldPath": "name", 41 | "order": "ASCENDING" 42 | } 43 | ] 44 | }, 45 | { 46 | "collectionGroup": "employees", 47 | "queryScope": "COLLECTION", 48 | "fields": [ 49 | { 50 | "fieldPath": "teamId", 51 | "order": "ASCENDING" 52 | }, 53 | { 54 | "fieldPath": "name", 55 | "order": "ASCENDING" 56 | } 57 | ] 58 | } 59 | ], 60 | "fieldOverrides": [] 61 | } 62 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | match /companies/{companyId} { 4 | allow read: if isSignedIn() && userBelongsToCompany(); 5 | allow update, delete: if companyAdmin() 6 | } 7 | 8 | match /teams/{teamId} { 9 | allow read: if isSignedIn() && adminOrEmployee(); 10 | allow create: if userAndAdmin(); 11 | allow update, delete: if companyAdmin() 12 | } 13 | 14 | match /employees/{employeeId} { 15 | allow read: if isSignedIn() && userBelongsToCompany(); 16 | allow create: if userAndAdmin(); 17 | allow update, delete: if companyAdmin() 18 | } 19 | } 20 | 21 | function isSignedIn() { 22 | return (request.auth.uid != null) 23 | } 24 | 25 | function userIsAdmin() { 26 | return request.auth.token.role == 'admin' 27 | } 28 | 29 | function userBelongsToCompany() { 30 | return request.auth.token.companyId == resource.data.companyId 31 | } 32 | 33 | function userAndAdmin() { 34 | return isSignedIn() && userIsAdmin() 35 | } 36 | 37 | function companyAdmin() { 38 | return userAndAdmin() && userBelongsToCompany() 39 | } 40 | 41 | function adminOrEmployee() { 42 | return userBelongsToCompany() && (userIsAdmin() || resource.data.id == request.auth.uid) 43 | } 44 | 45 | function adminOrOwner() { 46 | return userBelongsToCompany() && (userIsAdmin() || resource.data.employeeId == request.auth.uid) 47 | } 48 | 49 | function notCreator() { 50 | return resource.data.status != 'creator' 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /functions/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | // Required for certain syntax usages 4 | "ecmaVersion": 2017 5 | }, 6 | "plugins": [ 7 | "promise" 8 | ], 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | // Removed rule "disallow the use of console" from recommended eslint rules 12 | "no-console": "off", 13 | 14 | // Removed rule "disallow multiple spaces in regular expressions" from recommended eslint rules 15 | "no-regex-spaces": "off", 16 | 17 | // Removed rule "disallow the use of debugger" from recommended eslint rules 18 | "no-debugger": "off", 19 | 20 | // Removed rule "disallow unused variables" from recommended eslint rules 21 | "no-unused-vars": "off", 22 | 23 | // Removed rule "disallow mixed spaces and tabs for indentation" from recommended eslint rules 24 | "no-mixed-spaces-and-tabs": "off", 25 | 26 | // Removed rule "disallow the use of undeclared variables unless mentioned in /*global */ comments" from recommended eslint rules 27 | "no-undef": "off", 28 | 29 | // Warn against template literal placeholder syntax in regular strings 30 | "no-template-curly-in-string": 1, 31 | 32 | // Warn if return statements do not either always or never specify values 33 | "consistent-return": 1, 34 | 35 | // Warn if no return statements in callbacks of array methods 36 | "array-callback-return": 1, 37 | 38 | // Require the use of === and !== 39 | "eqeqeq": 2, 40 | 41 | // Disallow the use of alert, confirm, and prompt 42 | "no-alert": 2, 43 | 44 | // Disallow the use of arguments.caller or arguments.callee 45 | "no-caller": 2, 46 | 47 | // Disallow null comparisons without type-checking operators 48 | "no-eq-null": 2, 49 | 50 | // Disallow the use of eval() 51 | "no-eval": 2, 52 | 53 | // Warn against extending native types 54 | "no-extend-native": 1, 55 | 56 | // Warn against unnecessary calls to .bind() 57 | "no-extra-bind": 1, 58 | 59 | // Warn against unnecessary labels 60 | "no-extra-label": 1, 61 | 62 | // Disallow leading or trailing decimal points in numeric literals 63 | "no-floating-decimal": 2, 64 | 65 | // Warn against shorthand type conversions 66 | "no-implicit-coercion": 1, 67 | 68 | // Warn against function declarations and expressions inside loop statements 69 | "no-loop-func": 1, 70 | 71 | // Disallow new operators with the Function object 72 | "no-new-func": 2, 73 | 74 | // Warn against new operators with the String, Number, and Boolean objects 75 | "no-new-wrappers": 1, 76 | 77 | // Disallow throwing literals as exceptions 78 | "no-throw-literal": 2, 79 | 80 | // Require using Error objects as Promise rejection reasons 81 | "prefer-promise-reject-errors": 2, 82 | 83 | // Enforce “for” loop update clause moving the counter in the right direction 84 | "for-direction": 2, 85 | 86 | // Enforce return statements in getters 87 | "getter-return": 2, 88 | 89 | // Disallow await inside of loops 90 | "no-await-in-loop": 2, 91 | 92 | // Disallow comparing against -0 93 | "no-compare-neg-zero": 2, 94 | 95 | // Warn against catch clause parameters from shadowing variables in the outer scope 96 | "no-catch-shadow": 1, 97 | 98 | // Disallow identifiers from shadowing restricted names 99 | "no-shadow-restricted-names": 2, 100 | 101 | // Enforce return statements in callbacks of array methods 102 | "callback-return": 2, 103 | 104 | // Require error handling in callbacks 105 | "handle-callback-err": 2, 106 | 107 | // Warn against string concatenation with __dirname and __filename 108 | "no-path-concat": 1, 109 | 110 | // Prefer using arrow functions for callbacks 111 | "prefer-arrow-callback": 1, 112 | 113 | // Return inside each then() to create readable and reusable Promise chains. 114 | // Forces developers to return console logs and http calls in promises. 115 | "promise/always-return": 2, 116 | 117 | //Enforces the use of catch() on un-returned promises 118 | "promise/catch-or-return": 2, 119 | 120 | // Warn against nested then() or catch() statements 121 | "promise/no-nesting": 1 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions') 2 | const admin = require('firebase-admin') 3 | admin.initializeApp() 4 | const firestore = admin.firestore() 5 | firestore.settings({ timestampsInSnapshots: true }) 6 | 7 | exports.createCompany = functions.region('europe-west1').https.onCall(async (data, context) => { 8 | const Companies = firestore.collection('companies') 9 | const Employees = firestore.collection('employees') 10 | 11 | if (!context.auth && !context.auth.uid) { 12 | throw new functions.https.HttpsError('unauthenticated') 13 | } 14 | 15 | const { companyName } = data 16 | if (!companyName) { 17 | throw new functions.https.HttpsError('not-found') 18 | } 19 | const userId = context.auth.uid 20 | 21 | await admin.auth().setCustomUserClaims(userId, { role: 'admin' }) 22 | 23 | return Companies.add({ name: companyName.toString(), createdBy: userId, createdAt: new Date() }).then(doc => { 24 | Employees.doc(userId).set({ 25 | name: companyName.toString(), 26 | role: 'creator', 27 | companyId: doc.id, 28 | createdAt: new Date(), 29 | createdBy: userId, 30 | active: true 31 | }) 32 | admin.auth().setCustomUserClaims(userId, { companyId: doc.id, role: 'admin' }) 33 | 34 | return 'ok' 35 | }) 36 | }) 37 | 38 | exports.createEmployee = functions.region('europe-west1').https.onCall(async (data, context) => { 39 | const Employees = firestore.collection('employees') 40 | 41 | if (!context.auth && !context.auth.uid && !context.auth.token.companyId && !context.auth.token.role === 'admin') { 42 | throw new functions.https.HttpsError('unauthenticated') 43 | } 44 | 45 | const employeeData = data 46 | 47 | if (!employeeData || !employeeData.email || !employeeData.name || !employeeData.password) { 48 | throw new functions.https.HttpsError('not-found') 49 | } 50 | 51 | const newUser = await admin.auth().createUser({ 52 | email: employeeData.email, 53 | displayName: employeeData.name, 54 | password: employeeData.password 55 | }) 56 | 57 | if (!newUser) { 58 | throw new functions.https.HttpsError('not-found') 59 | } 60 | 61 | admin.auth().setCustomUserClaims(newUser.uid, { 62 | companyId: context.auth.token.companyId, 63 | role: 'user' 64 | }) 65 | 66 | const newEmployeeInfo = { 67 | id: newUser.uid, 68 | email: employeeData.email, 69 | name: employeeData.name, 70 | role: 'user', 71 | active: true, 72 | companyId: context.auth.token.companyId, 73 | createdAt: new Date(), 74 | createdBy: context.auth.uid 75 | } 76 | 77 | if (employeeData.teamId) { 78 | newEmployeeInfo.teamId = employeeData.teamId 79 | newEmployeeInfo.teamName = employeeData.teamName 80 | } 81 | 82 | return Employees.doc(newUser.uid) 83 | .set(newEmployeeInfo) 84 | .then(() => { 85 | return 'ok' 86 | }) 87 | }) 88 | 89 | exports.teamCreate = functions 90 | .region('europe-west1') 91 | .firestore.document('teams/{teamId}') 92 | .onCreate((snapshot, _context) => { 93 | const newTeam = snapshot.data() 94 | const teamRef = snapshot.ref 95 | 96 | if (!newTeam.createdBy) { 97 | return true 98 | } 99 | 100 | return admin 101 | .auth() 102 | .getUser(newTeam.createdBy) 103 | .then(userInfo => { 104 | return teamRef.update({ 105 | createdAt: new Date(), 106 | employeesCount: 0, 107 | companyId: userInfo.customClaims.companyId 108 | }) 109 | }) 110 | }) 111 | 112 | exports.updateTeamsCount = functions 113 | .region('europe-west1') 114 | .firestore.document('employees/{employeeID}') 115 | .onWrite(async (change, _context) => { 116 | let changes = [] 117 | 118 | // Update employee 119 | if (change.before.exists && change.after.exists) { 120 | const updatedEmployee = change.after.data() 121 | const oldEmployee = change.before.data() 122 | if (updatedEmployee !== oldEmployee) { 123 | if (oldEmployee.teamId) { 124 | changes.push({ action: admin.firestore.FieldValue.increment(-1), teamId: oldEmployee.teamId }) 125 | } 126 | 127 | if (updatedEmployee.teamId) { 128 | changes.push({ action: admin.firestore.FieldValue.increment(1), teamId: updatedEmployee.teamId }) 129 | } 130 | } 131 | } 132 | 133 | // New employee 134 | if (!change.before.exists) { 135 | const employee = change.after.data() 136 | changes.push({ action: admin.firestore.FieldValue.increment(1), teamId: employee.teamId }) 137 | } 138 | 139 | // Removed employee 140 | if (!change.after.exists) { 141 | const employee = change.before.data() 142 | changes.push({ action: admin.firestore.FieldValue.increment(-1), teamId: employee.teamId }) 143 | } 144 | 145 | // Updated team for employee 146 | if (changes.length === 0) { 147 | console.log('no changes') 148 | return 'no changes' 149 | } 150 | const Teams = firestore.collection('teams') 151 | 152 | changes.forEach(change => { 153 | Teams.doc(change.teamId).update({ employeesCount: change.action }) 154 | }) 155 | 156 | return 'ok' 157 | }) 158 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "serve": "firebase serve --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "10" 14 | }, 15 | "dependencies": { 16 | "firebase-admin": "^11.5.0", 17 | "firebase-functions": "^3.11.0" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^7.11.0", 21 | "eslint-plugin-promise": "^4.2.1", 22 | "firebase-functions-test": "^0.2.2" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-firebase", 3 | "version": "0.5.0", 4 | "devDependencies": { 5 | "npm-run-all": "^4.1.5", 6 | "rollup": "^2.0.0", 7 | "rollup-plugin-commonjs": "^10.0.0", 8 | "rollup-plugin-css-only": "^2.0.0", 9 | "rollup-plugin-livereload": "^2.0.0", 10 | "rollup-plugin-node-resolve": "^5.2.0", 11 | "rollup-plugin-svelte": "^6.0.0", 12 | "rollup-plugin-terser": "^7.0.0", 13 | "sirv-cli": "^1.0.0", 14 | "svelte": "^3.49.0" 15 | }, 16 | "scripts": { 17 | "build": "rollup -c", 18 | "autobuild": "rollup -c -w", 19 | "dev": "run-p start:dev autobuild", 20 | "start": "sirv public", 21 | "start:dev": "sirv public -s --dev", 22 | "deploy": "yarn build && firebase deploy" 23 | }, 24 | "dependencies": { 25 | "firebase": "^7.0.0", 26 | "materialize-css": "^1.0.0", 27 | "svelte-router-spa": "^5.0.0", 28 | "validate.js": "^0.13.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte & Firebase - A free template to create single page applications. 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 35 | 36 | 37 | 38 |
39 |

Page not found!

40 |

I've searched everywhere but it wasn't there. I'm as puzzled as you are!

41 |
42 | Return to the 43 | homepage 44 |
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgegorka/svelte-firebase/2f3ddac4d242f3dab15e8b3894066710fe02c1de/public/favicon.png -------------------------------------------------------------------------------- /public/global.css: -------------------------------------------------------------------------------- 1 | .primary-toast { 2 | background-color: #0277bd; 3 | } 4 | .success-toast { 5 | background-color: #2e7d32; 6 | } 7 | .danger-toast { 8 | background-color: #c62828; 9 | } 10 | 11 | .sidenav li a.active { 12 | background-color: rgba(0, 0, 0, 0.05); 13 | } 14 | -------------------------------------------------------------------------------- /public/images/almond.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgegorka/svelte-firebase/2f3ddac4d242f3dab15e8b3894066710fe02c1de/public/images/almond.jpg -------------------------------------------------------------------------------- /public/images/ebro_river.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgegorka/svelte-firebase/2f3ddac4d242f3dab15e8b3894066710fe02c1de/public/images/ebro_river.jpg -------------------------------------------------------------------------------- /public/images/firebase-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgegorka/svelte-firebase/2f3ddac4d242f3dab15e8b3894066710fe02c1de/public/images/firebase-logo.png -------------------------------------------------------------------------------- /public/images/materialize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 59 | 60 | 62 | 63 | 65 | 66 | 68 | 69 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 129 | 130 | 132 | 133 | 135 | 136 | 138 | 139 | 141 | 142 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /public/images/svelte-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/svelte-router-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgegorka/svelte-firebase/2f3ddac4d242f3dab15e8b3894066710fe02c1de/public/images/svelte-router-logo.png -------------------------------------------------------------------------------- /public/images/validate-js-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgegorka/svelte-firebase/2f3ddac4d242f3dab15e8b3894066710fe02c1de/public/images/validate-js-logo.png -------------------------------------------------------------------------------- /public/images/waterfall.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgegorka/svelte-firebase/2f3ddac4d242f3dab15e8b3894066710fe02c1de/public/images/waterfall.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte & Firebase - A free template to create single page applications. 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/stylesheets/custom.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgegorka/svelte-firebase/2f3ddac4d242f3dab15e8b3894066710fe02c1de/public/stylesheets/custom.css -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte' 2 | import resolve from 'rollup-plugin-node-resolve' 3 | import commonjs from 'rollup-plugin-commonjs' 4 | import livereload from 'rollup-plugin-livereload' 5 | import { terser } from 'rollup-plugin-terser' 6 | import css from 'rollup-plugin-css-only' 7 | 8 | const production = !process.env.ROLLUP_WATCH 9 | 10 | export default { 11 | input: 'src/main.js', 12 | output: { 13 | sourcemap: true, 14 | format: 'iife', 15 | name: 'app', 16 | file: 'public/bundle.js' 17 | }, 18 | plugins: [ 19 | svelte({ 20 | // enable run-time checks when not in production 21 | dev: !production, 22 | // we'll extract any component CSS out into 23 | // a separate file — better for performance 24 | css: css => { 25 | css.write('public/bundle.css') 26 | } 27 | }), 28 | css({ output: 'public/plugins.css' }), 29 | // If you have external dependencies installed from 30 | // npm, you'll most likely need these plugins. In 31 | // some cases you'll need additional configuration — 32 | // consult the documentation for details: 33 | // https://github.com/rollup/rollup-plugin-commonjs 34 | resolve({ browser: true }), 35 | commonjs(), 36 | 37 | // Watch the `public` directory and refresh the 38 | // browser on changes when not in production 39 | !production && livereload('public'), 40 | 41 | // If we're building for production (npm run build 42 | // instead of npm run dev), minify 43 | production && terser() 44 | ], 45 | watch: { 46 | clearScreen: false 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/config/firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app' 2 | import 'firebase/firebase-firestore' 3 | import 'firebase/firebase-auth' 4 | import 'firebase/firebase-functions' 5 | import 'firebase/firebase-storage' 6 | import { firebaseConfig } from './settings' 7 | 8 | firebase.initializeApp(firebaseConfig) 9 | 10 | const Firestore = firebase.firestore() 11 | const Auth = firebase.auth() 12 | const Functions = firebase.app().functions('europe-west1') 13 | const Storage = firebase.storage() 14 | 15 | export { Firestore, Auth, Functions, Storage } 16 | -------------------------------------------------------------------------------- /src/config/settings.js: -------------------------------------------------------------------------------- 1 | const firebaseConfig = { 2 | apiKey: '', 3 | authDomain: '', 4 | databaseURL: '', 5 | projectId: '', 6 | storageBucket: '', 7 | messagingSenderId: '', 8 | appId: '' 9 | } 10 | 11 | export { firebaseConfig } 12 | -------------------------------------------------------------------------------- /src/lib/diacritics.js: -------------------------------------------------------------------------------- 1 | const replacementList = [ 2 | { 3 | base: ' ', 4 | chars: '\u00A0' 5 | }, 6 | { 7 | base: '0', 8 | chars: '\u07C0' 9 | }, 10 | { 11 | base: 'A', 12 | chars: 13 | '\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F' 14 | }, 15 | { 16 | base: 'AA', 17 | chars: '\uA732' 18 | }, 19 | { 20 | base: 'AE', 21 | chars: '\u00C6\u01FC\u01E2' 22 | }, 23 | { 24 | base: 'AO', 25 | chars: '\uA734' 26 | }, 27 | { 28 | base: 'AU', 29 | chars: '\uA736' 30 | }, 31 | { 32 | base: 'AV', 33 | chars: '\uA738\uA73A' 34 | }, 35 | { 36 | base: 'AY', 37 | chars: '\uA73C' 38 | }, 39 | { 40 | base: 'B', 41 | chars: '\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0181' 42 | }, 43 | { 44 | base: 'C', 45 | chars: '\u24b8\uff23\uA73E\u1E08\u0106\u0043\u0108\u010A\u010C\u00C7\u0187\u023B' 46 | }, 47 | { 48 | base: 'D', 49 | chars: '\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018A\u0189\u1D05\uA779' 50 | }, 51 | { 52 | base: 'Dh', 53 | chars: '\u00D0' 54 | }, 55 | { 56 | base: 'DZ', 57 | chars: '\u01F1\u01C4' 58 | }, 59 | { 60 | base: 'Dz', 61 | chars: '\u01F2\u01C5' 62 | }, 63 | { 64 | base: 'E', 65 | chars: 66 | '\u025B\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E\u1D07' 67 | }, 68 | { 69 | base: 'F', 70 | chars: '\uA77C\u24BB\uFF26\u1E1E\u0191\uA77B' 71 | }, 72 | { 73 | base: 'G', 74 | chars: '\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E\u0262' 75 | }, 76 | { 77 | base: 'H', 78 | chars: '\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D' 79 | }, 80 | { 81 | base: 'I', 82 | chars: '\u24BE\uFF29\xCC\xCD\xCE\u0128\u012A\u012C\u0130\xCF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197' 83 | }, 84 | { 85 | base: 'J', 86 | chars: '\u24BF\uFF2A\u0134\u0248\u0237' 87 | }, 88 | { 89 | base: 'K', 90 | chars: '\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2' 91 | }, 92 | { 93 | base: 'L', 94 | chars: '\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780' 95 | }, 96 | { 97 | base: 'LJ', 98 | chars: '\u01C7' 99 | }, 100 | { 101 | base: 'Lj', 102 | chars: '\u01C8' 103 | }, 104 | { 105 | base: 'M', 106 | chars: '\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C\u03FB' 107 | }, 108 | { 109 | base: 'N', 110 | chars: '\uA7A4\u0220\u24C3\uFF2E\u01F8\u0143\xD1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u019D\uA790\u1D0E' 111 | }, 112 | { 113 | base: 'NJ', 114 | chars: '\u01CA' 115 | }, 116 | { 117 | base: 'Nj', 118 | chars: '\u01CB' 119 | }, 120 | { 121 | base: 'O', 122 | chars: 123 | '\u24C4\uFF2F\xD2\xD3\xD4\u1ED2\u1ED0\u1ED6\u1ED4\xD5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\xD6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\xD8\u01FE\u0186\u019F\uA74A\uA74C' 124 | }, 125 | { 126 | base: 'OE', 127 | chars: '\u0152' 128 | }, 129 | { 130 | base: 'OI', 131 | chars: '\u01A2' 132 | }, 133 | { 134 | base: 'OO', 135 | chars: '\uA74E' 136 | }, 137 | { 138 | base: 'OU', 139 | chars: '\u0222' 140 | }, 141 | { 142 | base: 'P', 143 | chars: '\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754' 144 | }, 145 | { 146 | base: 'Q', 147 | chars: '\u24C6\uFF31\uA756\uA758\u024A' 148 | }, 149 | { 150 | base: 'R', 151 | chars: '\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782' 152 | }, 153 | { 154 | base: 'S', 155 | chars: '\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784' 156 | }, 157 | { 158 | base: 'T', 159 | chars: '\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786' 160 | }, 161 | { 162 | base: 'Th', 163 | chars: '\u00DE' 164 | }, 165 | { 166 | base: 'TZ', 167 | chars: '\uA728' 168 | }, 169 | { 170 | base: 'U', 171 | chars: 172 | '\u24CA\uFF35\xD9\xDA\xDB\u0168\u1E78\u016A\u1E7A\u016C\xDC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244' 173 | }, 174 | { 175 | base: 'V', 176 | chars: '\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245' 177 | }, 178 | { 179 | base: 'VY', 180 | chars: '\uA760' 181 | }, 182 | { 183 | base: 'W', 184 | chars: '\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72' 185 | }, 186 | { 187 | base: 'X', 188 | chars: '\u24CD\uFF38\u1E8A\u1E8C' 189 | }, 190 | { 191 | base: 'Y', 192 | chars: '\u24CE\uFF39\u1EF2\xDD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE' 193 | }, 194 | { 195 | base: 'Z', 196 | chars: '\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762' 197 | }, 198 | { 199 | base: 'a', 200 | chars: 201 | '\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250\u0251' 202 | }, 203 | { 204 | base: 'aa', 205 | chars: '\uA733' 206 | }, 207 | { 208 | base: 'ae', 209 | chars: '\u00E6\u01FD\u01E3' 210 | }, 211 | { 212 | base: 'ao', 213 | chars: '\uA735' 214 | }, 215 | { 216 | base: 'au', 217 | chars: '\uA737' 218 | }, 219 | { 220 | base: 'av', 221 | chars: '\uA739\uA73B' 222 | }, 223 | { 224 | base: 'ay', 225 | chars: '\uA73D' 226 | }, 227 | { 228 | base: 'b', 229 | chars: '\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253\u0182' 230 | }, 231 | { 232 | base: 'c', 233 | chars: '\uFF43\u24D2\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184' 234 | }, 235 | { 236 | base: 'd', 237 | chars: '\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\u018B\u13E7\u0501\uA7AA' 238 | }, 239 | { 240 | base: 'dh', 241 | chars: '\u00F0' 242 | }, 243 | { 244 | base: 'dz', 245 | chars: '\u01F3\u01C6' 246 | }, 247 | { 248 | base: 'e', 249 | chars: 250 | '\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u01DD' 251 | }, 252 | { 253 | base: 'f', 254 | chars: '\u24D5\uFF46\u1E1F\u0192' 255 | }, 256 | { 257 | base: 'ff', 258 | chars: '\uFB00' 259 | }, 260 | { 261 | base: 'fi', 262 | chars: '\uFB01' 263 | }, 264 | { 265 | base: 'fl', 266 | chars: '\uFB02' 267 | }, 268 | { 269 | base: 'ffi', 270 | chars: '\uFB03' 271 | }, 272 | { 273 | base: 'ffl', 274 | chars: '\uFB04' 275 | }, 276 | { 277 | base: 'g', 278 | chars: '\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\uA77F\u1D79' 279 | }, 280 | { 281 | base: 'h', 282 | chars: '\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265' 283 | }, 284 | { 285 | base: 'hv', 286 | chars: '\u0195' 287 | }, 288 | { 289 | base: 'i', 290 | chars: '\u24D8\uFF49\xEC\xED\xEE\u0129\u012B\u012D\xEF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131' 291 | }, 292 | { 293 | base: 'j', 294 | chars: '\u24D9\uFF4A\u0135\u01F0\u0249' 295 | }, 296 | { 297 | base: 'k', 298 | chars: '\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3' 299 | }, 300 | { 301 | base: 'l', 302 | chars: 303 | '\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747\u026D' 304 | }, 305 | { 306 | base: 'lj', 307 | chars: '\u01C9' 308 | }, 309 | { 310 | base: 'm', 311 | chars: '\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F' 312 | }, 313 | { 314 | base: 'n', 315 | chars: '\u24DD\uFF4E\u01F9\u0144\xF1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5\u043B\u0509' 316 | }, 317 | { 318 | base: 'nj', 319 | chars: '\u01CC' 320 | }, 321 | { 322 | base: 'o', 323 | chars: 324 | '\u24DE\uFF4F\xF2\xF3\xF4\u1ED3\u1ED1\u1ED7\u1ED5\xF5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\xF6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\xF8\u01FF\uA74B\uA74D\u0275\u0254\u1D11' 325 | }, 326 | { 327 | base: 'oe', 328 | chars: '\u0153' 329 | }, 330 | { 331 | base: 'oi', 332 | chars: '\u01A3' 333 | }, 334 | { 335 | base: 'oo', 336 | chars: '\uA74F' 337 | }, 338 | { 339 | base: 'ou', 340 | chars: '\u0223' 341 | }, 342 | { 343 | base: 'p', 344 | chars: '\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755\u03C1' 345 | }, 346 | { 347 | base: 'q', 348 | chars: '\u24E0\uFF51\u024B\uA757\uA759' 349 | }, 350 | { 351 | base: 'r', 352 | chars: '\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783' 353 | }, 354 | { 355 | base: 's', 356 | chars: '\u24E2\uFF53\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B\u0282' 357 | }, 358 | { 359 | base: 'ss', 360 | chars: '\xDF' 361 | }, 362 | { 363 | base: 't', 364 | chars: '\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787' 365 | }, 366 | { 367 | base: 'th', 368 | chars: '\u00FE' 369 | }, 370 | { 371 | base: 'tz', 372 | chars: '\uA729' 373 | }, 374 | { 375 | base: 'u', 376 | chars: 377 | '\u24E4\uFF55\xF9\xFA\xFB\u0169\u1E79\u016B\u1E7B\u016D\xFC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289' 378 | }, 379 | { 380 | base: 'v', 381 | chars: '\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C' 382 | }, 383 | { 384 | base: 'vy', 385 | chars: '\uA761' 386 | }, 387 | { 388 | base: 'w', 389 | chars: '\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73' 390 | }, 391 | { 392 | base: 'x', 393 | chars: '\u24E7\uFF58\u1E8B\u1E8D' 394 | }, 395 | { 396 | base: 'y', 397 | chars: '\u24E8\uFF59\u1EF3\xFD\u0177\u1EF9\u0233\u1E8F\xFF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF' 398 | }, 399 | { 400 | base: 'z', 401 | chars: '\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763' 402 | } 403 | ] 404 | 405 | const diacriticsMap = {} 406 | for (let i = 0; i < replacementList.length; i += 1) { 407 | let chars = replacementList[i].chars 408 | for (let j = 0; j < chars.length; j += 1) { 409 | diacriticsMap[chars[j]] = replacementList[i].base 410 | } 411 | } 412 | 413 | function removeDiacritics(str) { 414 | return str.replace(/./g, function(char) { 415 | return diacriticsMap[char] || char 416 | }) 417 | } 418 | 419 | export { removeDiacritics } 420 | -------------------------------------------------------------------------------- /src/lib/filter_results.js: -------------------------------------------------------------------------------- 1 | import { removeDiacritics } from './diacritics' 2 | 3 | const filterResults = (searchTerm, elements, fieldName) => { 4 | let lowerCaseTerm = removeDiacritics(searchTerm).toLowerCase() 5 | 6 | const search = () => { 7 | if (elements.length > 0 && searchTerm.length > 1) { 8 | return elements.filter(result => { 9 | const normalisedName = removeDiacritics(result[fieldName]).toLowerCase() 10 | return normalisedName.includes(lowerCaseTerm) 11 | }) 12 | } else { 13 | return elements 14 | } 15 | } 16 | 17 | return { 18 | search 19 | } 20 | } 21 | 22 | export { filterResults } 23 | -------------------------------------------------------------------------------- /src/lib/routes/protected.js: -------------------------------------------------------------------------------- 1 | import AdminLayout from '../../views/admin/layout/index.svelte' 2 | import DashboardIndex from '../../views/admin/dashboard/index.svelte' 3 | import EmployeesIndex from '../../views/admin/employees/index.svelte' 4 | import EmployeesNew from '../../views/admin/employees/new.svelte' 5 | import EmployeesEdit from '../../views/admin/employees/edit.svelte' 6 | import EmployeesShow from '../../views/admin/employees/show.svelte' 7 | import EmployeesLayout from '../../views/admin/employees/layout.svelte' 8 | import TeamsIndex from '../../views/admin/teams/index.svelte' 9 | import TeamsShow from '../../views/admin/teams/show.svelte' 10 | 11 | const protectedRoutes = [ 12 | { 13 | name: 'admin', 14 | component: AdminLayout, 15 | nestedRoutes: [ 16 | { name: 'index', component: DashboardIndex }, 17 | { 18 | name: 'employees', 19 | component: EmployeesLayout, 20 | nestedRoutes: [ 21 | { name: 'index', component: EmployeesIndex }, 22 | { name: 'show/:id', component: EmployeesShow }, 23 | { name: 'new', component: EmployeesNew }, 24 | { name: 'edit/:id', component: EmployeesEdit } 25 | ] 26 | }, 27 | { 28 | name: 'teams', 29 | component: TeamsIndex 30 | }, 31 | { name: 'teams/show/:id', component: TeamsShow } 32 | ] 33 | } 34 | ] 35 | 36 | export { protectedRoutes } 37 | -------------------------------------------------------------------------------- /src/lib/routes/public.js: -------------------------------------------------------------------------------- 1 | import Login from '../../views/public/login/index.svelte' 2 | import Signup from '../../views/public/signup/index.svelte' 3 | import PublicIndex from '../../views/public/home/index.svelte' 4 | import PublicLayout from '../../views/public/layout/index.svelte' 5 | import NotFound from '../../views/404.svelte' 6 | 7 | const publicRoutes = [ 8 | { 9 | name: '/', 10 | component: PublicIndex, 11 | }, 12 | { name: 'login', component: Login, layout: PublicLayout }, 13 | { name: 'signup', component: Signup, layout: PublicLayout }, 14 | { name: '404', component: NotFound, layout: PublicLayout }, 15 | ] 16 | 17 | export { publicRoutes } 18 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | import './middleware/users/auth' 3 | 4 | const app = new App({ 5 | target: document.body, 6 | }) 7 | 8 | export default app 9 | -------------------------------------------------------------------------------- /src/middleware/database/employees.js: -------------------------------------------------------------------------------- 1 | import { FirebaseEmployees } from './index' 2 | 3 | const employeesDb = () => { 4 | const add = _employeeInfo => { 5 | return false 6 | // return FirebaseEmployees.add(employeeInfo) 7 | } 8 | 9 | // TODO: Move to server function to check permission 10 | const archive = employeeId => { 11 | return FirebaseEmployees.doc(employeeId).set({ active: false }, { merge: true }) 12 | } 13 | 14 | const update = (employeeId, employeeInfo) => { 15 | delete employeeInfo.email 16 | delete employeeInfo.password 17 | return FirebaseEmployees.doc(employeeId).update(employeeInfo) 18 | } 19 | 20 | const findOne = employeeId => { 21 | return FirebaseEmployees.doc(employeeId).get() 22 | } 23 | 24 | const findByUserId = userId => { 25 | return FirebaseEmployees.where('userId', '==', userId).get() 26 | } 27 | 28 | const findAll = ({ companyId, teamId, active = true }) => { 29 | let query = FirebaseEmployees.where('companyId', '==', companyId).where('active', '==', active) 30 | if (teamId) { 31 | query = query.where('teamId', '==', teamId) 32 | } 33 | 34 | return query.orderBy('name') 35 | } 36 | 37 | const toggleStatus = employee => { 38 | if (!employee.active) { 39 | return unarchive(employee.id) 40 | } else { 41 | return archive(employee.id) 42 | } 43 | } 44 | 45 | // TODO: Move to server function to check permissions 46 | const unarchive = employeeId => { 47 | return FirebaseEmployees.doc(employeeId).set({ active: true }, { merge: true }) 48 | } 49 | 50 | return Object.freeze({ 51 | add, 52 | archive, 53 | update, 54 | findOne, 55 | findAll, 56 | findByUserId, 57 | toggleStatus, 58 | unarchive 59 | }) 60 | } 61 | 62 | const Employees = employeesDb() 63 | 64 | export { Employees } 65 | -------------------------------------------------------------------------------- /src/middleware/database/firebase_results.js: -------------------------------------------------------------------------------- 1 | const firebaseResults = () => { 2 | const map = docs => { 3 | let newResults = [] 4 | docs.forEach(doc => { 5 | let newResult = doc.data() 6 | newResult.id = doc.id 7 | newResults.push(newResult) 8 | }) 9 | return newResults 10 | } 11 | 12 | return Object.freeze({ 13 | map 14 | }) 15 | } 16 | 17 | const FirebaseResults = firebaseResults() 18 | 19 | export default FirebaseResults 20 | -------------------------------------------------------------------------------- /src/middleware/database/index.js: -------------------------------------------------------------------------------- 1 | import { Firestore } from '../../config/firebase' 2 | import FirebaseResults from './firebase_results' 3 | 4 | const FirebaseEmployees = Firestore.collection('employees') 5 | const FirebaseTeams = Firestore.collection('teams') 6 | 7 | export { FirebaseEmployees, FirebaseResults, FirebaseTeams } 8 | -------------------------------------------------------------------------------- /src/middleware/database/teams.js: -------------------------------------------------------------------------------- 1 | import { FirebaseTeams } from './index' 2 | 3 | const teamsDb = () => { 4 | const add = teamInfo => { 5 | return FirebaseTeams.add(teamInfo) 6 | } 7 | 8 | const update = (teamId, teamInfo) => { 9 | return FirebaseTeams.doc(teamId).update(teamInfo) 10 | } 11 | 12 | const remove = teamId => { 13 | return FirebaseTeams.doc(teamId).delete() 14 | } 15 | 16 | const findOne = teamId => { 17 | return FirebaseTeams.doc(teamId).get() 18 | } 19 | 20 | const findAll = companyId => { 21 | return FirebaseTeams.where('companyId', '==', companyId).orderBy('name') 22 | } 23 | 24 | return Object.freeze({ 25 | add, 26 | update, 27 | findOne, 28 | findAll, 29 | remove 30 | }) 31 | } 32 | 33 | const Teams = teamsDb() 34 | 35 | export { Teams } 36 | -------------------------------------------------------------------------------- /src/middleware/employees/crud.js: -------------------------------------------------------------------------------- 1 | import { notificationMessage } from '../../stores/notification_message.js' 2 | 3 | const addEmployee = employeeInfo => { 4 | Employees.add(employeeInfo).then( 5 | notificationMessage.set({ 6 | message: 'Employee created successfully.', 7 | type: 'success-toast' 8 | }) 9 | ) 10 | } 11 | 12 | const editEmployee = (employeeId, employeeInfo) => { 13 | Employees.update(employeeId, employeeInfo).then( 14 | notificationMessage.set({ 15 | message: 'Employee updated successfully.', 16 | type: 'success-toast' 17 | }) 18 | ) 19 | } 20 | 21 | export { addEmployee, editEmployee } 22 | -------------------------------------------------------------------------------- /src/middleware/users/auth.js: -------------------------------------------------------------------------------- 1 | import { currentUser } from '../../stores/current_user' 2 | import { Auth } from '../../config/firebase' 3 | import { Employees } from '../database/employees' 4 | 5 | Auth.onAuthStateChanged(() => { 6 | if (Auth.currentUser) { 7 | const userInfo = { 8 | email: Auth.currentUser.email, 9 | id: Auth.currentUser.uid, 10 | phoneNumber: Auth.currentUser.phoneNumber, 11 | photoUrl: Auth.currentUser.photoUrl 12 | } 13 | 14 | Employees.findOne(Auth.currentUser.uid).then(doc => { 15 | userInfo.employee = doc.data() 16 | userInfo.employee.id = doc.id 17 | userInfo.displayName = userInfo.employee.name 18 | 19 | Auth.currentUser.getIdTokenResult().then(idToken => { 20 | userInfo.companyId = idToken.claims.companyId 21 | userInfo.isAdmin = idToken.claims.role === 'admin' || idToken.claims.role === 'superAdmin' 22 | 23 | currentUser.set(userInfo) 24 | }) 25 | }) 26 | } else { 27 | currentUser.set({ id: 0 }) 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import { publicRoutes } from './lib/routes/public' 2 | import { protectedRoutes } from './lib/routes/protected' 3 | 4 | const routes = [...publicRoutes, ...protectedRoutes] 5 | 6 | export { routes } 7 | -------------------------------------------------------------------------------- /src/stores/current_user.js: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | const userInfo = writable({}); 4 | 5 | const setUser = user => { 6 | userInfo.set(user); 7 | }; 8 | 9 | const removeUser = () => { 10 | userInfo.set({}); 11 | }; 12 | 13 | const currentUser = { 14 | subscribe: userInfo.subscribe, 15 | set: setUser, 16 | remove: removeUser 17 | }; 18 | 19 | export { currentUser }; 20 | -------------------------------------------------------------------------------- /src/stores/notification_message.js: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | 3 | export const notificationMessage = writable({}) 4 | -------------------------------------------------------------------------------- /src/views/404.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 |
12 |

Page not found!

13 |

I've searched everywhere but it wasn't there. I'm as puzzled as you are!

14 |
15 | Return to the 16 | homepage 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/views/admin/dashboard/index.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Dashboard

4 |
5 | 6 |
7 |
8 |
9 | Card Title 10 |

11 | I am a very simple card. I am good at containing small bits of information. I am convenient because I require 12 | little markup to use effectively. 13 |

14 |
15 | 19 |
20 |
21 |
22 |
23 |
24 | 25 | Card Title 26 |
27 |
28 |

29 | I am a very simple card. I am good at containing small bits of information. I am convenient because I require 30 | little markup to use effectively. 31 |

32 |
33 |
34 | This is a link 35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |

I am a very simple card. I am good at containing small bits of information.

46 |
47 |
48 | This is a link 49 |
50 |
51 |
52 |
53 |
54 | -------------------------------------------------------------------------------- /src/views/admin/employees/edit.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 | {#if loading} 48 | 49 | {:else} 50 |
51 |
52 |
53 |

{employee.name}

54 |
55 |
56 |

{employee.email}

57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {/if} 66 | -------------------------------------------------------------------------------- /src/views/admin/employees/form.svelte: -------------------------------------------------------------------------------- 1 | 127 | 128 | 129 | 136 | {#if !employee.id} 137 | 143 | 149 | {/if} 150 | 11 | {label} 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/components/forms/date_input.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
40 | {#if icon} 41 | {icon} 42 | {/if} 43 | (error = false)} 45 | type="text" 46 | name={inputName} 47 | {id} 48 | class="datepicker" 49 | class:invalid={error} 50 | autofocus={isFocused} 51 | on:blur /> 52 | 53 | 54 |
55 | -------------------------------------------------------------------------------- /src/views/components/forms/date_range.svelte: -------------------------------------------------------------------------------- 1 | 73 | 74 |
75 |
76 | 85 |
86 |
87 | 96 |
97 |
98 | -------------------------------------------------------------------------------- /src/views/components/forms/email_input.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | {#if icon} 14 | {icon} 15 | {/if} 16 | (error = false)} 19 | type="email" 20 | name={inputName} 21 | {id} 22 | class="validate" 23 | class:invalid={error} 24 | autofocus={isFocused} 25 | on:blur /> 26 | 27 | 28 |
29 | -------------------------------------------------------------------------------- /src/views/components/forms/number_input.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 | {#if icon} 21 | {icon} 22 | {/if} 23 | { 26 | const number = event.target.value 27 | if (number.length > maxlength) { 28 | value = number.slice(0, maxlength) 29 | } 30 | error = false 31 | }} 32 | type="number" 33 | name={inputName} 34 | {id} 35 | class:invalid={error} 36 | autofocus={isFocused} 37 | on:blur /> 38 | 39 | 40 |
41 | -------------------------------------------------------------------------------- /src/views/components/forms/password_input.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {#if icon} 15 | {icon} 16 | {/if} 17 | (error = false)} 20 | type="password" 21 | name={inputName} 22 | {id} 23 | class:invalid={error} 24 | autofocus={isFocused} 25 | on:blur /> 26 | 27 | {helpText} 28 |
29 | -------------------------------------------------------------------------------- /src/views/components/forms/radio_input.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 14 |
15 | -------------------------------------------------------------------------------- /src/views/components/forms/select.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 30 | 31 | {helpText} 32 |
33 | -------------------------------------------------------------------------------- /src/views/components/forms/text_input.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | {#if icon} 20 | {icon} 21 | {/if} 22 | (error = false)} 25 | type="text" 26 | name={inputName} 27 | {id} 28 | class:invalid={error} 29 | autofocus={isFocused} 30 | on:blur /> 31 | 32 | 33 |
34 | -------------------------------------------------------------------------------- /src/views/components/forms/textarea.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | {#if icon} 14 | {icon} 15 | {/if} 16 |