Hands-on: Note Taking App
Step-by-Step Guide to Creating a Note Taking App with Vite, React, and Supabase
This guide provides a step-by-step update for enhancing the Note-Taking App by integrating authentication, user session management, and CRUD operations with a Supabase database. The updated app will feature a Header with the app title and user email, a SignUp page, and a Dashboard where users can see, create, update, and delete notes.
Step 1: Install Node.js
Before you start, ensure you have Node.js installed. You can check your installation by running:
node -v
npm -v
If Node.js is not installed, download and install it from the official website.
Step 2: Create a New Vite Project
Vite is a fast build tool for modern web applications. To create a new Vite-powered React project, run the following command at Git-bash terminal or PowerShell terminal:
npm create vite@latest react-note-taker-app --template react
Alternatively, if you are using Yarn or PNPM, you can use:
yarn create vite@latest react-note-taker-app --template react
pnpm create vite@latest react-note-taker-app --template react
Step 3: Move into the Project Directory
Once the project is created, navigate into the project folder:
cd react-note-taker-app
Step 4: Install Dependencies
Run the following command to install the necessary dependencies:
npm install
This will install all required packages specified in package.json
.
Step 5: Start the Development Server
Then, open Visual Studio Code IDE and open a new terminal . To start the development server and preview the application, run:
npm run dev
This command will provide a local development URL (e.g., http://localhost:5173/
) where you can see your app in action.
Now, delete all unnecessary code snippet in App.jsx to get started a new React App creation.
Step 6: Connect to Supabase
a. Create a Supabase Project
Go to Supabase and sign up.
Create a new project and note down the
Project URL
andAnon Key
.
b. Install Supabase Client
In your project directory, install the Supabase client:
npm install @supabase/supabase-js
c. Configure Supabase
Create a .env
file in the root of your project and add:
VITE_SUPABASE_URL=your_supabase_project_url
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
Then, in src/
supabaseClient.js
, initialize Supabase:
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
Now your project is set up with Vite, React, and Supabase!
Step 7: Create a Database Table for NoteTaking App
Navigate to the Supabase Dashboard → Table Editor and create a new table called notes with the following columns:
id
(UUID, Primary Key, Default:gen_random_uuid()
)title
(Text, Not Null)content
(Text, Not Null)date
(Timestamp, Default:now()
)iscompleted
(Boolean, Default:false
)user_id
(UUID, Foreign Key referencing auth.users, Not Null)
Step 8: Create Authentication Components(final)
To enable user authentication, install:
npm install @supabase/auth-ui-react @supabase/auth-helpers-react
Then, Create src/components/Auth.jsx
to handle SignUp, SignIn, SignOut, Email Confirmation, and Reset Password.
a. Create Auth Component for SignUp and SignIn(final)
In src/components/Auth.jsx
, add:
// Before
import { useEffect, useState } from 'react';
import { supabase } from '../supabase';
export default function Auth() {
const [session, setSession] = useState(null);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
});
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
}, []);
const signIn = async () => {
await supabase.auth.signInWithOAuth({ provider: 'google' });
};
const signOut = async () => {
await supabase.auth.signOut();
};
return (
<div>
{session ? (
<button onClick={signOut}>Sign Out</button>
) : (
<button onClick={signIn}>Sign In with Google</button>
)}
</div>
);
}
b. Implement Password Reset (optional)
In src/components/ResetPassword.jsx
, add:
import { useState } from 'react';
import { supabase } from '../supabase';
export default function ResetPassword() {
const [email, setEmail] = useState('');
const resetPassword = async () => {
await supabase.auth.resetPasswordForEmail(email);
alert('Password reset email sent!');
};
return (
<div>
<input placeholder='Email' onChange={(e) => setEmail(e.target.value)} />
<button onClick={resetPassword}>Reset Password</button>
</div>
);
}
Step 9: Update App Header
Modify src/components/Header.jsx
to show the app title and logged-in user email.
import { useEffect, useState } from 'react';
import { Navbar, Container, Button } from 'react-bootstrap';
import { supabase } from '../supabaseClient';
const Header = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const { data: { user } } = await supabase.auth.getUser();
setUser(user);
};
fetchUser();
}, []);
const handleSignOut = async () => {
await supabase.auth.signOut();
window.location.reload();
};
return (
<Navbar bg="dark" variant="dark">
<Container>
<Navbar.Brand>Note Taking App</Navbar.Brand>
{user && (
<span className="text-white">{user.email} <Button variant="danger" onClick={handleSignOut}>Sign Out</Button></span>
)}
</Container>
</Navbar>
);
};
export default Header;
Step 10: Create the Dashboard Page
In src/pages/Dashboard.jsx
, add:
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
import { Container, Button, Form } from 'react-bootstrap';
import Note from '../components/Note';
const Dashboard = () => {
const [notes, setNotes] = useState([]);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
useEffect(() => {
fetchNotes();
}, []);
const fetchNotes = async () => {
const { data, error } = await supabase.from('notes').select('*');
if (!error) setNotes(data);
};
const addNote = async (e) => {
e.preventDefault();
const { data, error } = await supabase.from('notes').insert([{ title, content, date: new Date(), iscompleted: false }]);
if (!error) setNotes([...notes, ...data]);
setTitle('');
setContent('');
};
const updateNote = async (id, iscompleted) => {
await supabase.from('notes').update({ iscompleted }).match({ id });
setNotes(notes.map(note => note.id === id ? { ...note, iscompleted } : note));
};
const deleteNote = async (id) => {
await supabase.from('notes').delete().match({ id });
setNotes(notes.filter(note => note.id !== id));
};
return (
<Container className="mt-4">
<h2>Manage Your Notes</h2>
<Form onSubmit={addNote}>
<Form.Group>
<Form.Control type="text" placeholder="Title" value={title} onChange={(e) => setTitle(e.target.value)} required />
</Form.Group>
<Form.Group>
<Form.Control as="textarea" rows={3} placeholder="Content" value={content} onChange={(e) => setContent(e.target.value)} required />
</Form.Group>
<Button type="submit">Add Note</Button>
</Form>
<div className="mt-4">
{notes.map(note => (
<Note key={note.id} note={note} onUpdate={updateNote} onDelete={deleteNote} />
))}
</div>
</Container>
);
};
export default Dashboard;
Now your NoteTaker app includes authentication, password reset, and a fully functional dashboard to create, view, update, and delete notes using Supabase!
Step 11: Update App Header(final)
Modify src/components/Header.jsx
to show the app title and logged-in user email, with a Sign Out button.
// src/components/Header.jsx
import { useEffect, useState } from 'react';
import { Navbar, Container, Button } from 'react-bootstrap';
import { supabase } from '../supabaseClient';
import { useNavigate } from 'react-router-dom';
const Header = () => {
const [user, setUser] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const fetchUser = async () => {
const { data: { user } } = await supabase.auth.getUser();
setUser(user);
};
fetchUser();
}, []);
const handleSignOut = async () => {
await supabase.auth.signOut();
navigate('/');
};
return (
<Navbar bg="dark" variant="dark">
<Container>
<Navbar.Brand>Note Taking App</Navbar.Brand>
{user && (
<span className="text-white">{user.email} <Button variant="danger" onClick={handleSignOut}>Sign Out</Button></span>
)}
</Container>
</Navbar>
);
};
export default Header;
Step 12: Create a Dashboard Component with Card Display for Notes
Modify src/pages/Dashboard.jsx
to manage notes and display them in card format.
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
import { Container, Button, Card } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
const Dashboard = () => {
const [notes, setNotes] = useState([]);
const navigate = useNavigate();
useEffect(() => {
const checkUser = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (!user) navigate('/');
};
checkUser();
fetchNotes();
}, []);
const fetchNotes = async () => {
const { data, error } = await supabase.from('notes').select('*');
if (!error) setNotes(data);
};
const deleteNote = async (id) => {
await supabase.from('notes').delete().match({ id });
setNotes(notes.filter(note => note.id !== id));
};
return (
<Container className="mt-4">
<h2>Manage Your Notes</h2>
<div className="mt-4">
{notes.map(note => (
<Card key={note.id} className="mb-3">
<Card.Body>
<Card.Title>{note.title}</Card.Title>
<Card.Text><strong>Date:</strong> {new Date(note.date).toLocaleDateString()}</Card.Text>
<Card.Text>{note.content}</Card.Text>
<Card.Text><strong>Status:</strong> {note.iscompleted ? 'Completed' : 'Not Completed'}</Card.Text>
<Button variant="danger" onClick={() => deleteNote(note.id)}><i className="bi bi-trash"></i> Delete</Button>
</Card.Body>
</Card>
))}
</div>
</Container>
);
};
export default Dashboard;
Step 13: Create a Parent Component App.jsx (final)
App.jsx (final)
Create src/App.jsx
to integrate the Header, Dashboard, User Authentication, and Routes.
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Auth from './components/Auth';
import Header from './components/Header';
import Dashboard from './pages/Dashboard';
function App() {
return (
<Router>
<Header />
<Routes>
<Route path="/" element={<Auth />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Router>
);
}
export default App;
Step 14: Create a Dashboard Component with Card Display for Notes(final)
Modify src/pages/Dashboard.jsx
to manage notes and display them in card format.
// src/components/Dashboard.jsx
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
import { Container, Button, Card } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
const Dashboard = () => {
const [notes, setNotes] = useState([]);
const navigate = useNavigate();
useEffect(() => {
const checkUser = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (!user) navigate('/');
};
checkUser();
fetchNotes();
}, []);
const fetchNotes = async () => {
const { data, error } = await supabase.from('notes').select('*');
if (!error) setNotes(data);
};
return (
<Container className="mt-4">
<h2>Manage Your Notes</h2>
<div className="mt-4">
{notes.map(note => (
<Card key={note.id} className="mb-3">
<Card.Body>
<Card.Title>{note.title}</Card.Title>
<Card.Text><strong>Date:</strong> {new Date(note.date).toLocaleDateString()}</Card.Text>
<Card.Text>{note.content}</Card.Text>
<Card.Text><strong>Status:</strong> {note.iscompleted ? 'Completed' : 'Not Completed'}</Card.Text>
</Card.Body>
</Card>
))}
</div>
</Container>
);
};
export default Dashboard;
Step 15: Create a Note Taking Feature(final)
In src/components/FormNote.jsx
, add:
// src/components/FormNote.jsx
import { useState } from 'react'
import supabase from '../supabaseClient'
import { Form, Button } from 'react-bootstrap'
const FormNote = ({ refreshNotes }) => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const addNote = async (e) => {
e.preventDefault()
const { data, error } = await supabase
.from('notes')
.insert([{ title, content, date: new Date(), iscompleted: false }])
if (!error) {
setTitle('')
setContent('')
refreshNotes()
}
}
return (
<Form onSubmit={addNote} className="mb-4">
<Form.Group className="mb-3">
<Form.Control
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Control
as="textarea"
rows={3}
placeholder="Content"
value={content}
onChange={(e) => setContent(e.target.value)}
required
/>
</Form.Group>
<Button type="submit" variant="primary">
Add Note
</Button>
</Form>
)
}
export default FormNote
The updated app now includes:
A
FormNote
component to handle both note creation and updating.Dashboard functionality to create, update, and delete notes.
A new Update button that allows users to edit existing notes.
Step 16: Run the Application
Start the development server:
npm run dev
Conclusion
The updated app now includes a parent App.jsx
component that manages routing and integrates authentication, dashboard, and header components for a complete user experience.
Challenge: Step A: Create a Note Taking Feature
In src/components/FormNote.jsx
, add:
import { useState } from 'react'
import supabase from '../supabaseClient'
import { Form, Button } from 'react-bootstrap'
const FormNote = ({ refreshNotes }) => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const addNote = async (e) => {
e.preventDefault()
const { data, error } = await supabase
.from('notes')
.insert([{ title, content, date: new Date(), iscompleted: false }])
if (!error) {
setTitle('')
setContent('')
refreshNotes()
}
}
return (
<Form onSubmit={addNote} className="mb-4">
<Form.Group className="mb-3">
<Form.Control
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Control
as="textarea"
rows={3}
placeholder="Content"
value={content}
onChange={(e) => setContent(e.target.value)}
required
/>
</Form.Group>
<Button type="submit" variant="primary">
Add Note
</Button>
</Form>
)
}
export default FormNote
Step B: SQL Command Operation on Supabase DB table for Policies Setting
// Some code
-- Enable extensions required for UUID generation
create extension if not exists "pgcrypto";
-- Create the 'notes' table
create table public.notes (
id uuid primary key default gen_random_uuid(),
title text not null,
content text not null,
date timestamp default now() not null,
isCompleted boolean default false not null,
user_id uuid references auth.users(id) on delete cascade not null
);
-- Enable Row Level Security (RLS) on the 'notes' table
alter table public.notes enable row level security;
-- Create policies to allow users to interact only with their own notes
-- Allow users to insert their own notes
create policy "Allow users to insert their own notes"
on public.notes
for insert
with check (auth.uid() = user_id);
-- Allow users to read only their own notes
create policy "Allow users to read their own notes"
on public.notes
for select
using (auth.uid() = user_id);
-- Allow users to update only their own notes
create policy "Allow users to update their own notes"
on public.notes
for update
using (auth.uid() = user_id);
-- Allow users to delete only their own notes
create policy "Allow users to delete their own notes"
on public.notes
for delete
using (auth.uid() = user_id);
Step C: Fetch and Display Notes
In src/components/NoteList.js
:
import { useEffect, useState } from 'react';
import { supabase } from '../supabase';
export default function NoteList() {
const [notes, setNotes] = useState([]);
useEffect(() => {
const fetchNotes = async () => {
const { data, error } = await supabase.from('notes').select('*');
if (!error) setNotes(data);
};
fetchNotes();
}, []);
return (
<ul>
{notes.map((note) => (
<li key={note.id}>{note.title}: {note.content}</li>
))}
</ul>
);
}
Now your NoteTaker app is fully functional with authentication, database integration, and note management!
Step D: Create a Database Table in Supabase
a. Access Supabase Table Editor
Go to Supabase and log in.
Open your project dashboard.
Navigate to the Table Editor on the left sidebar.
Click New Table .
b. Define Table Schema
Create a new table named notes
with the following columns:
id (UUID, Primary Key, Default:
gen_random_uuid()
, Not Null)title (Text, Not Null)
content (Text, Not Null)
date (Timestamp, Default:
now()
, Not Null)isCompleted (Boolean, Default:
false
, Not Null)user_id (UUID, Foreign Key referencing
auth.users.id
, Not Null)
c. Save and Deploy
Click Save to create the table.
Ensure the table permissions allow
Authenticated
users to perform CRUD operations.
d. Configure Row-Level Security (RLS)
Navigate to the Authentication → Policies section.
Click New Policy and select the
notes
table.Add a rule that allows users to access only their own notes:
CREATE POLICY "Enable read access for authenticated users"
ON "public"."notes"
FOR SELECT USING (auth.uid() = user_id);
Add policies for insert, update, and delete accordingly.
Now, your Supabase database table is set up and secured!
As an alternative better way, use the SQL command to create the notes
table in Supabase with Row Level Security (RLS) enabled:
sql
-- Enable extensions required for UUID generation
create extension if not exists "pgcrypto";
-- Create the 'notes' table
create table public.notes (
id uuid primary key default gen_random_uuid(),
title text not null,
content text not null,
date timestamp default now() not null,
isCompleted boolean default false not null,
user_id uuid references auth.users(id) on delete cascade not null
);
-- Enable Row Level Security (RLS) on the 'notes' table
alter table public.notes enable row level security;
-- Create policies to allow users to interact only with their own notes
-- Allow users to insert their own notes
create policy "Allow users to insert their own notes"
on public.notes
for insert
with check (auth.uid() = user_id);
-- Allow users to read only their own notes
create policy "Allow users to read their own notes"
on public.notes
for select
using (auth.uid() = user_id);
-- Allow users to update only their own notes
create policy "Allow users to update their own notes"
on public.notes
for update
using (auth.uid() = user_id);
-- Allow users to delete only their own notes
create policy "Allow users to delete their own notes"
on public.notes
for delete
using (auth.uid() = user_id);
UUID Setting in RLS Policies
We have to set the column in the database table with a default of auth.uid().
Type: uuid
Default Value: auth.uid()

