feat: add Angular todo list example

add Angular todo list source code
add the example to the existing list on supabase.io
add codeowners file for automated assignment fo reviews
This commit is contained in:
geromegrignon committed 2021-07-14 23:20:57 +02:00
1 parent efe680dfdf
commit 13533229a9
50 files changed
+17662

No files matched your search

+1
View File
@@ -0,0 +1 @@
/examples/angular-todo-list @geromegrignon
@@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
+16
View File
@@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false
+45
View File
@@ -0,0 +1,45 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
+120
View File
@@ -0,0 +1,120 @@
# Todo example using Supabase & Angular
- Frontend:
- [Angular](https://angular.io)
- [Supabase.js](https://supabase.io/docs/library/getting-started) for user management and realtime data syncing.
- Backend:
- [app.supabase.io](https://app.supabase.io/): hosted Postgres database with restful API for usage with Supabase.js.
## Deploy your own
### 1. Create new project
Sign up to Supabase - [https://app.supabase.io](https://app.supabase.io) and create a new project. Wait for your database to start.
### 2. Run "Todo List" Quickstart
Once your database has started, run the "Todo List" quickstart. Inside of your project, enter the `SQL editor` tab and scroll down until you see `TODO LIST: Build a basic todo list with Row Level Security`.
### 3. Get the URL and Key
Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token. This enables row level security for your data. Read more about this [below](#postgres-row-level-security).
![image](https://user-images.githubusercontent.com/10214025/88916245-528c2680-d298-11ea-8a71-708f93e1ce4f.png)
**_NOTE_**: The `service_role` key has full access to your data, bypassing any security policies. These keys have to be kept secret and are meant to be used in server environments and never on a client or browser.
### 4. Run the Angular client locally
Copy the url and the key retrieved into the previous step into the `environment.ts` file :
```ts
export const environment = {
production: false,
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseKey: 'YOUR_SUPABASE_KEY',
}
```
run `ng serve`
### 5. Deploy the Angular client
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fangular-todo-list&env=ANGULAR_APP_SUPABASE_URL,ANGULAR_APP_SUPABASE_KEY&envDescription=Find%20the%20Supabase%20URL%20and%20key%20in%20the%20your%20auto-generated%20docs%20at%20app.supabase.io&project-name=supabase-todo-list&repo-name=supabase-todo-list)
Here, we recommend forking this repo so you can deploy through Vercel by clicking the button above. When you click the button, replace the repo URL with your fork's URL.
You will be asked for a `ANGULAR_APP_SUPABASE_URL` and `ANGULAR_APP_SUPABASE_KEY`. Use the API URL and `anon` key from [step 3](#3.-get-the-url-and-key).
### 6. Change authentication settings if necessary
![Change auth settings](https://user-images.githubusercontent.com/1811651/101840012-39be3800-3af8-11eb-8c32-73f2fae6299e.png)
On [app.supabase.io](https://app.supabase.io), you can go to Authentication -> Settings to change your auth settings for your project if necessary. Here, you can change the site URL, which is used for determining where to redirect users after they confirm their email addresses or attempt to use a magic link to log in.
Here, you can also enable external oauth providers, such as Google and GitHub.
Instructions to activate Google provider : https://supabase.io/docs/guides/auth/auth-google
Instructions to activate Github provider : https://supabase.io/docs/guides/auth/auth-github
## Supabase details
### Postgres Row level security
This project uses very high-level Authorization using Postgres' Role Level Security.
When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions.
When a user logs in, they are issued a JWT with the role `authenticated` and their UUID.
We can use these details to provide fine-grained control over what each user can and cannot do.
This is a trimmed-down schema, with the policies:
```sql
create table todos (
id bigint generated by default as identity primary key,
user_id uuid references auth.users not null,
task text check (char_length(task) > 3),
is_complete boolean default false,
inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);
alter table todos enable row level security;
create policy "Individuals can create todos." on todos for
insert with check (auth.uid() = user_id);
create policy "Individuals can view their own todos. " on todos for
select using (auth.uid() = user_id);
create policy "Individuals can update their own todos." on todos for
update using (auth.uid() = user_id);
create policy "Individuals can delete their own todos." on todos for
delete using (auth.uid() = user_id);
```
## Authors
- [Supabase](https://supabase.io)
- [Gerome Grignon](https://github.com/geromegrignon)
Supabase is open source, we'd love for you to follow along and get involved at https://github.com/supabase/supabase
## Local development
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
+97
View File
@@ -0,0 +1,97 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"cli": {
"analytics": false
},
"version": 1,
"newProjectRoot": "projects",
"projects": {
"angular-todo-list": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/angular-todo-list",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.css"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "angular-todo-list:build:production"
},
"development": {
"browserTarget": "angular-todo-list:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "angular-todo-list:build"
}
},
"test": {
"builder": "@angular-builders/jest:run",
"options": {
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.css"],
"scripts": []
}
}
}
}
},
"defaultProject": "angular-todo-list"
}
@@ -0,0 +1,7 @@
module.exports = {
moduleNameMapper: {
'@core/(.*)': '<rootDir>/src/app/core/$1',
},
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
}
File diff suppressed because it is too large. Load diff
+49
View File
@@ -0,0 +1,49 @@
{
"name": "angular-todo-list",
"version": "1.0.0",
"author": {
"name": "Gerome Grignon",
"url": "https://github.com/geromegrignon"
},
"scripts": {
"ng": "ng",
"start": "ng serve",
"config": "ts-node set-env.ts",
"build": "npm run config && ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "~12.1.1",
"@angular/common": "~12.1.1",
"@angular/compiler": "~12.1.1",
"@angular/core": "~12.1.1",
"@angular/forms": "~12.1.1",
"@angular/platform-browser": "~12.1.1",
"@angular/platform-browser-dynamic": "~12.1.1",
"@angular/router": "~12.1.1",
"@briebug/jest-schematic": "^3.1.0",
"@ngneat/tailwind": "^7.0.3",
"@supabase/supabase-js": "^1.18.1",
"dotenv": "^10.0.0",
"rxjs": "~6.6.0",
"tslib": "^2.2.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-builders/jest": "12.1.0",
"@angular-devkit/build-angular": "~12.1.1",
"@angular/cli": "~12.1.1",
"@angular/compiler-cli": "~12.1.1",
"@types/jasmine": "~3.6.0",
"@types/jest": "26.0.24",
"@types/node": "^12.11.1",
"jasmine-core": "~3.7.0",
"jest": "27.0.6",
"karma-coverage": "~2.0.3",
"tailwindcss": "2.2.4",
"ts-node": "^10.1.0",
"typescript": "~4.3.2"
}
}
+23
View File
@@ -0,0 +1,23 @@
import ErrnoException = NodeJS.ErrnoException
const { writeFile } = require('fs')
// Configure Angular `environment.ts` file path
const targetPath = './src/environments/environment.prod.ts'
// read environment variables from .env file
require('dotenv').config()
// `environment.ts` file structure
const envConfigFile = `export const environment = {
production: true,
supabaseUrl: '${process.env.ANGULAR_APP_SUPABASE_URL}',
supabaseKey: '${process.env.ANGULAR_APP_SUPABASE_KEY}'
};
`
writeFile(targetPath, envConfigFile, function (err: ErrnoException | null) {
if (err) {
throw console.error(err)
}
})
+30
View File
@@ -0,0 +1,30 @@
import 'jest-preset-angular'
/* global mocks for jsdom */
const mock = () => {
let storage: { [key: string]: string } = {}
return {
getItem: (key: string) => (key in storage ? storage[key] : null),
setItem: (key: string, value: string) => (storage[key] = value || ''),
removeItem: (key: string) => delete storage[key],
clear: () => (storage = {}),
}
}
Object.defineProperty(window, 'localStorage', { value: mock() })
Object.defineProperty(window, 'sessionStorage', { value: mock() })
Object.defineProperty(window, 'getComputedStyle', {
value: () => ['-webkit-appearance'],
})
Object.defineProperty(document.body.style, 'transform', {
value: () => {
return {
enumerable: true,
configurable: true,
}
},
})
/* output shorter and more meaningful Zone error stack traces */
// Error.stackTraceLimit = 2;
@@ -0,0 +1,29 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { AuthComponent } from './components/auth/auth.component'
import { HomeComponent } from './components/home/home.component'
import { AuthGuard } from './guards/auth.guard'
import { RecoveryPasswordGuard } from './guards/recovery-password.guard'
import { RecoveryPasswordComponent } from './components/recovery-password/recovery-password.component'
const routes: Routes = [
{
path: '',
component: HomeComponent,
canActivate: [AuthGuard, RecoveryPasswordGuard],
},
{
path: 'auth',
component: AuthComponent,
},
{
path: 'recovery-password',
component: RecoveryPasswordComponent,
},
]
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
@@ -0,0 +1 @@
<router-outlet></router-outlet>
@@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing'
import { AppComponent } from './app.component'
import { RouterTestingModule } from '@angular/router/testing'
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [RouterTestingModule],
}).compileComponents()
})
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent)
const app = fixture.componentInstance
expect(app).toBeTruthy()
})
})
@@ -0,0 +1,7 @@
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {}
@@ -0,0 +1,23 @@
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { AppComponent } from './app.component'
import { ReactiveFormsModule } from '@angular/forms'
import { AuthComponent } from './components/auth/auth.component'
import { HomeComponent } from './components/home/home.component'
import { AppRoutingModule } from './app-routing.module'
import { TodoItemComponent } from './components/todo-item/todo-item.component'
import { RecoveryPasswordComponent } from './components/recovery-password/recovery-password.component'
@NgModule({
declarations: [
AppComponent,
AuthComponent,
HomeComponent,
TodoItemComponent,
RecoveryPasswordComponent,
],
imports: [BrowserModule, ReactiveFormsModule, AppRoutingModule],
bootstrap: [AppComponent],
})
export class AppModule {}
@@ -0,0 +1,179 @@
<div class="min-w-full min-h-screen flex items-center justify-center bg-gray-200">
<form
[formGroup]="authForm"
class="w-full h-full sm:h-auto sm:w-2/5 max-w-sm p-5 bg-white shadow flex flex-col text-base"
>
<span class="font-sans text-4xl text-center pb-2 mb-1 border-b mx-4 align-center"> Login </span>
<label class="mt-3 mb-2 font-medium text-lg" for="email">
<span class="font-mono mr-1 text-red-400">*</span>
Email:
</label>
<input
id="email"
class="bg-gray-100 border py-1 px-3"
type="email"
name="email"
formControlName="email"
required
/>
<label class="mt-3 mb-2 font-medium text-lg" for="password">
<span class="font-mono mr-1 text-red-400">*</span>
Password:
</label>
<input
id="password"
class="bg-gray-100 border py-1 px-3"
type="password"
name="password"
formControlName="password"
required
/>
<span
class="text-blue-600 mt-2 cursor-pointer self-end text-sm font-medium"
(click)="forgotPassword()"
>
Forgot Password?
</span>
<div
*ngIf="helperText?.text && helperText?.error"
class="border px-1 py-2 my-2 text-center text-sm bg-red-100 border-red-300 text-red-400"
>
{{ helperText?.text }}
</div>
<div
*ngIf="helperText?.text && !helperText?.error"
class="border px-1 py-2 my-2 text-center text-sm bg-green-100 border-green-300 text-green-500"
>
{{ helperText?.text }}
</div>
<div class="mt-2 flex">
<span class="block mx-1.5 w-full rounded-md shadow-sm">
<button
type="button"
(click)="handleLogin('REGISTER')"
class="
border
w-full
border-blue-600
text-blue-600
flex
justify-center
py-2
px-4
text-sm
font-medium
rounded-md
hover:bg-blue-200
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
active:bg-blue-700
transition
duration-150
ease-in-out
"
>
Sign Up
</button>
</span>
<span class="block w-full mx-1.5 rounded-md shadow-sm">
<button
(click)="handleLogin('LOGIN')"
type="button"
class="
flex
w-full
justify-center
py-2
px-4
border border-transparent
text-sm
font-medium
rounded-md
text-white
bg-blue-600
hover:bg-blue-500
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
active:bg-blue-700
transition
duration-150
ease-in-out
"
>
Sign In
</button>
</span>
</div>
<div class="mt-3">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full mx-1.5 border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm leading-5">
<span class="px-2 bg-white text-gray-500"> Or continue with </span>
</div>
</div>
<div>
<div class="mt-3">
<span class="block rounded-md shadow-sm">
<button
(click)="handleOAuthLogin('github')"
type="button"
class="
w-3/4
mx-auto
flex
justify-center
py-2
px-4
border border-transparent
text-sm
font-medium
rounded-md
text-white
bg-blue-600
hover:bg-blue-500
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
active:bg-blue-700
transition
duration-150
ease-in-out
"
>
GitHub
</button>
</span>
</div>
<div class="mt-3">
<span class="block rounded-md shadow-sm">
<button
(click)="handleOAuthLogin('google')"
type="button"
class="
w-3/4
mx-auto
flex
justify-center
py-2
px-4
border border-transparent
text-sm
font-medium
rounded-md
text-white
bg-blue-600
hover:bg-blue-500
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
active:bg-blue-700
transition
duration-150
ease-in-out
"
>
Google
</button>
</span>
</div>
</div>
</div>
</form>
</div>
@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { AuthComponent } from './auth.component'
import { RouterTestingModule } from '@angular/router/testing'
import { ReactiveFormsModule } from '@angular/forms'
describe('AuthComponent', () => {
let component: AuthComponent
let fixture: ComponentFixture<AuthComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AuthComponent],
imports: [RouterTestingModule, ReactiveFormsModule],
}).compileComponents()
})
beforeEach(() => {
fixture = TestBed.createComponent(AuthComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})
@@ -0,0 +1,66 @@
import { Component } from '@angular/core'
import { FormControl, FormGroup, Validators } from '@angular/forms'
import { SupabaseService } from '../../services/supabase.service'
import { Provider } from '@supabase/supabase-js'
import { Router } from '@angular/router'
interface HelperText {
text: string
error: boolean
}
@Component({
selector: 'app-auth',
templateUrl: './auth.component.html',
})
export class AuthComponent {
authForm: FormGroup = new FormGroup({
email: new FormControl('', Validators.required),
password: new FormControl('', Validators.required),
})
helperText: HelperText | undefined
constructor(private readonly supabase: SupabaseService, private readonly router: Router) {}
async forgotPassword(): Promise<void> {
const email = prompt('Please enter your email:')
let { error } = await this.supabase.resetPassword(email as string)
if (error) {
console.error('Error: ', error.message)
} else {
this.helperText = {
error: false,
text: 'Password recovery email has been sent.',
}
}
}
async handleLogin(type: string): Promise<void> {
const { email, password } = this.authForm.value
const { user, error, session } =
type === 'LOGIN'
? await this.supabase.signIn(email, password)
: await this.supabase.signUp(email, password)
if (user && type === 'LOGIN') {
await this.router.navigate(['/'])
} else if (error) {
this.helperText = { error: true, text: error.message }
} else if (user && !session && !error) {
this.helperText = {
error: false,
text: 'An email has been sent to you for verification!',
}
}
}
async handleOAuthLogin(provider: Provider): Promise<void> {
// You need to enable the third party auth you want in Authentication > Settings
// Read more on: https://supabase.io/docs/guides/auth#third-party-logins
let { error } = await this.supabase.signInWithProvider(provider)
if (error) console.error('Error: ', error.message)
}
}
@@ -0,0 +1,3 @@
.todos-container {
height: calc(100vh - 11.5rem);
}
@@ -0,0 +1,94 @@
<div class="w-screen fixed flex flex-col min-h-screen bg-gray-50">
<header class="flex justify-between items-center px-4 h-16 bg-gray-900">
<span class="text-2xl sm:text-4xl text-white border-b font-sans"> Todo List. </span>
<button
(click)="handleLogout()"
class="
flex
justify-center
py-2
px-4
border border-transparent
text-sm
font-medium
rounded-md
text-white
bg-blue-600
hover:bg-blue-500
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
active:bg-blue-700
transition
duration-150
ease-in-out
"
>
Logout
</button>
</header>
<div class="flex flex-col flex-grow p-4 todos-container">
<div
[ngClass]="{ 'auto-rows-min': todos.length }"
class="p-2 border flex-grow grid gap-2 grid-cols-1 h-2/3 overflow-y-scroll first:mt-8"
>
<ng-container *ngIf="todos.length; else emptyList">
<app-todo-item
*ngFor="let todo of todos"
[todo]="todo"
(delete)="delete(todo.id)"
(toggleComplete)="toggleComplete(todo.id, todo.is_complete)"
>
</app-todo-item>
</ng-container>
<ng-template #emptyList>
<span class="h-full flex justify-center items-center"> You do have any tasks yet! </span>
</ng-template>
</div>
<div
*ngIf="errorText"
class="
border
max-w-sm
self-center
px-4
py-2
mt-4
text-center text-sm
bg-red-100
border-red-300
text-red-400
"
>
{{ errorText }}
</div>
<form [formGroup]="todoForm" (ngSubmit)="addTodo()" class="flex m-4 mt-0 h-10 mt-8">
<input
formControlName="task"
type="text"
class="bg-gray-200 border px-2 border-gray-300 w-full mr-4"
/>
<button
type="submit"
class="
flex
justify-center
py-2
px-4
border border-transparent
text-sm
font-medium
rounded-md
text-white
bg-blue-600
hover:bg-blue-500
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
active:bg-blue-700
transition
duration-150
ease-in-out
"
>
Add
</button>
</form>
</div>
</div>
@@ -0,0 +1,28 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { HomeComponent } from './home.component'
import { RouterTestingModule } from '@angular/router/testing'
import { ReactiveFormsModule } from '@angular/forms'
import { TodoItemComponent } from '../todo-item/todo-item.component'
describe('HomeComponent', () => {
let component: HomeComponent
let fixture: ComponentFixture<HomeComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HomeComponent, TodoItemComponent],
imports: [RouterTestingModule, ReactiveFormsModule],
}).compileComponents()
})
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})
@@ -0,0 +1,81 @@
import { Component, OnInit } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { Todo } from '../../models/todo'
import { SupabaseService } from '../../services/supabase.service'
import { Router } from '@angular/router'
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css'],
})
export class HomeComponent implements OnInit {
todos: Todo[] = []
todoForm: FormGroup = new FormGroup({
task: new FormControl(),
})
errorText: string | undefined | null
constructor(private readonly supabase: SupabaseService, private readonly router: Router) {}
ngOnInit(): void {
this.fetchTodos()
}
async fetchTodos(): Promise<void> {
let { data: todos, error } = await this.supabase.fetchTodos()
if (error) {
console.error('error', error.message)
} else {
this.todos = todos ?? []
}
}
async addTodo(): Promise<void> {
let task = this.todoForm.value.task.trim()
if (task.length <= 3) {
this.errorText = 'Task length should be more than 3!'
} else {
let { data: todo, error } = await this.supabase.addTodo(task)
if (error) {
this.errorText = error.message
} else {
this.todos = [todo, ...this.todos]
this.errorText = null
this.todoForm.reset()
}
}
}
async toggleComplete(id: string, is_complete: boolean): Promise<void> {
try {
await this.supabase.toggleComplete(id, is_complete)
this.todos = this.todos.map((todo) => {
if (todo.id === id) {
return { ...todo, is_complete: !is_complete }
}
return todo
})
} catch (error) {
console.error(error)
}
}
async delete(id: string): Promise<void> {
try {
await this.supabase.deleteTodo(id)
this.todos = this.todos.filter((todo) => todo.id !== id)
} catch (error) {
console.error('error', error)
}
}
async handleLogout(): Promise<void> {
try {
await this.supabase.signOut()
await this.router.navigate(['/auth'])
} catch (error) {
console.error(error)
}
}
}
@@ -0,0 +1,46 @@
<div class="min-w-full min-h-screen flex items-center justify-center bg-gray-200">
<div
class="w-full h-full sm:h-auto sm:w-2/5 max-w-sm p-5 bg-white shadow flex flex-col text-base"
>
<span class="font-sans text-2xl text-center pb-2 mb-1 border-b mx-4 align-center">
Recover Password
</span>
<label class="mt-3 mb-2 text-lg" for="email">
<span class="font-mono mr-1 text-red-400">*</span>Enter new password:
</label>
<input
class="bg-gray-100 border py-1 px-3"
type="password"
id="email"
[formControl]="passwordControl"
required
/>
<span class="block mt-4 rounded-md shadow-sm">
<button
(click)="handleNewPassword()"
type="button"
class="
flex
mx-auto
justify-center
py-2
px-4
border border-transparent
text-sm
font-medium
rounded-md
text-white
bg-blue-600
hover:bg-blue-500
focus:outline-none focus:border-blue-700 focus:shadow-outline-blue
active:bg-blue-700
transition
duration-150
ease-in-out
"
>
Change Password
</button>
</span>
</div>
</div>
@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { RecoveryPasswordComponent } from './recovery-password.component'
import { RouterTestingModule } from '@angular/router/testing'
import { ReactiveFormsModule } from '@angular/forms'
describe('RecoveryPasswordComponent', () => {
let component: RecoveryPasswordComponent
let fixture: ComponentFixture<RecoveryPasswordComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RecoveryPasswordComponent],
imports: [RouterTestingModule, ReactiveFormsModule],
}).compileComponents()
})
beforeEach(() => {
fixture = TestBed.createComponent(RecoveryPasswordComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})
@@ -0,0 +1,24 @@
import { Component } from '@angular/core'
import { SupabaseService } from '../../services/supabase.service'
import { FormControl, Validators } from '@angular/forms'
import { Router } from '@angular/router'
@Component({
selector: 'app-recovery-password',
templateUrl: './recovery-password.component.html',
})
export class RecoveryPasswordComponent {
passwordControl: FormControl = new FormControl('', Validators.required)
constructor(private readonly supabase: SupabaseService, private readonly router: Router) {}
async handleNewPassword(): Promise<void> {
if (this.passwordControl.valid) {
try {
await this.supabase.handleNewPassword(this.passwordControl.value)
await this.router.navigate(['/'])
} catch (error) {
console.error(error)
}
}
}
}
@@ -0,0 +1,16 @@
<div class="p-3 max-h-14 flex align-center justify-between border">
<span class="truncate flex-grow">
<input
class="cursor-pointer mr-2"
(change)="toggleComplete.emit()"
type="checkbox"
[checked]="todo?.is_complete"
/>
<span [ngClass]="{ 'line-through': todo?.is_complete }" class="w-full flex-grow">
{{ todo?.task }}
</span>
</span>
<button type="button" class="font-mono text-red-500 text-xl border px-2" (click)="delete.emit()">
X
</button>
</div>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { TodoItemComponent } from './todo-item.component'
describe('TodoItemComponent', () => {
let component: TodoItemComponent
let fixture: ComponentFixture<TodoItemComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TodoItemComponent],
}).compileComponents()
})
beforeEach(() => {
fixture = TestBed.createComponent(TodoItemComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})
@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'
import { Todo } from '../../models/todo'
@Component({
selector: 'app-todo-item',
templateUrl: './todo-item.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodoItemComponent {
@Input() todo: Todo | undefined
@Output() delete: EventEmitter<void> = new EventEmitter<void>()
@Output() toggleComplete: EventEmitter<void> = new EventEmitter<void>()
}
@@ -0,0 +1,19 @@
import { TestBed } from '@angular/core/testing'
import { AuthGuard } from './auth.guard'
import { RouterTestingModule } from '@angular/router/testing'
describe('AuthGuard', () => {
let guard: AuthGuard
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
})
guard = TestBed.inject(AuthGuard)
})
it('should be created', () => {
expect(guard).toBeTruthy()
})
})
@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core'
import { CanActivate, Router } from '@angular/router'
import { SupabaseService } from '../services/supabase.service'
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private readonly supabase: SupabaseService, private readonly router: Router) {}
canActivate(): boolean {
const isSignedIn = !!this.supabase.getSession()?.user
if (!isSignedIn) {
this.router.navigate(['/auth'])
}
return isSignedIn
}
}
@@ -0,0 +1,19 @@
import { TestBed } from '@angular/core/testing'
import { RecoveryPasswordGuard } from './recovery-password.guard'
import { RouterTestingModule } from '@angular/router/testing'
describe('RecoveryPasswordGuard', () => {
let guard: RecoveryPasswordGuard
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
})
guard = TestBed.inject(RecoveryPasswordGuard)
})
it('should be created', () => {
expect(guard).toBeTruthy()
})
})
@@ -0,0 +1,36 @@
import { Injectable } from '@angular/core'
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router'
import { Observable } from 'rxjs'
import { SupabaseService } from '../services/supabase.service'
@Injectable({
providedIn: 'root',
})
export class RecoveryPasswordGuard implements CanActivate {
constructor(private readonly supabase: SupabaseService, private readonly router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const result = route.fragment?.split('&').reduce((acc, element) => {
const [k, v] = element.split('=')
return { ...acc, [k]: decodeURIComponent(v) }
}, {})
// @ts-ignore
if (result?.type === 'recovery') {
// @ts-ignore
this.supabase.token = result.access_token
this.router.navigate(['/recovery-password'])
return false
}
return true
}
}
@@ -0,0 +1,7 @@
export interface Todo {
id: string
user_id: string
task: string
is_complete: boolean
inserted_at: Date
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing'
import { SupabaseService } from './supabase.service'
describe('SupabaseService', () => {
let service: SupabaseService
beforeEach(() => {
TestBed.configureTestingModule({})
service = TestBed.inject(SupabaseService)
})
it('should be created', () => {
expect(service).toBeTruthy()
})
})
@@ -0,0 +1,76 @@
import { Injectable } from '@angular/core'
import {
AuthChangeEvent,
createClient,
Provider,
Session,
SupabaseClient,
} from '@supabase/supabase-js'
import { environment } from '../../environments/environment'
@Injectable({
providedIn: 'root',
})
export class SupabaseService {
private supabaseClient: SupabaseClient
token: string | undefined
constructor() {
this.supabaseClient = createClient(environment.supabaseUrl, environment.supabaseKey)
}
getSession(): Session | null {
return this.supabaseClient.auth.session()
}
signUp(email: string, password: string) {
return this.supabaseClient.auth.signUp({ email, password })
}
signIn(email: string, password: string) {
return this.supabaseClient.auth.signIn({ email, password })
}
signInWithProvider(provider: Provider) {
return this.supabaseClient.auth.signIn({ provider })
}
signOut() {
this.supabaseClient.auth.signOut().catch(console.error)
}
authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) {
return this.supabaseClient.auth.onAuthStateChange(callback)
}
resetPassword(email: string) {
return this.supabaseClient.auth.api.resetPasswordForEmail(email)
}
handleNewPassword(newPassword: string) {
return this.supabaseClient.auth.api.updateUser(this.token as string, {
password: newPassword,
})
}
fetchTodos() {
return this.supabaseClient.from('todos').select('*').order('id', { ascending: false })
}
addTodo(task: string) {
const userId = this.getSession()?.user?.id as string
return this.supabaseClient.from('todos').insert({ task, user_id: userId }).single()
}
toggleComplete(id: string, isCompleted: boolean) {
return this.supabaseClient
.from('todos')
.update({ is_complete: !isCompleted })
.eq('id', id)
.single()
}
deleteTodo(id: string) {
return this.supabaseClient.from('todos').delete().eq('id', id)
}
}
@@ -0,0 +1,5 @@
export const environment = {
production: true,
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseKey: 'YOUR_SUPABASE_KEY',
}
@@ -0,0 +1,18 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseKey: 'YOUR_SUPABASE_KEY',
}
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>AngularTodoList</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
import { enableProdMode } from '@angular/core'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module'
import { environment } from './environments/environment'
if (environment.production) {
enableProdMode()
}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err))
@@ -0,0 +1,64 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* IE11 requires the following for NgClass support on SVG elements
*/
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js' // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
+12
View File
@@ -0,0 +1,12 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -0,0 +1,17 @@
const { guessProductionMode } = require('@ngneat/tailwind')
module.exports = {
prefix: '',
purge: {
enabled: guessProductionMode(),
content: ['./src/**/*.{html,ts}'],
},
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
@@ -0,0 +1,21 @@
import { TestBed } from '@angular/core/testing'
type CompilerOptions = Partial<{
providers: any[]
useJit: boolean
preserveWhitespaces: boolean
}>
export type ConfigureFn = (testBed: typeof TestBed) => void
export const configureTests = (configure: ConfigureFn, compilerOptions: CompilerOptions = {}) => {
const compilerConfig: CompilerOptions = {
preserveWhitespaces: false,
...compilerOptions,
}
const configuredTestBed = TestBed.configureCompiler(compilerConfig)
configure(configuredTestBed)
return configuredTestBed.compileComponents().then(() => configuredTestBed)
}
@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts", "src/polyfills.ts"],
"include": ["src/**/*.d.ts"]
}
+27
View File
@@ -0,0 +1,27 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
"module": "es2020",
"lib": ["es2018", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["jest"],
"module": "commonjs",
"emitDecoratorMetadata": true,
"allowJs": true
},
"files": ["src/polyfills.ts"],
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}
+2
View File
@@ -15,6 +15,8 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
- [React Todo List.](https://github.com/supabase/supabase/tree/master/examples/react-todo-list)
- [Svelte Todo List.](https://github.com/supabase/supabase/tree/master/examples/sveltejs-todo-list)
- [Vue 3 Todo List (Typescript).](https://github.com/supabase/supabase/tree/master/examples/vue3-ts-todo-list)
- [Angular Todo List.](https://github.com/supabase/supabase/tree/master/examples/angular-todo-list)
## Auth examples
+13
View File
@@ -154,5 +154,18 @@
"repo_url": "https://github.com/supabase/supabase/tree/master/examples/vue3-ts-todo-list",
"vercel_deploy_url": "",
"demo_url": ""
},
{
"type": "example",
"products": ["database"],
"title": "Angular todo list app",
"description": "Angular todo List example",
"author": "geromegrignon",
"author_url": "https://github.com/geromegrignon",
"author_img": "https://avatars.githubusercontent.com/u/32737308",
"repo_name": "angular-todo-list",
"repo_url": "https://github.com/supabase/supabase/tree/master/examples/angular-todo-list",
"vercel_deploy_url": "",
"demo_url": ""
}
]