├── .browserslistrc ├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── README.md ├── angular.json ├── db.sql ├── karma.conf.js ├── netlify.toml ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── guards │ │ ├── auth.guard.spec.ts │ │ └── auth.guard.ts │ ├── inside │ │ ├── footer │ │ │ ├── footer.component.html │ │ │ ├── footer.component.scss │ │ │ ├── footer.component.spec.ts │ │ │ └── footer.component.ts │ │ ├── header │ │ │ ├── header.component.html │ │ │ ├── header.component.scss │ │ │ ├── header.component.spec.ts │ │ │ └── header.component.ts │ │ ├── inside.module.ts │ │ ├── settings │ │ │ ├── settings.component.html │ │ │ ├── settings.component.scss │ │ │ ├── settings.component.spec.ts │ │ │ └── settings.component.ts │ │ ├── ui │ │ │ ├── ui.component.html │ │ │ ├── ui.component.scss │ │ │ ├── ui.component.spec.ts │ │ │ └── ui.component.ts │ │ ├── votings-details │ │ │ ├── votings-details.component.html │ │ │ ├── votings-details.component.scss │ │ │ ├── votings-details.component.spec.ts │ │ │ └── votings-details.component.ts │ │ └── votings-list │ │ │ ├── votings-list.component.html │ │ │ ├── votings-list.component.scss │ │ │ ├── votings-list.component.spec.ts │ │ │ └── votings-list.component.ts │ ├── interfaces │ │ ├── currentUser.ts │ │ ├── index.ts │ │ ├── votingOption.ts │ │ └── votings.ts │ ├── login │ │ ├── login.component.html │ │ ├── login.component.scss │ │ ├── login.component.spec.ts │ │ └── login.component.ts │ ├── services │ │ ├── auth.service.spec.ts │ │ ├── auth.service.ts │ │ ├── data.service.spec.ts │ │ └── data.service.ts │ └── voting │ │ ├── voting.component.html │ │ ├── voting.component.scss │ │ ├── voting.component.spec.ts │ │ └── voting.component.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tailwind.config.js ├── todo.md ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | package-lock.json 44 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "pwa-chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Votetastic, make your voting fantastic 2 | 3 | This is a project led by Simon Grimm. 4 | 5 | He is actively working on this project during livestreams on 6 | his [YouTube channel](https://www.youtube.com/c/SimonGrimmDev), 7 | as well as his [Twitch Channel](https://www.twitch.tv/captainionic), where he interacts with the community in different 8 | ways. 9 | 10 | Simon is streaming every Thursday 15:00 (3p.m.) CET (Central European Time). Come and join us in helping him develop 11 | this app! 12 | 13 | ## Overall description 14 | 15 | We (the community and Simon) are making an app where everyone can vote for future projects, 16 | that Simon should cover in his videos and livestreams. 17 | 18 | ## Technologies 19 | 20 | We are using Angular 14 as our frontend Framework, together with Tailwind(-UI). 21 | We are using Supabase as our BaaS where we store all data. 22 | 23 | ## Implementation 24 | 25 | The work is uploaded to [Git](https://github.com/saimon24/Votetastic) and the WIP is deployed 26 | on [Netlify](https://classy-kitsune-3f9088.netlify.app/). 27 | You can use the dummy account on the app to test it out yourself! 28 | 29 | ![image](https://user-images.githubusercontent.com/40073861/184167833-9351f3c4-86ac-45ac-bc57-d9932e6126bb.png) 30 | 31 | ## Contribution 32 | 33 | Fork the code and submit your contribution as a Pull Request. Get your name out there! 34 | 35 | Anyone can contribute and help out! 36 | 37 | You can find a list of open tasks in the ```todo.md``` file in the root Folder. 38 | 39 | ## Socials 40 | 41 | - [YouTube](https://www.youtube.com/c/SimonGrimmDev) 42 | - [Twitch](https://www.twitch.tv/captainionic) 43 | - [Git](https://github.com/saimon24) 44 | - [Git (votetastic Repo)](https://github.com/saimon24/Votetastic) 45 | - [Devdactic](http://devdactic.com/devblog/) 46 | - [Ionic Academy](https://ionicacademy.com/) 47 | 48 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "votingApp": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/voting-app", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "inlineStyleLanguage": "scss", 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "src/styles.scss", 32 | "node_modules/ngx-toastr/toastr.css" 33 | ], 34 | "scripts": [] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "budgets": [ 39 | { 40 | "type": "initial", 41 | "maximumWarning": "500kb", 42 | "maximumError": "1mb" 43 | }, 44 | { 45 | "type": "anyComponentStyle", 46 | "maximumWarning": "2kb", 47 | "maximumError": "4kb" 48 | } 49 | ], 50 | "fileReplacements": [ 51 | { 52 | "replace": "src/environments/environment.ts", 53 | "with": "src/environments/environment.prod.ts" 54 | } 55 | ], 56 | "outputHashing": "all" 57 | }, 58 | "development": { 59 | "buildOptimizer": false, 60 | "optimization": false, 61 | "vendorChunk": true, 62 | "extractLicenses": false, 63 | "sourceMap": true, 64 | "namedChunks": true 65 | } 66 | }, 67 | "defaultConfiguration": "production" 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "votingApp:build:production" 74 | }, 75 | "development": { 76 | "browserTarget": "votingApp:build:development" 77 | } 78 | }, 79 | "defaultConfiguration": "development" 80 | }, 81 | "extract-i18n": { 82 | "builder": "@angular-devkit/build-angular:extract-i18n", 83 | "options": { 84 | "browserTarget": "votingApp:build" 85 | } 86 | }, 87 | "test": { 88 | "builder": "@angular-devkit/build-angular:karma", 89 | "options": { 90 | "main": "src/test.ts", 91 | "polyfills": "src/polyfills.ts", 92 | "tsConfig": "tsconfig.spec.json", 93 | "karmaConfig": "karma.conf.js", 94 | "inlineStyleLanguage": "scss", 95 | "assets": [ 96 | "src/favicon.ico", 97 | "src/assets" 98 | ], 99 | "styles": [ 100 | "src/styles.scss" 101 | ], 102 | "scripts": [] 103 | } 104 | } 105 | } 106 | } 107 | }, 108 | "cli": { 109 | "analytics": false 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /db.sql: -------------------------------------------------------------------------------- 1 | -- USERS 2 | create table users ( 3 | id uuid not null primary key, 4 | email text 5 | ); 6 | 7 | create or replace function public.handle_new_user() 8 | returns trigger as $$ 9 | begin 10 | insert into public.users (id, email) 11 | values (new.id, new.email); 12 | return new; 13 | end; 14 | $$ language plpgsql security definer; 15 | 16 | create trigger on_auth_user_created 17 | after insert on auth.users 18 | for each row execute procedure public.handle_new_user(); 19 | 20 | 21 | -- votings and voting_options 22 | 23 | create table votings ( 24 | id bigint generated by default as identity primary key, 25 | title text default 'New voting', 26 | voting_question text check (char_length(voting_question) > 0), 27 | description text check (char_length(description) > 0), 28 | creator_id uuid references auth.users ON DELETE CASCADE not null default auth.uid(), 29 | created_at timestamp with time zone default timezone('utc'::text, now()) not null, 30 | public boolean default false 31 | ); 32 | 33 | create table voting_options ( 34 | id bigint generated by default as identity primary key, 35 | voting_id bigint references votings ON DELETE CASCADE not null, 36 | title text check (char_length(title) > 0), 37 | creator_id uuid references auth.users ON DELETE CASCADE not null default auth.uid(), 38 | votes int default 0 39 | ); 40 | 41 | 42 | -- votings row level security 43 | alter table votings enable row level security; 44 | 45 | create policy "Users can add votings" on votings for 46 | insert to authenticated with check (true); 47 | 48 | create policy "Everyone can view votings" on votings for 49 | select using (true); 50 | 51 | create policy "Users can update their votings" on votings for 52 | update using (auth.uid() = creator_id); 53 | 54 | 55 | 56 | -- voting_options row level security 57 | alter table voting_options enable row level security; 58 | 59 | create policy "Users can add options" on voting_options for 60 | insert to authenticated with check (true); 61 | 62 | create policy "Everyone can view options" on voting_options for 63 | select using (true); 64 | 65 | create policy "Users can delete their options" on voting_options for 66 | delete using (auth.uid() = creator_id); 67 | 68 | 69 | create policy "Users can update their options" on voting_options for 70 | update using (auth.uid() = creator_id); 71 | 72 | 73 | 74 | 75 | create function increment (row_id int) 76 | returns void as 77 | $$ 78 | update voting_options 79 | set votes = votes + 1 80 | where id = row_id 81 | $$ 82 | language sql volatile; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/voting-app'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist/voting-app" 3 | command = "ng build" 4 | [[redirects]] 5 | from = "/*" 6 | to = "/index.html" 7 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voting-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^14.0.0", 14 | "@angular/cdk": "^14.0.0", 15 | "@angular/common": "^14.0.0", 16 | "@angular/compiler": "^14.0.0", 17 | "@angular/core": "^14.0.0", 18 | "@angular/forms": "^14.0.0", 19 | "@angular/platform-browser": "^14.0.0", 20 | "@angular/platform-browser-dynamic": "^14.0.0", 21 | "@angular/router": "^14.0.0", 22 | "@supabase/supabase-js": "^1.35.4", 23 | "@sweetalert2/ngx-sweetalert2": "^11.0.0", 24 | "@tailwindcss/forms": "^0.5.2", 25 | "@tailwindcss/line-clamp": "^0.4.0", 26 | "chart.js": "^3.6.0", 27 | "ng2-charts": "^4.0.0", 28 | "ngx-bootstrap": "^9.0.0", 29 | "ngx-toastr": "^15.0.0", 30 | "prettier": "^2.7.1", 31 | "rxjs": "~7.5.0", 32 | "sweetalert2": "^11.4.26", 33 | "tslib": "^2.3.0", 34 | "zone.js": "~0.11.4" 35 | }, 36 | "devDependencies": { 37 | "@angular-devkit/build-angular": "^14.0.5", 38 | "@angular/cli": "~14.0.5", 39 | "@angular/compiler-cli": "^14.0.0", 40 | "@types/jasmine": "~4.0.0", 41 | "autoprefixer": "^10.4.7", 42 | "jasmine-core": "~4.1.0", 43 | "karma": "~6.3.0", 44 | "karma-chrome-launcher": "~3.1.0", 45 | "karma-coverage": "~2.2.0", 46 | "karma-jasmine": "~5.0.0", 47 | "karma-jasmine-html-reporter": "~1.7.0", 48 | "postcss": "^8.4.14", 49 | "tailwindcss": "^3.1.6", 50 | "typescript": "~4.7.2" 51 | } 52 | } -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { VotingComponent } from './voting/voting.component'; 2 | import { AuthGuard } from './guards/auth.guard'; 3 | import { LoginComponent } from './login/login.component'; 4 | import { NgModule } from '@angular/core'; 5 | import { RouterModule, Routes } from '@angular/router'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: LoginComponent, 11 | }, 12 | { 13 | path: 'app', 14 | loadChildren: () => 15 | import('./inside/inside.module').then((m) => m.InsideModule), 16 | canActivate: [AuthGuard], 17 | }, 18 | { 19 | path: 'voting/:id', 20 | component: VotingComponent, 21 | }, 22 | ]; 23 | 24 | @NgModule({ 25 | imports: [RouterModule.forRoot(routes)], 26 | exports: [RouterModule], 27 | }) 28 | export class AppRoutingModule {} 29 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saimon24/Votetastic/0c623b150d9471417e14b5a72cc9d65e2575ee66/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'votingApp'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('votingApp'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement as HTMLElement; 33 | expect(compiled.querySelector('.content span')?.textContent).toContain('votingApp app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'votingApp'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ReactiveFormsModule } from '@angular/forms'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './app.component'; 7 | import { LoginComponent } from './login/login.component'; 8 | import { ToastrModule } from 'ngx-toastr'; 9 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 10 | import { VotingComponent } from './voting/voting.component'; 11 | import { NgChartsModule } from 'ng2-charts'; 12 | 13 | @NgModule({ 14 | declarations: [AppComponent, LoginComponent, VotingComponent], 15 | imports: [ 16 | BrowserModule, 17 | BrowserAnimationsModule, 18 | AppRoutingModule, 19 | ReactiveFormsModule, 20 | ToastrModule.forRoot(), 21 | NgChartsModule, 22 | ], 23 | providers: [], 24 | bootstrap: [AppComponent], 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /src/app/guards/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthGuard } from './auth.guard'; 4 | 5 | describe('AuthGuard', () => { 6 | let guard: AuthGuard; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | guard = TestBed.inject(AuthGuard); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(guard).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from '../services/auth.service'; 2 | import { Injectable } from '@angular/core'; 3 | import { CanActivate, UrlTree, Router } from '@angular/router'; 4 | import { Observable } from 'rxjs'; 5 | import { filter, map, take } from 'rxjs/operators'; 6 | import { CurrentUser } from '../interfaces'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class AuthGuard implements CanActivate { 12 | constructor(private authService: AuthService, private router: Router) {} 13 | 14 | canActivate(): 15 | | Observable 16 | | Promise 17 | | boolean 18 | | UrlTree { 19 | return this.authService.getCurrentUser().pipe( 20 | filter((val) => val !== null), 21 | take(1), 22 | map((isAuthenticated: CurrentUser) => { 23 | if (isAuthenticated) { 24 | return true; 25 | } else { 26 | return this.router.createUrlTree(['/']); 27 | } 28 | }) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/inside/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 2 | 92 | -------------------------------------------------------------------------------- /src/app/inside/footer/footer.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saimon24/Votetastic/0c623b150d9471417e14b5a72cc9d65e2575ee66/src/app/inside/footer/footer.component.scss -------------------------------------------------------------------------------- /src/app/inside/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FooterComponent } from './footer.component'; 4 | 5 | describe('FooterComponent', () => { 6 | let component: FooterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ FooterComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(FooterComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/inside/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.scss'] 7 | }) 8 | export class FooterComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/inside/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 52 |
53 | -------------------------------------------------------------------------------- /src/app/inside/header/header.component.scss: -------------------------------------------------------------------------------- 1 | .active-link { 2 | @apply underline; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/inside/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent', () => { 6 | let component: HeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HeaderComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(HeaderComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/inside/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '@angular/router'; 2 | import { AuthService } from './../../services/auth.service'; 3 | import { Component, OnInit } from '@angular/core'; 4 | import { ToastrService } from "ngx-toastr"; 5 | 6 | @Component({ 7 | selector: 'app-header', 8 | templateUrl: './header.component.html', 9 | styleUrls: ['./header.component.scss'], 10 | }) 11 | export class HeaderComponent implements OnInit { 12 | constructor(private authService: AuthService, private router: Router, private toaster: ToastrService) {} 13 | 14 | ngOnInit(): void {} 15 | 16 | async signOut() { 17 | await this.authService.logout(); 18 | this.router.navigateByUrl('/') 19 | .then(() => this.toaster.warning('You signed out')) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/inside/inside.module.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveFormsModule } from '@angular/forms'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { VotingsListComponent } from './votings-list/votings-list.component'; 6 | import { VotingsDetailsComponent } from './votings-details/votings-details.component'; 7 | import { UiComponent } from './ui/ui.component'; 8 | import { HeaderComponent } from './header/header.component'; 9 | import { FooterComponent } from './footer/footer.component'; 10 | import { SettingsComponent } from './settings/settings.component'; 11 | import { SweetAlert2Module } from '@sweetalert2/ngx-sweetalert2'; 12 | import { PopoverModule } from 'ngx-bootstrap/popover'; 13 | import { TooltipModule } from 'ngx-bootstrap/tooltip'; 14 | 15 | const routes: Routes = [ 16 | { 17 | path: '', 18 | component: UiComponent, 19 | children: [ 20 | { 21 | path: '', 22 | component: VotingsListComponent, 23 | }, 24 | { 25 | path: 'settings', 26 | component: SettingsComponent, 27 | }, 28 | { 29 | path: ':id', 30 | component: VotingsDetailsComponent, 31 | }, 32 | ], 33 | }, 34 | ]; 35 | 36 | @NgModule({ 37 | declarations: [ 38 | VotingsListComponent, 39 | VotingsDetailsComponent, 40 | UiComponent, 41 | HeaderComponent, 42 | FooterComponent, 43 | SettingsComponent, 44 | ], 45 | imports: [ 46 | CommonModule, 47 | RouterModule.forChild(routes), 48 | ReactiveFormsModule, 49 | SweetAlert2Module.forRoot(), 50 | PopoverModule, 51 | TooltipModule, 52 | ], 53 | exports: [], 54 | }) 55 | export class InsideModule {} 56 | -------------------------------------------------------------------------------- /src/app/inside/settings/settings.component.html: -------------------------------------------------------------------------------- 1 |

settings works!

2 | -------------------------------------------------------------------------------- /src/app/inside/settings/settings.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saimon24/Votetastic/0c623b150d9471417e14b5a72cc9d65e2575ee66/src/app/inside/settings/settings.component.scss -------------------------------------------------------------------------------- /src/app/inside/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsComponent } from './settings.component'; 4 | 5 | describe('SettingsComponent', () => { 6 | let component: SettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ SettingsComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SettingsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/inside/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-settings', 5 | templateUrl: './settings.component.html', 6 | styleUrls: ['./settings.component.scss'] 7 | }) 8 | export class SettingsComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/inside/ui/ui.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /src/app/inside/ui/ui.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saimon24/Votetastic/0c623b150d9471417e14b5a72cc9d65e2575ee66/src/app/inside/ui/ui.component.scss -------------------------------------------------------------------------------- /src/app/inside/ui/ui.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UiComponent } from './ui.component'; 4 | 5 | describe('UiComponent', () => { 6 | let component: UiComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ UiComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(UiComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/inside/ui/ui.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-ui', 5 | templateUrl: './ui.component.html', 6 | styleUrls: ['./ui.component.scss'] 7 | }) 8 | export class UiComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/inside/votings-details/votings-details.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | Share this Voting 6 |

7 |
8 |
9 |
10 | 11 |
12 | 16 | 28 |
29 |
30 | 40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 |

48 | Voting Information 49 |

50 |

51 | Setup your voting information. 52 |

53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | 66 | 73 |
74 | 75 |
76 | 81 | 88 |
89 | 90 |
91 | 96 | 105 |
106 | 107 |
108 | 113 | 120 |
121 |
122 |
123 |
124 | 139 | 140 | 146 |
147 |
148 |
149 |
150 |
151 |
152 | 153 | 158 | 159 |
160 |
161 |
162 |
163 |

164 | Voting Options 165 |

166 |

Define your voting options

167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 | Add your voting options 176 |
177 |
178 |
179 | 185 | 205 |
206 |
207 |
208 |
209 |
210 |
211 | 218 | 219 | 226 |
227 |
228 |
229 |
230 |
231 | -------------------------------------------------------------------------------- /src/app/inside/votings-details/votings-details.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saimon24/Votetastic/0c623b150d9471417e14b5a72cc9d65e2575ee66/src/app/inside/votings-details/votings-details.component.scss -------------------------------------------------------------------------------- /src/app/inside/votings-details/votings-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { VotingsDetailsComponent } from './votings-details.component'; 4 | 5 | describe('VotingsDetailsComponent', () => { 6 | let component: VotingsDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ VotingsDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(VotingsDetailsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/inside/votings-details/votings-details.component.ts: -------------------------------------------------------------------------------- 1 | import { zip } from 'rxjs'; 2 | import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { DataService } from '../../services/data.service'; 4 | import { Component, OnInit } from '@angular/core'; 5 | import { ActivatedRoute, Router } from '@angular/router'; 6 | import { ToastrService } from 'ngx-toastr'; 7 | import { Voting } from 'src/app/interfaces'; 8 | import { Clipboard } from '@angular/cdk/clipboard'; 9 | 10 | @Component({ 11 | selector: 'app-votings-details', 12 | templateUrl: './votings-details.component.html', 13 | styleUrls: ['./votings-details.component.scss'], 14 | }) 15 | export class VotingsDetailsComponent implements OnInit { 16 | voting: Voting = null!; 17 | form: FormGroup; 18 | formOptions: FormGroup; 19 | 20 | constructor( 21 | private route: ActivatedRoute, 22 | private dataService: DataService, 23 | private fb: FormBuilder, 24 | private toaster: ToastrService, 25 | private router: Router, 26 | private clipboard: Clipboard 27 | ) { 28 | this.form = this.fb.group({ 29 | voting_question: ['', Validators.required], 30 | title: ['', Validators.required], 31 | description: [''], 32 | public: [false], 33 | }); 34 | 35 | this.formOptions = this.fb.group({ 36 | options: this.fb.array([]), 37 | }); 38 | } 39 | 40 | get options(): FormArray { 41 | return this.formOptions.controls['options'] as FormArray; 42 | } 43 | 44 | async ngOnInit() { 45 | const id = this.route.snapshot.paramMap.get('id'); 46 | 47 | if (id) { 48 | this.voting = await (await this.dataService.getVotingDetails(+id)).data; 49 | const options = await (await this.dataService.getVotingOptions(+id)).data; 50 | options?.map((item) => { 51 | const option = this.fb.group({ 52 | title: [item.title, Validators.required], 53 | id: item.id, 54 | }); 55 | this.options.push(option); 56 | }); 57 | 58 | this.form.patchValue(this.voting); 59 | } 60 | } 61 | 62 | async updateVoting() { 63 | await this.dataService.updateVotingDetails(this.form.value, this.voting.id); 64 | this.toaster.success('Voting updated!'); 65 | } 66 | 67 | async deleteVoting() { 68 | await this.dataService.deleteVoting(this.voting.id); 69 | this.toaster.info('Voting deleted!'); 70 | this.router.navigateByUrl('/app'); 71 | } 72 | 73 | addOption() { 74 | const option = this.fb.group({ 75 | title: ['', Validators.required], 76 | id: null, 77 | voting_id: this.voting.id, 78 | }); 79 | 80 | this.options.push(option); 81 | } 82 | 83 | async deleteOption(index: number) { 84 | const control = this.options.at(index); 85 | const id = control.value.id; 86 | await this.dataService.deleteVotingOption(id); 87 | this.options.removeAt(index); 88 | } 89 | 90 | saveOptions() { 91 | console.log('SAVE: ', this.formOptions.value); 92 | // TODO: Add loading 93 | 94 | const obs = []; 95 | for (let entry of this.formOptions.value.options) { 96 | if (!entry.id) { 97 | const newObs = this.dataService.addVotingOption(entry); 98 | obs.push(newObs); 99 | } else { 100 | const newObs = this.dataService.updateVotingOption(entry); 101 | obs.push(newObs); 102 | } 103 | } 104 | 105 | zip(obs).subscribe((res) => { 106 | console.log('AFTER ADD: ', res); 107 | this.toaster.success('Voting updated!'); 108 | }); 109 | } 110 | 111 | copyUrlToClipboard() { 112 | const shareableUrl = `${window.location.origin}/voting/${this.voting.id}`; 113 | console.log(shareableUrl); 114 | 115 | this.clipboard.copy(shareableUrl); 116 | this.toaster.info('URL copied to Clipboard'); 117 | } 118 | 119 | getUrl(): string { 120 | const shareableUrl = `${window.location.origin}/voting/${this.voting.id}`; 121 | return shareableUrl; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/app/inside/votings-list/votings-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

My Votings

5 |
6 |
7 |
8 |
9 | 10 |
11 | 18 | 19 | 29 | 59 |
60 |
61 |
62 |
63 | -------------------------------------------------------------------------------- /src/app/inside/votings-list/votings-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saimon24/Votetastic/0c623b150d9471417e14b5a72cc9d65e2575ee66/src/app/inside/votings-list/votings-list.component.scss -------------------------------------------------------------------------------- /src/app/inside/votings-list/votings-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { VotingsListComponent } from './votings-list.component'; 4 | 5 | describe('VotingsListComponent', () => { 6 | let component: VotingsListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ VotingsListComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(VotingsListComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/inside/votings-list/votings-list.component.ts: -------------------------------------------------------------------------------- 1 | import { DataService } from '../../services/data.service'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { Voting } from 'src/app/interfaces'; 5 | 6 | @Component({ 7 | selector: 'app-votings-list', 8 | templateUrl: './votings-list.component.html', 9 | styleUrls: ['./votings-list.component.scss'], 10 | }) 11 | export class VotingsListComponent implements OnInit { 12 | votings: Voting[] = []; 13 | 14 | constructor(private dataService: DataService, private router: Router) {} 15 | 16 | ngOnInit(): void { 17 | this.loadVotings(); 18 | } 19 | 20 | async loadVotings() { 21 | this.votings = await this.dataService.getVotings(); 22 | } 23 | 24 | async startVoting() { 25 | const record = await this.dataService.startVoting(); 26 | 27 | if (!record.error && record.data?.length) { 28 | this.router.navigateByUrl(`/app/${record.data[0].id}`); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/interfaces/currentUser.ts: -------------------------------------------------------------------------------- 1 | import {User} from "@supabase/supabase-js"; 2 | 3 | export type CurrentUser = boolean | User | null; 4 | -------------------------------------------------------------------------------- /src/app/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './currentUser'; 2 | export * from './votings'; 3 | export * from './votingOption'; 4 | -------------------------------------------------------------------------------- /src/app/interfaces/votingOption.ts: -------------------------------------------------------------------------------- 1 | export interface VotingOption { 2 | id?: number; 3 | creator_id?: string; 4 | 5 | voting_id: number; 6 | title: string; 7 | votes: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/interfaces/votings.ts: -------------------------------------------------------------------------------- 1 | export interface Voting { 2 | id: number, 3 | title: string, 4 | voting_question: string, 5 | description: string, 6 | creator_id: string, 7 | created_at: Date, 8 | public: boolean 9 | } 10 | -------------------------------------------------------------------------------- /src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Workflow 8 |

9 | Votetastic 10 |

11 |
12 | 13 |
14 |
15 |
16 |
17 | 20 |
21 | 30 |
31 |
32 | 33 |
34 | 37 |
38 | 47 |
48 |
49 | 50 | 60 | 61 |
62 | 68 |
69 | 70 |
71 | 78 |
79 |
80 | 81 | 154 |
155 |
156 |
157 | -------------------------------------------------------------------------------- /src/app/login/login.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saimon24/Votetastic/0c623b150d9471417e14b5a72cc9d65e2575ee66/src/app/login/login.component.scss -------------------------------------------------------------------------------- /src/app/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(LoginComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { AuthService } from './../services/auth.service'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | import { Router } from '@angular/router'; 5 | import { ToastrService } from 'ngx-toastr'; 6 | 7 | @Component({ 8 | selector: 'app-login', 9 | templateUrl: './login.component.html', 10 | styleUrls: ['./login.component.scss'], 11 | }) 12 | export class LoginComponent implements OnInit { 13 | form: FormGroup; 14 | 15 | constructor( 16 | private fb: FormBuilder, 17 | private auth: AuthService, 18 | private router: Router, 19 | private toaster: ToastrService 20 | ) { 21 | this.form = this.fb.group({ 22 | email: ['', [Validators.required, Validators.email]], 23 | password: ['', [Validators.required, Validators.minLength(6)]], 24 | }); 25 | } 26 | 27 | ngOnInit(): void {} 28 | 29 | async login() { 30 | console.log(this.form.value); 31 | const { session, error } = await this.auth.login(this.form.value); 32 | console.log(error); 33 | if (error) { 34 | // TODO show error alert 35 | } else { 36 | this.router 37 | .navigateByUrl('/app', { replaceUrl: true }) 38 | .then(() => this.toaster.success('You signed in')); 39 | } 40 | } 41 | 42 | async register() { 43 | console.log(this.form.value); 44 | const { session, error, user } = await this.auth.createAccount( 45 | this.form.value 46 | ); 47 | console.log(error); 48 | 49 | if (error) { 50 | // TODO show error alert 51 | } else { 52 | console.log(session); 53 | console.log(user); 54 | 55 | this.router.navigateByUrl('/app', { replaceUrl: true }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AuthService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { environment } from '../../environments/environment.prod'; 2 | import { Injectable } from '@angular/core'; 3 | import { createClient, SupabaseClient, User } from '@supabase/supabase-js'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | import { CurrentUser } from "../interfaces"; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class AuthService { 11 | private supabase: SupabaseClient; 12 | private currentUser: BehaviorSubject = 13 | new BehaviorSubject(null); 14 | 15 | constructor() { 16 | this.supabase = createClient( 17 | environment.supabaseUrl, 18 | environment.supabaseKey 19 | ); 20 | 21 | // Fallback 22 | const user = this.supabase.auth.user(); 23 | if (user) { 24 | this.currentUser.next(user); 25 | } else { 26 | this.currentUser.next(false); 27 | } 28 | 29 | this.supabase.auth.onAuthStateChange((event, session) => { 30 | console.log('auth changed: ', event); 31 | console.log('auth changed session: ', session); 32 | if (session) { 33 | this.currentUser.next(session.user); 34 | } else { 35 | this.currentUser.next(false); 36 | } 37 | }); 38 | } 39 | 40 | createAccount({ email, password }: { email: string; password: string }) { 41 | return this.supabase.auth.signUp({ email, password }); 42 | } 43 | 44 | login({ email, password }: { email: string; password: string }) { 45 | return this.supabase.auth.signIn({ email, password }); 46 | } 47 | 48 | getCurrentUser() { 49 | return this.currentUser.asObservable(); 50 | } 51 | 52 | async logout() { 53 | this.currentUser.next(false); 54 | return this.supabase.auth.signOut(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/services/data.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { DataService } from './data.service'; 4 | 5 | describe('DataService', () => { 6 | let service: DataService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(DataService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/services/data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { createClient, SupabaseClient } from '@supabase/supabase-js'; 3 | import { environment } from 'src/environments/environment'; 4 | import { Voting, VotingOption } from '../interfaces'; 5 | 6 | export const TABLE_VOTINGS = 'votings'; 7 | export const TABLE_VOTING_OPTIONS = 'voting_options'; 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class DataService { 13 | private supabase: SupabaseClient; 14 | 15 | constructor() { 16 | this.supabase = createClient( 17 | environment.supabaseUrl, 18 | environment.supabaseKey 19 | ); 20 | } 21 | 22 | startVoting() { 23 | return this.supabase.from(TABLE_VOTINGS).insert({ 24 | voting_question: 'My questions', 25 | description: 'My description', 26 | }); 27 | } 28 | 29 | async getVotings() { 30 | const votings = await this.supabase 31 | .from(TABLE_VOTINGS) 32 | .select('*') 33 | .eq('creator_id', this.supabase.auth.user()?.id); 34 | return votings.data || []; 35 | } 36 | 37 | async getVotingDetails(id: number) { 38 | return this.supabase.from(TABLE_VOTINGS).select('*').eq('id', id).single(); 39 | } 40 | 41 | async updateVotingDetails(voting: Voting, id: number) { 42 | return this.supabase 43 | .from(TABLE_VOTINGS) 44 | .update(voting) 45 | .eq('id', id) 46 | .single(); 47 | } 48 | 49 | async deleteVoting(id: number) { 50 | return this.supabase.from(TABLE_VOTINGS).delete().eq('id', id).single(); 51 | } 52 | 53 | async getVotingOptions(votingId: number) { 54 | return this.supabase 55 | .from(TABLE_VOTING_OPTIONS) 56 | .select('*') 57 | .eq('voting_id', votingId); 58 | } 59 | 60 | async addVotingOption(option: VotingOption) { 61 | option.creator_id = this.supabase.auth.user()?.id; 62 | option.votes = 0; 63 | delete option.id; 64 | 65 | return this.supabase.from(TABLE_VOTING_OPTIONS).insert(option); 66 | } 67 | 68 | async updateVotingOption(option: VotingOption) { 69 | return this.supabase 70 | .from(TABLE_VOTING_OPTIONS) 71 | .update({ title: option.title }) 72 | .eq('id', option.id); 73 | } 74 | 75 | async deleteVotingOption(id: number) { 76 | return this.supabase 77 | .from(TABLE_VOTING_OPTIONS) 78 | .delete() 79 | .eq('id', id) 80 | .single(); 81 | } 82 | 83 | voteForOption(id: string) { 84 | return this.supabase.rpc('increment', { row_id: id }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/app/voting/voting.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ voting.title }} 5 |

6 | {{ voting.voting_question }} 7 |

8 |

9 |
10 | 11 |
15 |
16 |
17 |

18 | {{ voting.description }} 19 |
20 |
21 | 22 |
23 |
28 | {{ option.title }} 29 |
30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 |

38 | {{ voting.description }} 39 |
40 |
41 | 42 |
43 | 50 | 51 |
52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /src/app/voting/voting.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saimon24/Votetastic/0c623b150d9471417e14b5a72cc9d65e2575ee66/src/app/voting/voting.component.scss -------------------------------------------------------------------------------- /src/app/voting/voting.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { VotingComponent } from './voting.component'; 4 | 5 | describe('VotingComponent', () => { 6 | let component: VotingComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ VotingComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(VotingComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/voting/voting.component.ts: -------------------------------------------------------------------------------- 1 | import { ToastrService } from 'ngx-toastr'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { DataService } from './../services/data.service'; 4 | import { Component, OnInit } from '@angular/core'; 5 | import { Voting } from '../interfaces/votings'; 6 | import { VotingOption } from '../interfaces'; 7 | import { ChartConfiguration } from 'chart.js'; 8 | 9 | @Component({ 10 | selector: 'app-voting', 11 | templateUrl: './voting.component.html', 12 | styleUrls: ['./voting.component.scss'], 13 | }) 14 | export class VotingComponent implements OnInit { 15 | voting: Voting = null!; 16 | options: VotingOption[] = []; 17 | voted = false; 18 | 19 | public barChartData: ChartConfiguration<'bar'>['data'] = { 20 | labels: [], 21 | datasets: [], 22 | }; 23 | 24 | public barChartOptions: ChartConfiguration<'bar'>['options'] = { 25 | responsive: false, 26 | }; 27 | 28 | constructor( 29 | private dataService: DataService, 30 | private route: ActivatedRoute, 31 | private toaster: ToastrService 32 | ) {} 33 | 34 | async ngOnInit() { 35 | const id = this.route.snapshot.paramMap.get('id'); 36 | 37 | if (id) { 38 | this.voting = await (await this.dataService.getVotingDetails(+id)).data; 39 | this.options = 40 | (await (await this.dataService.getVotingOptions(+id)).data) || []; 41 | } 42 | } 43 | 44 | async vote(option: VotingOption) { 45 | const data = await this.dataService.voteForOption(`${option.id}`); 46 | if (!data.error) { 47 | this.showVotingResult(); 48 | } 49 | } 50 | 51 | async showVotingResult() { 52 | this.options = 53 | (await (await this.dataService.getVotingOptions(this.voting.id)).data) || 54 | []; 55 | 56 | this.barChartData = { 57 | labels: this.options.map((item) => item.title), 58 | datasets: [ 59 | { 60 | data: this.options.map((item) => item.votes), 61 | backgroundColor: '#6366f1', 62 | borderRadius: 2, 63 | }, 64 | ], 65 | }; 66 | 67 | this.toaster.success('Thanks for your vote!'); 68 | this.voted = true; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saimon24/Votetastic/0c623b150d9471417e14b5a72cc9d65e2575ee66/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | supabaseUrl: 'https://ijqxbxlroqdpabrftffk.supabase.co', 4 | supabaseKey: 5 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlqcXhieGxyb3FkcGFicmZ0ZmZrIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTkwMTUxMjAsImV4cCI6MTk3NDU5MTEyMH0.xrzfgY0LhXigDSCXPsh068ev0_x9AO8zGzO1Six1Uug', 6 | }; 7 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | supabaseUrl: 'https://ijqxbxlroqdpabrftffk.supabase.co', 8 | supabaseKey: 9 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlqcXhieGxyb3FkcGFicmZ0ZmZrIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTkwMTUxMjAsImV4cCI6MTk3NDU5MTEyMH0.xrzfgY0LhXigDSCXPsh068ev0_x9AO8zGzO1Six1Uug', 10 | }; 11 | 12 | /* 13 | * For easier debugging in development mode, you can import the following file 14 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 15 | * 16 | * This import should be commented out in production mode because it will have a negative impact 17 | * on performance if an error is thrown. 18 | */ 19 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 20 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saimon24/Votetastic/0c623b150d9471417e14b5a72cc9d65e2575ee66/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VotingApp 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | (id: string): T; 13 | keys(): string[]; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), 21 | ); 22 | 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().forEach(context); 27 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{html,ts}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require('@tailwindcss/line-clamp'), require('@tailwindcss/forms')], 8 | } 9 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # Todos 2 | 3 | - Favicon 4 | - Loading spinner during login and API calls 5 | - Share URL on voting details page 6 | - 404 page 7 | - Show results for logged in user 8 | - Different authentication like magic link and social 9 | - Vote only once per IP 10 | 11 | - Add image option to voting 12 | - Supabase file upload! 13 | - Capacitor for image capturing 14 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2020", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------