mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 03:45:06 +03:00
feat: add cra-todo-list-auth in examples
This commit is contained in:
1 parent
67cb034987
commit
210829fe4e
30 files changed
+12529
No files matched your search
@@ -0,0 +1,24 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.eslintcache
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/../../../../../:\supabase\examples\cra-todo-list-auth\.idea/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/cra-todo-list-auth.iml" filepath="$PROJECT_DIR$/.idea/cra-todo-list-auth.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myRunOnSave" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"printWidth": 80
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# Todo example using Supabase
|
||||
|
||||
- Frontend:
|
||||
- [create-react-app](https://reactjs.org/docs/create-a-new-react-app.html). React Framework for building single-page applications.
|
||||
- [Tailwind](https://tailwindcss.com/) for styling and layout.
|
||||
- [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.
|
||||
|
||||
|
||||
## Demo
|
||||
|
||||
-not added-
|
||||
|
||||
## Deploy with Vercel
|
||||
|
||||
-not added-
|
||||
|
||||
## Build from scratch
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
You will be asked for a `REACT_APP_SUPABASE_URL` and `REACT_APP_SUPABASE_KEY`. Use the API URL and `anon` key from [step 3](#3.-get-the-url-and-key).
|
||||
|
||||
## 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 thier 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)
|
||||
|
||||
Supabase is open source, we'd love for you to follow along and get involved at https://github.com/supabase/supabase
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
style: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "cra-todo-list-auth",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@craco/craco": "^6.0.0",
|
||||
"@supabase/supabase-js": "^1.2.1",
|
||||
"@tailwindcss/postcss7-compat": "^2.0.2",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"autoprefixer": "9",
|
||||
"postcss": "7",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-scripts": "4.0.1",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="CRA Supabase TodoList Example"
|
||||
/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Supabase TodoList Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "CRA Supabase TodoList Example",
|
||||
"name": "Create React App Supabase TodoList Example",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { supabase } from "./lib/api";
|
||||
import Auth from "./components/Auth";
|
||||
import Home from "./components/Home";
|
||||
|
||||
function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const session = supabase.auth.session();
|
||||
setUser(session?.user ?? null);
|
||||
|
||||
const { data: authListener } = supabase.auth.onAuthStateChange(
|
||||
async (event, session) => {
|
||||
const currentUser = session?.user;
|
||||
setUser(currentUser ?? null);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
authListener?.unsubscribe();
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<div className="min-w-full min-h-screen flex items-center justify-center bg-gray-200">
|
||||
{!user ? <Auth /> : <Home user={user} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,181 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { supabase } from "../lib/api";
|
||||
|
||||
const Auth = () => {
|
||||
const [helperText, setHelperText] = useState({ error: null, text: null });
|
||||
const emailRef = useRef();
|
||||
const passwordRef = useRef();
|
||||
|
||||
const handleLogin = async (type) => {
|
||||
const email = emailRef.current?.value;
|
||||
const password = passwordRef.current?.value;
|
||||
|
||||
const { user, error } =
|
||||
type === "LOGIN"
|
||||
? await supabase.auth.signIn({ email, password })
|
||||
: await supabase.auth.signUp({ email, password });
|
||||
|
||||
if (error) {
|
||||
setHelperText({ error: true, text: error.message });
|
||||
} else if (!user && !error) {
|
||||
setHelperText({
|
||||
error: false,
|
||||
text: "An email has been sent to you for verification!",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOAuthLogin = async (provider) => {
|
||||
// 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 supabase.auth.signIn({ provider });
|
||||
if (error) console.log("Error: ", error.message);
|
||||
};
|
||||
|
||||
const forgotPassword = async (e) => {
|
||||
// Read more on https://supabase.io/docs/client/reset-password-email#notes
|
||||
e.preventDefault();
|
||||
const email = prompt("Please enter your email:");
|
||||
|
||||
if (email === null || email === "") {
|
||||
setHelperText({ error: true, text: "You must enter your email." });
|
||||
} else {
|
||||
let { error } = await supabase.auth.api.resetPasswordForEmail(
|
||||
email
|
||||
);
|
||||
if (error) {
|
||||
console.error("Error: ", error.message);
|
||||
} else {
|
||||
setHelperText({
|
||||
error: false,
|
||||
text: "Password recovery email has been sent.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"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
|
||||
className={
|
||||
"font-sans text-4xl text-center pb-2 mb-1 border-b mx-4 align-center"
|
||||
}
|
||||
>
|
||||
Login
|
||||
</span>
|
||||
<label
|
||||
className={"mt-3 mb-2 font-medium text-lg"}
|
||||
htmlFor={"email"}
|
||||
>
|
||||
<span className={"font-mono mr-1 text-red-400"}>*</span>Email:
|
||||
</label>
|
||||
<input
|
||||
className={"bg-gray-100 border py-1 px-3"}
|
||||
type={"email"}
|
||||
name={"email"}
|
||||
ref={emailRef}
|
||||
required
|
||||
/>
|
||||
<label
|
||||
className={"mt-3 mb-2 font-medium text-lg"}
|
||||
htmlFor={"password"}
|
||||
>
|
||||
<span className={"font-mono mr-1 text-red-400"}>*</span>
|
||||
Password:
|
||||
</label>
|
||||
<input
|
||||
className={"bg-gray-100 border py-1 px-3"}
|
||||
type={"password"}
|
||||
name={"password"}
|
||||
ref={passwordRef}
|
||||
required
|
||||
/>
|
||||
<span
|
||||
className={
|
||||
"text-blue-600 mt-2 cursor-pointer self-end text-sm font-medium"
|
||||
}
|
||||
onClick={forgotPassword}
|
||||
>
|
||||
Forgot Password?
|
||||
</span>
|
||||
{!!helperText.text && (
|
||||
<div
|
||||
className={`border px-1 py-2 my-2 text-center text-sm ${
|
||||
helperText.error
|
||||
? "bg-red-100 border-red-300 text-red-400"
|
||||
: "bg-green-100 border-green-300 text-green-500"
|
||||
}`}
|
||||
>
|
||||
{helperText.text}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 flex">
|
||||
<span className="block mx-1.5 w-full rounded-md shadow-sm">
|
||||
<button
|
||||
type="submit"
|
||||
onClick={() =>
|
||||
handleLogin("REGISTER").catch(console.error)
|
||||
}
|
||||
className={
|
||||
"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 className="block w-full mx-1.5 rounded-md shadow-sm">
|
||||
<button
|
||||
onClick={() => handleLogin("LOGIN")}
|
||||
type="button"
|
||||
className="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 className="mt-3">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full mx-1.5 border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm leading-5">
|
||||
<span className="px-2 bg-white text-gray-500">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mt-3">
|
||||
<span className="block rounded-md shadow-sm">
|
||||
<button
|
||||
onClick={() => handleOAuthLogin("github")}
|
||||
type="button"
|
||||
className="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 className="mt-3">
|
||||
<span className="block rounded-md shadow-sm">
|
||||
<button
|
||||
onClick={() => handleOAuthLogin("google")}
|
||||
type="button"
|
||||
className="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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Auth;
|
||||
@@ -0,0 +1,161 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { supabase } from "../lib/api";
|
||||
import RecoverPassword from "./RecoverPassword";
|
||||
import TodoItem from "./TodoItem";
|
||||
|
||||
const Home = ({ user }) => {
|
||||
const [recoveryToken, setRecoveryToken] = useState(null);
|
||||
const [todos, setTodos] = useState([]);
|
||||
const newTaskTextRef = useRef();
|
||||
const [errorText, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
/* Recovery url is of the form
|
||||
* <SITE_URL>#access_token=x&refresh_token=y&expires_in=z&token_type=bearer&type=recovery
|
||||
* Read more on https://supabase.io/docs/client/reset-password-email#notes
|
||||
*/
|
||||
let url = window.location.hash;
|
||||
let query = url.substr(1);
|
||||
let result = {};
|
||||
|
||||
query.split("&").forEach((part) => {
|
||||
const item = part.split("=");
|
||||
result[item[0]] = decodeURIComponent(item[1]);
|
||||
});
|
||||
|
||||
if (result.type === "recovery") {
|
||||
setRecoveryToken(result.access_token);
|
||||
}
|
||||
|
||||
fetchTodos().catch(console.error);
|
||||
}, []);
|
||||
|
||||
const fetchTodos = async () => {
|
||||
let { data: todos, error } = await supabase
|
||||
.from("todos")
|
||||
.select("*")
|
||||
.order("id", { ascending: false });
|
||||
if (error) console.log("error", error);
|
||||
else setTodos(todos);
|
||||
};
|
||||
|
||||
const deleteTodo = async (id) => {
|
||||
try {
|
||||
await supabase.from("todos").delete().eq("id", id);
|
||||
setTodos(todos.filter((x) => x.id !== id));
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
const addTodo = async () => {
|
||||
let taskText = newTaskTextRef.current.value;
|
||||
let task = taskText.trim();
|
||||
if (task.length <= 3) {
|
||||
setError("Task length should be more than 3!");
|
||||
} else {
|
||||
let { data: todo, error } = await supabase
|
||||
.from("todos")
|
||||
.insert({ task, user_id: user.id })
|
||||
.single();
|
||||
if (error) setError(error.message);
|
||||
else {
|
||||
setTodos([todo, ...todos]);
|
||||
setError(null);
|
||||
newTaskTextRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
supabase.auth.signOut().catch(console.error);
|
||||
};
|
||||
|
||||
return recoveryToken ? (
|
||||
<RecoverPassword
|
||||
token={recoveryToken}
|
||||
setRecoveryToken={setRecoveryToken}
|
||||
/>
|
||||
) : (
|
||||
<div className={"w-screen fixed flex flex-col min-h-screen bg-gray-50"}>
|
||||
<header
|
||||
className={
|
||||
"flex justify-between items-center px-4 h-16 bg-gray-900"
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"text-2xl sm:text-4xl text-white border-b font-sans"
|
||||
}
|
||||
>
|
||||
Todo List.
|
||||
</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className={
|
||||
"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
|
||||
className={"flex flex-col flex-grow p-4"}
|
||||
style={{ height: "calc(100vh - 11.5rem)" }}
|
||||
>
|
||||
<div
|
||||
className={`p-2 border flex-grow grid gap-2 ${
|
||||
todos.length ? "auto-rows-min" : ""
|
||||
} grid-cols-1 h-2/3 overflow-y-scroll first:mt-8`}
|
||||
>
|
||||
{todos.length ? (
|
||||
todos.map((todo) => (
|
||||
<TodoItem
|
||||
key={todo.id}
|
||||
todo={todo}
|
||||
onDelete={() => deleteTodo(todo.id)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<span
|
||||
className={
|
||||
"h-full flex justify-center items-center"
|
||||
}
|
||||
>
|
||||
You do have any tasks yet!
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!!errorText && (
|
||||
<div
|
||||
className={
|
||||
"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>
|
||||
)}
|
||||
</div>
|
||||
<div className={"flex m-4 mt-0 h-10"}>
|
||||
<input
|
||||
ref={newTaskTextRef}
|
||||
type="text"
|
||||
onKeyUp={(e) => e.key === "Enter" && addTodo()}
|
||||
className={
|
||||
"bg-gray-200 border px-2 border-gray-300 w-full mr-4"
|
||||
}
|
||||
/>
|
||||
<button
|
||||
onClick={addTodo}
|
||||
className={
|
||||
"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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { supabase } from "../lib/api";
|
||||
import { useRef } from "react";
|
||||
|
||||
const RecoverPassword = ({ token, setRecoveryToken }) => {
|
||||
const newPasswordRef = useRef();
|
||||
|
||||
const handleNewPassword = async () => {
|
||||
const newPassword = newPasswordRef.current.value;
|
||||
const { error } = await supabase.auth.api.updateUser(token, {
|
||||
password: newPassword,
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
// To render our Todo list again
|
||||
setRecoveryToken(null);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"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
|
||||
className={
|
||||
"font-sans text-2xl text-center pb-2 mb-1 border-b mx-4 align-center"
|
||||
}
|
||||
>
|
||||
Recover Password
|
||||
</span>
|
||||
<label className={"mt-3 mb-2 text-lg"} htmlFor={"email"}>
|
||||
<span className={"font-mono mr-1 text-red-400"}>*</span>Enter
|
||||
new password:
|
||||
</label>
|
||||
<input
|
||||
className={"bg-gray-100 border py-1 px-3"}
|
||||
type="password"
|
||||
ref={newPasswordRef}
|
||||
required
|
||||
/>
|
||||
<span className="block mt-4 rounded-md shadow-sm">
|
||||
<button
|
||||
onClick={handleNewPassword}
|
||||
type="button"
|
||||
className="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>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecoverPassword;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useState } from "react";
|
||||
import { supabase } from "../lib/api";
|
||||
|
||||
const TodoItem = ({ todo, onDelete }) => {
|
||||
const [isCompleted, setIsCompleted] = useState(todo.is_complete);
|
||||
|
||||
const toggleCompleted = async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("todos")
|
||||
.update({ is_complete: !isCompleted })
|
||||
.eq("id", todo.id)
|
||||
.single();
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
setIsCompleted(data.is_complete);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={"p-3 max-h-14 flex align-center justify-between border"}
|
||||
>
|
||||
<span className={"truncate flex-grow"}>
|
||||
<input
|
||||
className="cursor-pointer mr-2"
|
||||
onChange={toggleCompleted}
|
||||
type="checkbox"
|
||||
checked={isCompleted ? true : ""}
|
||||
/>
|
||||
<span
|
||||
className={`w-full flex-grow ${
|
||||
isCompleted ? "line-through" : ""
|
||||
}`}
|
||||
>
|
||||
{todo.task}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
className={"font-mono text-red-500 text-xl border px-2"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TodoItem;
|
||||
@@ -0,0 +1,13 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind 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 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { REACT_APP_SUPABASE_URL, REACT_APP_SUPABASE_KEY } from './constants';
|
||||
|
||||
export const supabase = createClient(REACT_APP_SUPABASE_URL, REACT_APP_SUPABASE_KEY);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const REACT_APP_SUPABASE_URL=process.env.REACT_APP_SUPABASE_URL
|
||||
export const REACT_APP_SUPABASE_KEY=process.env.REACT_APP_SUPABASE_KEY
|
||||
@@ -0,0 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user