mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
efe680dfdf
commit
13533229a9
50 files changed
+17662
No files matched your search
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
|
||||

|
||||
|
||||
**_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
|
||||
|
||||
[](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
|
||||
|
||||

|
||||
|
||||
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
|
||||
```
|
||||
@@ -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'],
|
||||
}
|
||||
+16131
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -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>
|
||||
+27
@@ -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()
|
||||
})
|
||||
})
|
||||
+24
@@ -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 |
@@ -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>
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
]
|
||||
Reference in new issue
Block a user