SQL Command Explanation
The
id
column is a UUID primary key generated usinggen_random_uuid()
.The
user_id
column references theauth.users.id
field, ensuring that each note belongs to a specific authenticated user.RLS is enabled to ensure users can only access their own data.
Four security policies are created to allow authenticated users to insert, select, update, and delete only their own notes.
You can execute this SQL command in the SQL Editor of your Supabase project to create and secure the notes
table. 🚀
Step E: Integrate Components into App.jsx
a. See step F to create App.jsx
App.jsx
Modify src/App.jsx
to integrate authentication and note-taking functionality:
b. Update Dashboard.jsx
Dashboard.jsx
Ensure Dashboard.jsx
manages note creation, retrieval, update, and deletion:
Final Steps
Run
npm run dev
to start the development server.Test authentication and note management features.
Deploy the project using Vercel or Netlify for hosting.
Now your Vite React Note Taker App is fully functional with authentication and CRUD operations using Supabase! 🚀
Repeat Step 13 to accomodate sign out functionality.
Note on 403 Forbidden Error
Step F: Integrate Components into App.jsx
a. Create App.jsx
App.jsx
Modify src/App.jsx
to integrate authentication and note-taking functionality:
//src/App.jsx
import { useState, useEffect } from 'react';
import { supabase } from './supabase';
import Auth from './components/Auth';
import Dashboard from './pages/Dashboard';
export default function App() {
const [session, setSession] = useState(null);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
});
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
}, []);
return (
<div>
{session ? <Dashboard session={session} /> : <Auth />}
</div>
);
}
b. Update Dashboard.jsx
Dashboard.jsx
Ensure Dashboard.jsx
includes a sign-out button and correctly saves notes:
// src/pages/Dashboard.jsx
import { useEffect, useState } from 'react';
import { supabase } from '../supabase';
export default function Dashboard({ session }) {
const [notes, setNotes] = useState([]);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
useEffect(() => {
const fetchNotes = async () => {
const { data, error } = await supabase.from('notes').select('*').eq('user_id', session.user.id);
if (!error) setNotes(data);
};
fetchNotes();
}, [session]);
const createNote = async () => {
const { data, error } = await supabase.from('notes').insert([
{ title, content, user_id: session.user.id }
]);
if (!error) setNotes([...notes, data[0]]);
};
const signOut = async () => {
await supabase.auth.signOut();
};
return (
<div>
<h1>Dashboard</h1>
<button onClick={signOut}>Sign Out</button>
<input placeholder='Title' value={title} onChange={(e) => setTitle(e.target.value)} />
<textarea placeholder='Content' value={content} onChange={(e) => setContent(e.target.value)} />
<button onClick={createNote}>Create Note</button>
<ul>
{notes.map((note) => (
<li key={note.id}>{note.title}: {note.content}</li>
))}
</ul>
</div>
);
}
c. Update Header.jsx
// src/components/Header.jsx
import { useEffect, useState } from 'react'
import { Navbar, Container, Button } from 'react-bootstrap'
import supabase from '../supabaseClient'
import { useNavigate } from 'react-router-dom'
const Header = () => {
const [user, setUser] = useState(null)
const navigate = useNavigate()
useEffect(() => {
const fetchUser = async () => {
const {
data: { user },
} = await supabase.auth.getUser()
setUser(user)
}
fetchUser()
}, [])
const handleSignOut = async () => {
await supabase.auth.signOut()
setUser(null)
navigate('/')
}
return (
<Navbar bg="dark" variant="dark">
<Container>
<Navbar.Brand>Note Taking App</Navbar.Brand>
{user ? (
<span className="text-white mr-2">
{user.email}{' '}
<Button variant="danger" onClick={handleSignOut}>
Sign Out
</Button>
</span>
) : null}
</Container>
</Navbar>
)
}
export default Header
Step G: Modify FormNote.jsx
to Include Date Input and Completion Checkbox
FormNote.jsx
to Include Date Input and Completion CheckboxIn src/components/FormNote.jsx
, update the form to include an input for the creation date and a checkbox for task completion:
// src/components/FormNote.jsx
import { useState } from 'react';
import { supabase } from '../supabase';
export default function NoteForm({ onNoteAdded }) {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
const [isCompleted, setIsCompleted] = useState(false);
const createNote = async () => {
const { data, error } = await supabase.from('notes').insert([
{ title, content, date, isCompleted }
]);
if (!error) {
onNoteAdded(data[0]);
setTitle('');
setContent('');
setDate(new Date().toISOString().split('T')[0]);
setIsCompleted(false);
}
};
return (
<div>
<input placeholder='Title' value={title} onChange={(e) => setTitle(e.target.value)} />
<textarea placeholder='Content' value={content} onChange={(e) => setContent(e.target.value)} />
<input type='date' value={date} onChange={(e) => setDate(e.target.value)} />
<label>
<input type='checkbox' checked={isCompleted} onChange={(e) => setIsCompleted(e.target.checked)} />
Completed
</label>
<button onClick={createNote}>Create Note</button>
</div>
);
}
Step H: Modify NoteList.jsx
to Display Date, Completion Checkbox, and Delete Button
NoteList.jsx
to Display Date, Completion Checkbox, and Delete ButtonIn src/components/NoteList.jsx
, update the list to show the note creation date, allow marking completion, and add a delete button:
import { useEffect, useState } from 'react';
import { supabase } from '../supabase';
import 'bootstrap-icons/font/bootstrap-icons.css';
export default function NoteList() {
const [notes, setNotes] = useState([]);
useEffect(() => {
const fetchNotes = async () => {
const { data, error } = await supabase.from('notes').select('*');
if (!error) setNotes(data);
};
fetchNotes();
}, []);
const deleteNote = async (id) => {
await supabase.from('notes').delete().match({ id });
setNotes(notes.filter(note => note.id !== id));
};
return (
<ul>
{notes.map((note) => (
<li key={note.id}>
<h3>{note.title}</h3>
<p>{note.content}</p>
<p><strong>Date Created:</strong> {note.date}</p>
<label>
<input type='checkbox' checked={note.isCompleted} readOnly />
Completed
</label>
<button onClick={() => deleteNote(note.id)}>
<i className='bi bi-trash'></i>
</button>
</li>
))}
</ul>
);
}
Step I ::
Step J: Run and Test Your Application
Start your development server with
npm run dev
.Add new notes using the form, ensuring they store
date
andisCompleted
.Ensure the delete button removes notes successfully.
Verify that notes are stored and retrieved correctly from Supabase.
Now your Vite React Note Taker App includes a date input for note creation, a checkbox for marking completion, and a delete button using Bootstrap icons! 🚀
Last updated