Build a modern social app with a stunning UI with a native mobile feel, a special tech stack, an infinite scroll feature, and amazing performance using React JS, Appwrite, TypeScript, and more.
Visit the Figma Design and see how the feature should look like
Demo
This project was developed using
1. Create a React Project using Vite1
I. Go to Vite.js and click on Get Started
II. Run command:
1
npm create vite@latest ./
III. Configure the Vite Project
You will be asked a few questions:
1
2
3
4
5
Need to install the following packages:
create-vite@4.4.1
Ok to proceed? (y) no / `yes`
Select a Framework: › `React`
Select a variant? › `Javascript`
IV. Run command:
1
2
npm install
npm run dev
2. Customize the Project (SetUp)2
A. Starting Point Of The Project: Delete
the src
folder and create a new one with the files App.tsx
, main.tsx
and edit the files (more info)
B. Create a global.css
file and add the tailwind classes that we will use in the project. (more info) (Github gist)
C. Install Tailwind in the project with Vite.
1
2
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Note: it will show an error if I run the project at this point. the
bg-dark-1
andfont-Inter
needs to be install (step D)
D. Modify the tailwind.config
file by adding the info in the Github gist
- Install the necesary plugins:
1
npm install -D tailwindcss-animate
Make sure to install the plugin and modify (more info)
3. Routing3
Install React Router DOM (repo details)
1
npm i react-router-dom
A. Edit the main.tsx
file:
1
2
3
4
5
6
7
8
9
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
B. Edit the App.tsx
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Routes, Route } from "react-router-dom";
import "./globals.css";
import SigninForm from "./_auth/forms/SigninForm";
import SignupForm from "./_auth/forms/SignupForm";
import { Home } from "./_root/Pages";
import AuthLayout from "./_auth/AuthLayout";
import RootLayout from "./_root/RootLayout";
const App = () => {
return (
<main className="flex h-screen">
<Routes>
{/* Public Routes */}
<Route path="/sign-in" element={<SigninForm />} />
<Route path="/sign-up" element={<SignupForm />} />
{/* Private Routes */}
<Route index element={<Home />} />
</Routes>
</main>
);
};
export default App;
4. File & Folder Structure4
A. Create & edit the files:
- Create two folders: "src/_auth" for the Public content: sign-in & register pages. "src/_root" for the Private content once the user sign in
- Create a SinginForm.tsx file in the path: "src/_auth/forms/SinginForm.tsx". Edit the file using the rafce (reactArrowFunction). And Ensure to delete the import React from 'react';
- Create a SingupForm.tsx file in the path: "src/_auth/forms/SingupForm.tsx". Edit the file using the rafce (reactArrowFunction). And Ensure to delete the import React from 'react';
- Create a AuthLayout.tsx file in the path: "src/_auth/AuthLayout.tsx". Edit the file using the rafce (reactArrowFunction). And Ensure to delete the import React from 'react';
- Create a Home.tsx file in the path: "src/_root/pages/Home.tsx". Edit the file using the rafce (reactArrowFunction). And Ensure to delete the import React from 'react';
- Create a index.tsx file in the path: "src/_root/pages/index.tsx". Edit the file with the code:
export { default as Home } from "./Home";
- Create a RootLayout.tsx file in the path: "src/_root/RootLayout.tsx". Edit the file using the rafce (reactArrowFunction). And Ensure to delete the import React from 'react';
B. Edit the App.tsx
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Routes, Route } from "react-router-dom";
import "./globals.css";
import SigninForm from "./_auth/forms/SigninForm";
import SignupForm from "./_auth/forms/SignupForm";
import { Home } from "./_root/Pages";
import AuthLayout from "./_auth/AuthLayout";
import RootLayout from "./_root/RootLayout";
const App = () => {
return (
<main className="flex h-screen">
<Routes>
{/* Public Routes */}
<Route element={<AuthLayout />}>
<Route path="/sign-in" element={<SigninForm />} />
<Route path="/sign-up" element={<SignupForm />} />
</Route>
{/* Private Routes */}
<Route element={<RootLayout />}>
<Route index element={<Home />} />
</Route>
</Routes>
</main>
);
};
export default App;
5. Auth Pages5
I. SetUp Shadcn UI in the Project
a. Install Shadcn UI for Vite.
Shadcn UI. is a library with multiple designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source. it will be use to style the page.
- Edit
tsconfig.json
file by adding the following code to the paths section:1 2 3 4 5 6 7 8 9 10 11 12
{ "compilerOptions": { // ... "baseUrl": ".", "paths": { "@/*": [ "./src/*" ] } // ... } }
The tsconfig.json file should look like the one in the link.
b. Install Node.
1
npm i -D @types/node
c. Overwrite the vite.config.ts
file:
1
2
3
4
5
6
7
8
9
10
11
12
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
d. Run the shadcn-ui init command to setup your project:
1
npx shadcn-ui@latest init
e. Configure components.json
You will be asked a few questions to configure components.json
:
1
2
3
4
5
6
7
8
9
Would you like to use TypeScript (recommended)? no / `yes`
Which style would you like to use? › `Default`
Which color would you like to use as base color? › `Slate`
Where is your global CSS file? › › `src/index.css`
Do you want to use CSS variables for colors? › no / `yes`
Where is your tailwind.config.js located? › `tailwind.config.js`
Configure the import alias for components: › `@/components`
Configure the import alias for utils: › `@/lib/utils`
Are you using React Server Components? › no / `yes` (no)
This will install all the necesary components
II. Implement the AuthLayout Page
a. Delete the public
folder and replace it with the info from this new public folder.
b. Modify the AuthLayout.tsx
file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Outlet, Navigate } from "react-router-dom";
const AuthLayout = () => {
const isAutenticated = false;
return (
<>
{isAutenticated ? (
<Navigate to="/" />
) : (
<>
<section className="flex flex-1 justify-center items-center flex-col py-10">
<Outlet />
</section>
<img
src="/assets/images/side-img.svg"
alt="side image"
className="hidden xl:block h-screen w-1/2 object-cover bg-no-repeat"
/>
</>
)}
</>
);
};
export default AuthLayout;
c. Modify the SignupForm.tsx
file
- Install the
button
component from shadcn:1
npx shadcn-ui@latest add button
- Copy and past to
SignupForm.tsx
file1 2 3 4 5 6 7 8 9 10 11
import { Button } from "@/components/ui/button"; const SignupForm = () => { return ( <div> <Button>click me</Button> </div> ); }; export default SignupForm;
confirm it works.
III. Create the Form for the project (which has Form, input and label components)
a. Go to Shadcn-ui Components forms.
Follow the steps on the Shadcn-UI site and check the documentation for more details.
- Install form component
1
npx shadcn-ui@latest add form
- Install input component
1
npx shadcn-ui@latest add input
-
Modify the
SignupForm.tsx
file1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useForm } from "react-hook-form"; const formSchema = z.object({ username: z.string().min(2).max(50), }); const SignupForm = () => { // 1. Define your form. const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { username: "", }, }); // 2. Define a submit handler. function onSubmit(values: z.infer<typeof formSchema>) { // Do something with the form values. // ✅ This will be type-safe and validated. console.log(values); } return ( <div> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <FormField control={form.control} name="username" render={({ field }) => ( <FormItem> <FormLabel>Username</FormLabel> <FormControl> <Input placeholder="shadcn" {...field} /> </FormControl> <FormDescription> This is your public display name. </FormDescription> <FormMessage /> </FormItem> )} /> <Button type="submit">Submit</Button> </form> </Form> </div> ); }; export default SignupForm;
This is a basic but powerful form.
-
Create Reusable Components by extracting the
formSchema
a. Create an
index.ts
file insrc/lib/validation/index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import * as z from "zod"; export const SignupValidation = z.object({ name: z .string() .min(2, { message: "Name is too short" }) .max(50, { message: "Name is too long" }), username: z .string() .min(2, { message: "Username is too short" }) .max(50, { message: "Username is too short" }), email: z.string().email(), password: z .string() .min(8, { message: "Password must be at least 8 characters" }) .max(60, { message: "Password is too long" }), });
b. Modify the
SignupForm.tsx
file to reuse the validation sectionThis will modify the functionality of form but not the apperience.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useForm } from "react-hook-form"; import { SignupValidation } from "@/lib/validation"; import { z } from "zod"; const SignupForm = () => { // 1. Define your form. const form = useForm<z.infer<typeof SignupValidation>>({ resolver: zodResolver(SignupValidation), defaultValues: { name: "", username: "", email: "", password: "", }, }); // 2. Define a submit handler. function onSubmit(values: z.infer<typeof SignupValidation>) { // Do something with the form values. // ✅ This will be type-safe and validated. console.log(values); } return ( <div> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <FormField control={form.control} name="username" render={({ field }) => ( <FormItem> <FormLabel>Username</FormLabel> <FormControl> <Input placeholder="shadcn" {...field} /> </FormControl> <FormDescription> This is your public display name. </FormDescription> <FormMessage /> </FormItem> )} /> <Button type="submit">Submit</Button> </form> </Form> </div> ); }; export default SignupForm;
c. Modify the
SignupForm.tsx
file to display the form section on the sign up page1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useForm } from "react-hook-form"; import { SignupValidation } from "@/lib/validation"; import { z } from "zod"; const SignupForm = () => { const isLoading = true; // 1. Define your form. const form = useForm<z.infer<typeof SignupValidation>>({ resolver: zodResolver(SignupValidation), defaultValues: { name: "", username: "", email: "", password: "", }, }); // 2. Define a submit handler. function onSubmit(values: z.infer<typeof SignupValidation>) { // Do something with the form values. // ✅ This will be type-safe and validated. console.log(values); } return ( <Form {...form}> <div className="sm:w-420 flex-center flex-col"> <img src="/assets/images/logo.svg" alt="logo" /> <h2 className="h3-bold md:h2-bold pt-5 sm:pt-12"> Create a new account </h2> <p className="text-light-3 small-medium md:base-regular mt-2"> To use Snapgram enter your details </p> <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-5 w-full mt-4" > <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Name</FormLabel> <FormControl> <Input type="text" className="shad-input" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="username" render={({ field }) => ( <FormItem> <FormLabel>Username</FormLabel> <FormControl> <Input type="text" className="shad-input" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input type="email" className="shad-input" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="password" render={({ field }) => ( <FormItem> <FormLabel>Password</FormLabel> <FormControl> <Input type="password" className="shad-input" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit" className="shad-button_primary"> {isLoading ? ( <div className="flex-center gap-2">Loading...</div> ) : ( "Sign up" )} </Button> </form> </div> </Form> ); }; export default SignupForm;
d. The styles in the project were unintentionaly modify with the intalation of shadcn, go to the
global.css
file and overwrite it using the previous global.css file.e. The
tailwind.config
file got modify too. Overwrite it using the previous tailwind.config file.
IV. Add the Loader Component
a. Create the Loader.tsx
file in the path: src/components/shared/Loader.tsx
and copy the code:
1
2
3
4
5
6
7
8
const Loader = () => {
return (
<div className="flex-center w-full">
<img src="/assets/icons/loader.svg" alt="loader" width={24} height={24} />
</div>
);
};
export default Loader;
Include the Loader component to the SignupForm.tsx
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "react-router-dom";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useForm } from "react-hook-form";
import { SignupValidation } from "@/lib/validation";
import { z } from "zod";
import Loader from "@/components/shared/Loader";
const SignupForm = () => {
const isLoading = false;
// 1. Define your form.
const form = useForm<z.infer<typeof SignupValidation>>({
resolver: zodResolver(SignupValidation),
defaultValues: {
name: "",
username: "",
email: "",
password: "",
},
});
// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof SignupValidation>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values);
}
return (
<Form {...form}>
<div className="sm:w-420 flex-center flex-col">
<img src="/assets/images/logo.svg" alt="logo" />
<h2 className="h3-bold md:h2-bold pt-5 sm:pt-12">
Create a new account
</h2>
<p className="text-light-3 small-medium md:base-regular mt-2">
To use Snapgram, Please enter your details
</p>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-5 w-full mt-4"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input type="text" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="shad-button_primary">
{isLoading ? (
<div className="flex-center gap-2">
<Loader />
Loading...
</div>
) : (
"Sign up"
)}
</Button>
<p className="text-small-regular text-light-2 text-center mt-2">
Already have an account?
<Link
to="/sign-in"
className="text-primary-500 text-small-semibold ml-1"
>
Log in
</Link>
</p>
</form>
</div>
</Form>
);
};
export default SignupForm;
6. Auth Functionality6
I. Appwrite
A. Install appwrite dependency
1
npm install appwrite
B. Go to cloud.appwrite.io, login with Github and create a new project.
C. In Appwrite, Copy the Project ID
provided.
D. Create a config.ts
file located in the path src/lib/appwrite/config.ts
and add the code below:
1
2
3
4
5
import { Client, Account, Databases, Storage, Avatars } from "appwrite";
export const appwriteConfig = {
projectId: import.meta.env.VITE_APPWRITE_PROJECT_ID,
};
E. Create a .env.local
file OUTSIDE
of the src
. In the path ./.env.local
and add the code below:
Warning: Make sure it’s save at the
root
of the project and not in thesrc
folder (I had issues bc I save it in the wrong place.)
1
VITE_APPWRITE_PROJECT_ID='YOUR_APPWRITE_PROJECT_ID'
Replace the ‘YOUR_APPWRITE_PROJECT_ID’ with your actual ID number.
F. Create a vite-env.d.ts
file located in the path src/vite-env.d.ts
and add the code below:
1
/// <reference types="vite/client" />
G. Modify the config.ts
file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Client, Account, Databases, Storage, Avatars } from "appwrite";
export const appwriteConfig = {
projectId: import.meta.env.VITE_APPWRITE_PROJECT_ID,
url: import.meta.env.VITE_APPWRITE_URL,
};
export const client = new Client();
client.setProject(appwriteConfig.projectId);
client.setEndpoint(appwriteConfig.url);
export const account = new Account(client);
export const databases = new Databases(client);
export const storage = new Storage(client);
export const avatars = new Avatars(client);
H. Modify the .env.local
file. Which should be located in the root of the project (./.env.local
)
1
2
VITE_APPWRITE_PROJECT_ID='YOUR_APPWRITE_PROJECT_ID'
VITE_APPWRITE_URL='https://cloud.appwrite.io/v1'
Replace the ‘YOUR_APPWRITE_PROJECT_ID’ with your actual ID number.
-
Learn More: Appwrite user Appwrite React .Env Variables
I. Create a api.ts
file in the path src/lib/appwrite/api.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ID } from "appwrite";
import { INewUser } from "@/types";
import { account } from "./config";
export async function createUserAccount(user: INewUser) {
try {
const newAccount = await account.create(
ID.unique(),
user.email,
user.password,
user.name
);
return newAccount;
} catch (error) {
console.log(error);
return error;
}
}
J. Create a index.ts
file in the path src/types/index.ts
and add the code from Github gist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
export type INavLink = {
imgURL: string;
route: string;
label: string;
};
export type IUpdateUser = {
userId: string;
name: string;
bio: string;
imageId: string;
imageUrl: URL | string;
file: File[];
};
export type INewPost = {
userId: string;
caption: string;
file: File[];
location?: string;
tags?: string;
};
export type IUpdatePost = {
postId: string;
caption: string;
imageId: string;
imageUrl: URL;
file: File[];
location?: string;
tags?: string;
};
export type IUser = {
id: string;
name: string;
username: string;
email: string;
imageUrl: string;
bio: string;
};
export type INewUser = {
name: string;
email: string;
username: string;
password: string;
};
K. Modify the SignupForm.tsx
file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "react-router-dom";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useForm } from "react-hook-form";
import Loader from "@/components/shared/Loader";
import { SignupValidation } from "@/lib/validation";
import { z } from "zod";
import { createUserAccount } from "@/lib/appwrite/api";
const SignupForm = () => {
const isLoading = false;
// 1. Define your form.
const form = useForm<z.infer<typeof SignupValidation>>({
resolver: zodResolver(SignupValidation),
defaultValues: {
name: "",
username: "",
email: "",
password: "",
},
});
// 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof SignupValidation>) {
const newUser = await createUserAccount(values);
console.log(newUser);
}
return (
<Form {...form}>
<div className="sm:w-420 flex-center flex-col">
<img src="/assets/images/logo.svg" alt="logo" />
<h2 className="h3-bold md:h2-bold pt-5 sm:pt-12">
Create a new account
</h2>
<p className="text-light-3 small-medium md:base-regular mt-2">
To use Snapgram, Please enter your details
</p>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-5 w-full mt-4"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input type="text" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="shad-button_primary">
{isLoading ? (
<div className="flex-center gap-2">
<Loader />
Loading...
</div>
) : (
"Sign up"
)}
</Button>
<p className="text-small-regular text-light-2 text-center mt-2">
Already have an account?
<Link
to="/sign-in"
className="text-primary-500 text-small-semibold ml-1"
>
Log in
</Link>
</p>
</form>
</div>
</Form>
);
};
export default SignupForm;
L. Go to the Sign up page (http://localhost:5173/Sign-up
) and fill out the form.
M. Go to the Auth tab in the Appwrite page to check that the new user has been created. (reload page if needed)
7. Storage & Database Design7
I. Storage
A. Go to the Storage tab in the Appwrite page, click on Create bucket
and type media
under the name of the bucket.
B. Copy the Storage ID (media) and paste it in the ./.env.local
file:
1
2
3
VITE_APPWRITE_URL='https://cloud.appwrite.io/v1'
VITE_APPWRITE_PROJECT_ID='YOUR_APPWRITE_PROJECT_ID'
VITE_APPWRITE_STORAGE_ID='YOUR_APPWRITE_STORAGE_ID'
II. Database
A. Go to the Database tab in the Appwrite page, click on Create Database
and type snapgram
under the name of the database. (no need to enter a database ID, it will be generated automaticaly)
B. Copy the Database ID (snapgram) and paste it in the ./.env.local
file:
1
2
3
4
VITE_APPWRITE_URL='https://cloud.appwrite.io/v1'
VITE_APPWRITE_PROJECT_ID='YOUR_APPWRITE_PROJECT_ID'
VITE_APPWRITE_STORAGE_ID='YOUR_APPWRITE_STORAGE_ID'
VITE_APPWRITE_DATABASE_ID='YOUR_APPWRITE_DATABASE_ID'
C. Under the new Database (snapgram), click on Create Collection
and type Posts
then create.
D. Go to settings
(inside Posts) > Permissions
> +
> Any
> check all the boxes > update
.
E. Under the new Database (snapgram), click on Create Collection
and type Users
then create.
F. Go to settings
(inside Users) > Permissions
> +
> Any
> check all the boxes > update
.
G. Under the new Database (snapgram), click on Create Collection
and type Saves
then create.
D. Go to settings
(inside Saves) > Permissions
> +
> Any
> check all the boxes > update
.
III. Database Relations in Appwrite8…
A. Posts
Collection > Relationship
- Click the
Attributes
tab >Create Attribute
>Relationship
- Relationship,
- Select
Two-Way Relationship
- Related Collection:
Users
- Attribute Key:
creator
(deleteusers
) - Attribute Key(related collection):
posts
- Relation:
Many-to-One
- On deleting a document:
Null
- Create
- Select
B. Posts
Collection > Relationship
- Click the
Attributes
tab >Create Attribute
>Relationship
- Relationship,
- Select
Two-Way Relationship
- Related Collection:
Users
- Attribute Key:
likes
(deleteusers
) - Attribute Key(related collection):
liked
- Relation:
Many-to-Many
- On deleting a document:
Null
- Create
- Select
C. Posts
Collection > String
- Click the
Attributes
tab >Create Attribute
>String
- String,
- Attribute Key:
caption
- size:
2200
- Default:
null
- Create
- Attribute Key:
D. Posts
Collection > String
- Click the
Attributes
tab >Create Attribute
>String
- String,
- Attribute Key:
tags
- size:
2200
- Default:
null
Array
- Create
- Attribute Key:
E. Posts
Collection > URL
- Click the
Attributes
tab >Create Attribute
>URL
- URL,
- Attribute Key:
imageUrl
Required
- Create
- Attribute Key:
F. Posts
Collection > String
- Click the
Attributes
tab >Create Attribute
>String
- String,
- Attribute Key:
imageId
- size:
2200
Required
- Create
- Attribute Key:
G. Posts
Collection > String
- Click the
Attributes
tab >Create Attribute
>String
- String,
- Attribute Key:
location
- size:
2200
- Create
- Attribute Key:
Indexes tab:
Go to the indexes tab under Posts
H. Posts
Collection > String
- Click the
Indexes
tab >Create index
>String
- String,
- Index Key:
caption
- Index type:
FullText
- Attribute:
caption
- Order:
DESC
- Create
- Index Key:
Users:
I. Users
Collection > String
- Click the
Attributes
tab >Create Attribute
>String
- String,
- Attribute Key:
name
- size:
2200
- Default:
null
- Create
- Attribute Key:
J. Users
Collection > String
- Click the
Attributes
tab >Create Attribute
>String
- String,
- Attribute Key:
username
- size:
2200
- Default:
null
- Create
- Attribute Key:
K. Users
Collection > String
- Click the
Attributes
tab >Create Attribute
>String
- String,
- Attribute Key:
accountId
- size:
2200
- Default:
null
- Required
- Create
- Attribute Key:
L. Users
Collection > Email
- Click the
Attributes
tab >Create Attribute
>Email
- Email,
- Attribute Key:
email
- Required
- Create
- Attribute Key:
M. Users
Collection > String
- Click the
Attributes
tab >Create Attribute
>String
- String,
- Attribute Key:
bio
- size:
2200
- Default:
null
- Create
- Attribute Key:
N. Users
Collection > String
- Click the
Attributes
tab >Create Attribute
>String
- String,
- Attribute Key:
imageId
- size:
2200
- Default:
null
- Create
- Attribute Key:
O. Users
Collection > URL
- Click the
Attributes
tab >Create Attribute
>URL
- URL,
- Attribute Key:
imageUrl
Required
- Create
- Attribute Key:
Saves:
P. Saves
Collection > Relationship
- Click the
Attributes
tab >Create Attribute
>Relationship
- Relationship,
- Select
Two-Way Relationship
- Related Collection:
Users
- Attribute Key:
user
- Attribute Key(related collection):
save
- Relation:
Many-to-One
- On deleting a document:
Null
- Create
- Select
P. Saves
Collection > Relationship
- Click the
Attributes
tab >Create Attribute
>Relationship
- Relationship,
- Select
Two-Way Relationship
- Related Collection:
Posts
- Attribute Key:
post
- Attribute Key(related collection):
save
- Relation:
Many-to-One
- On deleting a document:
Null
- Create
- Select
IV. Integrate
-
Go to Appwrite > Select the project > database Tab > Select the database just created (snapgram)
- Copy the
collection ID
forSaves
- Copy the
collection ID
forUsers
- Copy the
collection ID
forPosts
- Paste them in the
./.env.local
file:
1
2
3
4
5
6
7
VITE_APPWRITE_URL='https://cloud.appwrite.io/v1'
VITE_APPWRITE_PROJECT_ID='YOUR_APPWRITE_PROJECT_ID'
VITE_APPWRITE_STORAGE_ID='YOUR_APPWRITE_STORAGE_ID'
VITE_APPWRITE_DATABASE_ID='YOUR_APPWRITE_DATABASE_ID'
VITE_APPWRITE_SAVES_COLLECTION_ID='YOUR_APPWRITE_SAVES_COLLECTION_ID'
VITE_APPWRITE_USER_COLLECTION_ID='YOUR_APPWRITE_USER_COLLECTION_ID'
VITE_APPWRITE_POST_COLLECTION_ID='YOUR_APPWRITE_POST_COLLECTION_ID'
- Modify the
config.ts
file (src/lib/appwrite/config.ts
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Client, Account, Databases, Storage, Avatars } from "appwrite";
export const appwriteConfig = {
url: import.meta.env.VITE_APPWRITE_URL,
projectId: import.meta.env.VITE_APPWRITE_PROJECT_ID,
databaseId: import.meta.env.VITE_APPWRITE_DATABASE_ID,
storageId: import.meta.env.VITE_APPWRITE_STORAGE_ID,
userCollectionId: import.meta.env.VITE_APPWRITE_USER_COLLECTION_ID,
postCollectionId: import.meta.env.VITE_APPWRITE_POST_COLLECTION_ID,
savesCollectionId: import.meta.env.VITE_APPWRITE_SAVES_COLLECTION_ID,
};
export const client = new Client();
// console.log(import.meta.env.MODE);
client.setEndpoint(appwriteConfig.url);
client.setProject(appwriteConfig.projectId);
console.log(appwriteConfig.url);
console.log(appwriteConfig.projectId);
export const account = new Account(client);
export const databases = new Databases(client);
export const storage = new Storage(client);
export const avatars = new Avatars(client);
Warning: Double check that your config.ts (appwrite) are using the exact same names as your ENV.LOCAL variables.
I had a
bug
When I sign up a user, the new account gets created on Appwrite. But I was only seeing it on theAuth
section and not in thedatabase > users
section on Appwrite. (Resolved it by using the EXACT same variable names)
V. Complete Back-End SetUp
- Modify the
api.ts
file (src/lib/appwrite/api.ts
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { ID } from "appwrite";
import { INewUser } from "@/types";
import { account, appwriteConfig, avatars, databases } from "./config";
export async function createUserAccount(user: INewUser) {
try {
const newAccount = await account.create(
ID.unique(),
user.email,
user.password,
user.name
);
if (!newAccount) throw Error;
const avatarUrl = avatars.getInitials(user.name);
const newUser = await saveUserToDB({
accountId: newAccount.$id,
email: newAccount.email,
name: newAccount.name,
username: user.username,
imageUrl: avatarUrl,
});
return newUser;
} catch (error) {
console.log(error);
return error;
}
}
export async function saveUserToDB(user: {
accountId: string;
email: string;
name: string;
imageUrl: URL;
username?: string;
}) {
try {
const newUser = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
ID.unique(),
user
);
return newUser;
} catch (error) {
console.log(error);
return error;
}
}
VI. Add Toast Component from Shadcn UI
A. Install Toast
1
npx shadcn-ui@latest add toast
B. Modify the App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Routes, Route } from "react-router-dom";
import "./globals.css";
import SigninForm from "./_auth/forms/SigninForm";
import SignupForm from "./_auth/forms/SignupForm";
import { Home } from "./_root/Pages";
import AuthLayout from "./_auth/AuthLayout";
import RootLayout from "./_root/RootLayout";
import { Toaster } from "@/components/ui/toaster";
const App = () => {
return (
<main className="flex h-screen">
<Routes>
{/* Public Routes */}
<Route element={<AuthLayout />}>
<Route path="/sign-in" element={<SigninForm />} />
<Route path="/sign-up" element={<SignupForm />} />
</Route>
{/* Private Routes */}
<Route element={<RootLayout />}>
<Route index element={<Home />} />
</Route>
</Routes>
<Toaster />
</main>
);
};
export default App;
C. Modify the SignUpForm.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "react-router-dom";
import { useToast } from "@/components/ui/use-toast";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useForm } from "react-hook-form";
import Loader from "@/components/shared/Loader";
import { SignupValidation } from "@/lib/validation";
import { z } from "zod";
import { createUserAccount } from "@/lib/appwrite/api";
const SignupForm = () => {
const { toast } = useToast();
const isLoading = false;
// 1. Define your form.
const form = useForm<z.infer<typeof SignupValidation>>({
resolver: zodResolver(SignupValidation),
defaultValues: {
name: "",
username: "",
email: "",
password: "",
},
});
// 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof SignupValidation>) {
const newUser = await createUserAccount(values);
if (!newUser) return;
console.log(newUser);
return toast({
title: "Sign up failed. Please try again later.",
});
}
// const session = await signInAccount();
return (
<Form {...form}>
<div className="sm:w-420 flex-center flex-col">
<img src="/assets/images/logo.svg" alt="logo" />
<h2 className="h3-bold md:h2-bold pt-5 sm:pt-12">
Create a new account
</h2>
<p className="text-light-3 small-medium md:base-regular mt-2">
To use Snapgram, Please enter your details
</p>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-5 w-full mt-4"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input type="text" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="shad-button_primary">
{isLoading ? (
<div className="flex-center gap-2">
<Loader />
Loading...
</div>
) : (
"Sign up"
)}
</Button>
<p className="text-small-regular text-light-2 text-center mt-2">
Already have an account?
<Link
to="/sign-in"
className="text-primary-500 text-small-semibold ml-1"
>
Log in
</Link>
</p>
</form>
</div>
</Form>
);
};
export default SignupForm;
8. TanStack Query (React Query)9
I. Install React Query
1
npm install @tanstrack/react-query
II. Create a queriesAndMutations.ts
file
Located in src/lib/react-query/queriesAndMutations.ts
and copy the code below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Source code: https://github.com/adrianhajdin/social_media_app
import {
useQuery,
useMutation,
useQueryClient,
useInfiniteQuery,
} from "@tanstack/react-query";
import { createUserAccount, signInAccount } from "@/lib/appwrite/api";
import { INewUser } from "@/types";
export const useCreateUserAccount = () => {
return useMutation({
mutationFn: (user: INewUser) => createUserAccount(user),
});
};
export const useSignInAccount = () => {
return useMutation({
mutationFn: (user: { email: string; password: string }) =>
signInAccount(user),
});
};
IV. Modify the SignupForm.tsx
file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import { zodResolver } from "@hookform/resolvers/zod";
import { Link, useNavigate } from "react-router-dom";
import { useToast } from "@/components/ui/use-toast";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useForm } from "react-hook-form";
import Loader from "@/components/shared/Loader";
import { SignupValidation } from "@/lib/validation";
import { z } from "zod";
import {
useCreateUserAccount,
useSignInAccount,
} from "@/lib/react-query/queriesAndMutations";
import { useUserContext } from "@/context/AuthContext";
const SignupForm = () => {
const { toast } = useToast();
const { checkAuthUser, isLoading: isUserLoading } = useUserContext();
const navigate = useNavigate();
const { mutateAsync: createUserAccount, isPending: isCreatingAccount } =
useCreateUserAccount();
const { mutateAsync: signInAccount, isPending: isSigningIn } =
useSignInAccount();
// 1. Define your form.
const form = useForm<z.infer<typeof SignupValidation>>({
resolver: zodResolver(SignupValidation),
defaultValues: {
name: "",
username: "",
email: "",
password: "",
},
});
// 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof SignupValidation>) {
const newUser = await createUserAccount(values);
if (!newUser) {
return toast({
title: "Sign up failed. Please try again later.",
});
// console.log(newUser);
}
const session = await signInAccount({
email: values.email,
password: values.password,
});
if (!session) {
return toast({
title: "Sign in failed. Please try again later.",
});
}
const isLoggedIn = await checkAuthUser();
if (isLoggedIn) {
form.reset();
navigate("/");
} else {
return toast({ title: "Sign up failed. Please try again later." });
}
}
return (
<Form {...form}>
<div className="sm:w-420 flex-center flex-col">
<img src="/assets/images/logo.svg" alt="logo" />
<h2 className="h3-bold md:h2-bold pt-5 sm:pt-12">
Create a new account
</h2>
<p className="text-light-3 small-medium md:base-regular mt-2">
To use Snapgram, Please enter your details
</p>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-5 w-full mt-4"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input type="text" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="shad-button_primary">
{isCreatingAccount || isSigningIn || isUserLoading ? (
<div className="flex-center gap-2">
<Loader />
Loading...
</div>
) : (
"Sign up"
)}
</Button>
<p className="text-small-regular text-light-2 text-center mt-2">
Already have an account?
<Link
to="/sign-in"
className="text-primary-500 text-small-semibold ml-1"
>
Log in
</Link>
</p>
</form>
</div>
</Form>
);
};
export default SignupForm;
V. Modify the api.ts
file
Located in src/lib/appwrite/api.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Source code: https://github.com/adrianhajdin/social_media_app
import { ID, Query } from "appwrite";
import { appwriteConfig, account, databases, avatars } from "./config";
import { INewUser } from "@/types";
export async function createUserAccount(user: INewUser) {
try {
const newAccount = await account.create(
ID.unique(),
user.email,
user.password,
user.name
);
if (!newAccount) throw Error;
const avatarUrl = avatars.getInitials(user.name);
const newUser = await saveUserToDB({
accountId: newAccount.$id,
email: newAccount.email,
name: newAccount.name,
username: user.username,
imageUrl: avatarUrl,
});
return newUser;
} catch (error) {
console.log(error);
return error;
}
}
export async function saveUserToDB(user: {
accountId: string;
email: string;
name: string;
imageUrl: URL;
username?: string;
}) {
try {
const newUser = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
ID.unique(),
user
);
return newUser;
} catch (error) {
console.log(error);
}
}
export async function signInAccount(user: { email: string; password: string }) {
try {
const session = await account.createEmailSession(user.email, user.password);
return session;
} catch (error) {
console.log(error);
}
}
export async function getCurrentUser() {
try {
const currentAccount = await account.get();
if (!currentAccount) throw Error;
const currentUser = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
[Query.equal("accountId", currentAccount.$id)]
);
if (!currentUser) throw Error;
return currentUser.documents[0];
} catch (error) {
console.log(error);
return null;
}
}
VI. Create a AuthContext.tsx
file
Located in src/context/AuthContext.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// Source code: https://github.com/adrianhajdin/social_media_app
import { getCurrentUser } from "@/lib/appwrite/api";
import { IContextType, IUser } from "@/types";
import { createContext, useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
export const INITIAL_USER = {
id: "",
name: "",
username: "",
email: "",
imageUrl: "",
bio: "",
};
const INITIAL_STATE = {
user: INITIAL_USER,
isLoading: false, //* check this line ---> isPending maybe
isAuthenticated: false,
setUser: () => {},
setIsAuthenticated: () => {},
checkAuthUser: async () => false as boolean,
};
const AuthContext = createContext<IContextType>(INITIAL_STATE);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<IUser>(INITIAL_USER);
const [isLoading, setIsLoading] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const navigate = useNavigate();
const checkAuthUser = async () => {
setIsLoading(true);
try {
const currentAccount = await getCurrentUser();
if (currentAccount) {
setUser({
id: currentAccount.$id,
name: currentAccount.name,
username: currentAccount.username,
email: currentAccount.email,
imageUrl: currentAccount.imageUrl,
bio: currentAccount.bio,
});
setIsAuthenticated(true);
return true;
}
return false;
} catch (error) {
console.log(error);
return false;
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (
localStorage.getItem("cookieFallback") === "[]" ||
localStorage.getItem("cookieFallback") === null
)
navigate("/sign-in");
checkAuthUser();
}, []);
const value = {
user,
setUser,
isLoading,
isAuthenticated,
setIsAuthenticated,
checkAuthUser,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export const useUserContext = () => useContext(AuthContext);
VII. Modify the main.tsx
file
Located in src/main.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Source code: https://github.com/adrianhajdin/social_media_app
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { AuthProvider } from "@/context/AuthContext";
import { QueryProvider } from "@/lib/react-query/QueryProvider";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<BrowserRouter>
<QueryProvider>
<AuthProvider>
<App />
</AuthProvider>
</QueryProvider>
</BrowserRouter>
);
VII. Create a QueryProvider.tsx
file
Located in src/lib/react-query/QueryProvider.tsx
1
2
3
4
5
6
7
8
9
10
11
12
// Source code: https://github.com/adrianhajdin/social_media_app
import React from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient();
export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
VIII. Test the Sign up Section
A. Go to the http://localhost:5173/sign-up and fill out the form.
B. Check in Appwrite if the new account has been created.
- Under Auth > Users.
- Under Databases > snapgram > Users.
C. Debugging as needed
Debugging example from the video.
IX. Modify the SigninForm.tsx
file
Located in src/_auth/forms/SigninForm.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import { zodResolver } from "@hookform/resolvers/zod";
import { Link, useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import {
Form, FormControl, FormField, FormItem, FormLabel, FormMessage,
} from "@/components/ui/form";
import { useToast } from "@/components/ui/use-toast";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { SigninValidation } from "@/lib/validation";
import { z } from "zod";
import Loader from "@/components/shared/Loader";
import { useSignInAccount } from "@/lib/react-query/queriesAndMutations";
import { useUserContext } from "@/context/AuthContext";
const SigninForm = () => {
const { toast } = useToast();
const { checkAuthUser, isLoading: isUserLoading } = useUserContext();
const navigate = useNavigate();
const { mutateAsync: signInAccount } = useSignInAccount();
// 1. Define your form.
const form = useForm<z.infer<typeof SigninValidation>>({
resolver: zodResolver(SigninValidation),
defaultValues: {
email: "",
password: "",
},
});
// 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof SigninValidation>) {
const session = await signInAccount({
email: values.email,
password: values.password,
});
if (!session) {
return toast({
title: "Sign in failed. Please try again later.",
});
}
const isLoggedIn = await checkAuthUser();
if (isLoggedIn) {
form.reset();
navigate("/");
} else {
return toast({ title: "Sign in failed. Please try again later." });
}
}
return (
<Form {...form}>
<div className="sm:w-420 flex-center flex-col">
<img src="/assets/images/logo.svg" alt="logo" />
<h2 className="h3-bold md:h2-bold pt-5 sm:pt-12">
Log in to your account
</h2>
<p className="text-light-3 small-medium md:base-regular mt-2">
Welcome back! Please enter your details to enjoy Snapgram
</p>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-5 w-full mt-4"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" className="shad-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="shad-button_primary">
{isUserLoading ? (
<div className="flex-center gap-2">
<Loader />
Loading...
</div>
) : (
"Sign in"
)}
</Button>
<p className="text-small-regular text-light-2 text-center mt-2">
Don't have an account?
<Link
to="/sign-up"
className="text-primary-500 text-small-semibold ml-1"
>
Sign up
</Link>
</p>
</form>
</div>
</Form>
);
};
export default SigninForm;
X. Modify the index.ts
file
Located in src/lib/validation/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Source code: https://github.com/adrianhajdin/social_media_app
import * as z from "zod";
export const SignupValidation = z.object({
name: z
.string()
.min(2, { message: "Name is too short" })
.max(50, { message: "Name is too long" }),
username: z
.string()
.min(2, { message: "Username is too short" })
.max(50, { message: "Username is too short" }),
email: z.string().email(),
password: z
.string()
.min(8, { message: "Password must be at least 8 characters" })
.max(60, { message: "Password is too long" }),
});
export const SigninValidation = z.object({
email: z.string().email(),
password: z
.string()
.min(8, { message: "Password must be at least 8 characters" })
.max(60, { message: "Password is too long" }),
});
9. HomePage10
I. Modify the RootLayout.tss
file
Located in src/_root/RootLayout.tsx
and copy the code below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Source code: https://github.com/adrianhajdin/social_media_app
import Bottombar from "@/components/shared/Bottombar";
import LeftSidebar from "@/components/shared/LeftSidebar";
import Topbar from "@/components/shared/Topbar";
import { Outlet } from "react-router-dom";
const RootLayout = () => {
return (
<div className="w-full md:flex">
<Topbar />
<LeftSidebar />
<section className="flex flex-1 h-full">
<Outlet />
</section>
<Bottombar />
</div>
);
};
export default RootLayout;
- Create a Component name
Topbar.tsx
Located insrc/components/shared/Topbar.tsx
and runrafce
. - Create a Component name
LeftSidebar.tsx
Located insrc/components/shared/LeftSidebar.tsx
and runrafce
. - Create a Component name
Bottombar.tsx
Located insrc/components/shared/Bottombar.tsx
and runrafce
.
II. Modify Component: Topbar.tsx
Located in src/components/shared/Topbar.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Source code: https://github.com/adrianhajdin/social_media_app
import { Link, useNavigate } from "react-router-dom";
import { Button } from "../ui/button";
import { useSignOutAccount } from "@/lib/react-query/queriesAndMutations";
import { useEffect } from "react";
import { useUserContext } from "@/context/AuthContext";
const Topbar = () => {
const { mutate: signOut, isSuccess } = useSignOutAccount();
const navigate = useNavigate();
const { user } = useUserContext();
useEffect(() => {
if (isSuccess) navigate(0);
}, [isSuccess]);
return (
<section className="topbar">
<div className="flex-between py-4 px-5">
<Link to="/" className="flex gap-3 items-center">
<img
src="/assets/images/logo.svg"
alt="logo"
width={130}
height={325}
/>
</Link>
<div className="flex gap-4">
<Button
variant="ghost"
className="shad-button_ghost"
onClick={() => signOut()}
>
<img src="/assets/icons/logout.svg" alt="logout" />
</Button>
<Link to={`/profile/${user.id}`} className="flex-center gap-3">
<img
src={user.imageUrl || "assets/images/profile.png"}
alt="profile avatar"
className="h-8 w-8 rounded-full"
/>
</Link>
</div>
</div>
</section>
);
};
export default Topbar;
III. Modify the queriesAndMutations.ts
Located in src/lib/react-query/queriesAndMutations.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Source code: https://github.com/adrianhajdin/social_media_app
import {
useQuery,
useMutation,
useQueryClient,
useInfiniteQuery,
} from "@tanstack/react-query";
import {
createUserAccount,
signInAccount,
signOutAccount,
} from "@/lib/appwrite/api";
import { INewUser } from "@/types";
export const useCreateUserAccount = () => {
return useMutation({
mutationFn: (user: INewUser) => createUserAccount(user),
});
};
export const useSignInAccount = () => {
return useMutation({
mutationFn: (user: { email: string; password: string }) =>
signInAccount(user),
});
};
export const useSignOutAccount = () => {
return useMutation({
mutationFn: signOutAccount,
});
};
IV. Modify the api.ts
Located in src/lib/appwrite/api.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Source code: https://github.com/adrianhajdin/social_media_app
import { ID, Query } from "appwrite";
import { appwriteConfig, account, databases, avatars } from "./config";
import { INewUser } from "@/types";
export async function createUserAccount(user: INewUser) {
try {
const newAccount = await account.create(
ID.unique(),
user.email,
user.password,
user.name
);
if (!newAccount) throw Error;
const avatarUrl = avatars.getInitials(user.name);
const newUser = await saveUserToDB({
accountId: newAccount.$id,
email: newAccount.email,
name: newAccount.name,
username: user.username,
imageUrl: avatarUrl,
});
return newUser;
console.log("new user created: " + newUser);
} catch (error) {
console.log(error);
return error;
}
}
export async function saveUserToDB(user: {
accountId: string;
email: string;
name: string;
imageUrl: URL;
username?: string;
}) {
try {
const newUser = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
ID.unique(),
user
);
return newUser;
} catch (error) {
console.log(error);
}
}
export async function signInAccount(user: { email: string; password: string }) {
try {
const session = await account.createEmailSession(user.email, user.password);
return session;
} catch (error) {
console.log(error);
}
}
export async function getCurrentUser() {
try {
const currentAccount = await account.get();
if (!currentAccount) throw Error;
const currentUser = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
[Query.equal("accountId", currentAccount.$id)]
);
if (!currentUser) throw Error;
return currentUser.documents[0];
} catch (error) {
console.log(error);
return null;
}
}
export async function signOutAccount() {
try {
const session = await account.deleteSession("current");
return session;
} catch (error) {
console.log(error);
}
}
V. Modify Component: LeftSidebar.tsx
Located in src/components/shared/LeftSidebar.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Source code: https://github.com/adrianhajdin/social_media_app
import { Link, NavLink, useNavigate, useLocation } from "react-router-dom";
import { Button } from "../ui/button";
import { useSignOutAccount } from "@/lib/react-query/queriesAndMutations";
import { useEffect } from "react";
import { useUserContext } from "@/context/AuthContext";
import { sidebarLinks } from "@/constants";
import { INavLink } from "@/types";
const LeftSidebar = () => {
const { pathname } = useLocation();
const { mutate: signOut, isSuccess } = useSignOutAccount();
const navigate = useNavigate();
const { user } = useUserContext();
useEffect(() => {
if (isSuccess) navigate(0);
}, [isSuccess]);
return (
<nav className="leftsidebar">
<div className="flex flex-col gap-11">
<Link to="/" className="flex gap-3 items-center">
<img
src="/assets/images/logo.svg"
alt="logo" width={170} height={36}
/>
</Link>
<Link to={`/profile/${user.id}`} className="flex gap-3 items-center">
<img
src={user.imageUrl || "/assets/icons/profile-placeholder.svg"}
alt="profile" className="h-14 w-14 rounded-full"/>
<div className="flex flex-col">
<p className="body-bold">{user.name}</p>
<p className="small-regular text-light-3">@{user.username}</p>
</div>
</Link>
<ul className="flex flex-col gap-6">
{sidebarLinks.map((link: INavLink) => {
const isActive = pathname === link.route;
return (
<li
key={link.label}
className={`leftsidebar-link group ${
isActive && "bg-primary-500"
}`}
>
<NavLink
to={link.route}
className="flex gap-4 items-center p-4"
>
<img
src={link.imgURL}
alt={link.label}
className={`group-hover:invert-white ${
isActive && "invert-white"
}`}
/>
{link.label}
</NavLink>
</li>
);
})}
</ul>
</div>
<Button
variant="ghost"
className="shad-button_ghost"
onClick={() => signOut()}
>
<img src="/assets/icons/logout.svg" alt="logout" />
<p className="small-medium lg:base-medium">Logout</p>
</Button>
</nav>
);
};
export default LeftSidebar;
VI. Create a constants: index.ts
file
Located in src/constants/index.ts |
From (Github gist) |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
export const sidebarLinks = [
{
imgURL: "/assets/icons/home.svg",
route: "/",
label: "Home",
},
{
imgURL: "/assets/icons/wallpaper.svg",
route: "/explore",
label: "Explore",
},
{
imgURL: "/assets/icons/people.svg",
route: "/all-users",
label: "People",
},
{
imgURL: "/assets/icons/bookmark.svg",
route: "/saved",
label: "Saved",
},
{
imgURL: "/assets/icons/gallery-add.svg",
route: "/create-post",
label: "Create Post",
},
];
export const bottombarLinks = [
{
imgURL: "/assets/icons/home.svg",
route: "/",
label: "Home",
},
{
imgURL: "/assets/icons/wallpaper.svg",
route: "/explore",
label: "Explore",
},
{
imgURL: "/assets/icons/bookmark.svg",
route: "/saved",
label: "Saved",
},
{
imgURL: "/assets/icons/gallery-add.svg",
route: "/create-post",
label: "Create",
},
];
VII. Modify the App.tsx
file
Located in src/App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Source code: https://github.com/adrianhajdin/social_media_app
import { Routes, Route } from "react-router-dom";
import "./globals.css";
import SigninForm from "./_auth/forms/SigninForm";
import SignupForm from "./_auth/forms/SignupForm";
import { AllUsers, CreatePost, EditPost, Explore, Home, PostDetails, Profile, Saved, UpdateProfile, } from "./_root/Pages";
import AuthLayout from "./_auth/AuthLayout";
import RootLayout from "./_root/RootLayout";
import { Toaster } from "@/components/ui/toaster";
const App = () => {
return (
<main className="flex h-screen">
<Routes>
{/* Public Routes */}
<Route element={<AuthLayout />}>
<Route path="/sign-in" element={<SigninForm />} />
<Route path="/sign-up" element={<SignupForm />} />
</Route>
{/* Private Routes */}
<Route element={<RootLayout />}>
<Route index element={<Home />} />
<Route path="/explore" element={<Explore />} />
<Route path="/saved" element={<Saved />} />
<Route path="/all-users" element={<AllUsers />} />
<Route path="/create-post" element={<CreatePost />} />
<Route path="/update-post/:id" element={<EditPost />} />
<Route path="/posts/:id" element={<PostDetails />} />
<Route path="/profile/:id/*" element={<Profile />} />
<Route path="/update-profile/:id" element={<UpdateProfile />} />
</Route>
</Routes>
<Toaster />
</main>
);
};
export default App;
VIII. Create multiple files under the pages folder and run rafce
- Located on
src/_root/pages/
- Create a
Explore.tsx
file and runrafce
. - Create a
Saved.tsx
file and runrafce
. - Create a
CreatePost.tsx
file and runrafce
. - Create a
Profile.tsx
file and runrafce
. - Create a
UpdateProfile.tsx
file and runrafce
. - Create a
EditPost.tsx
file and runrafce
. - Create a
PostDetails.tsx
file and runrafce
. - Create a
LikedPosts.tsx
file and runrafce
. - Create a
AllUsers.tsx
file and runrafce
.
Delete
import React from "react";
from all the new files.
IX. Modify the index.ts
file on pages
Located in src/_root/pages/index.ts
1
2
3
4
5
6
7
8
9
10
export { default as Home } from "./Home";
export { default as Explore } from "./Explore";
export { default as Saved } from "./Saved";
export { default as CreatePost } from "./CreatePost";
export { default as Profile } from "./Profile";
export { default as UpdateProfile } from "./UpdateProfile";
export { default as EditPost } from "./EditPost";
export { default as PostDetails } from "./PostDetails";
export { default as LikedPosts } from "./LikedPosts";
export { default as AllUsers } from "./AllUsers";
IV. Modify Component: Bottombar.tsx
Located in src/components/shared/Bottombar.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Source code: https://github.com/adrianhajdin/social_media_app
import { bottombarLinks } from "@/constants";
import { Link, useLocation } from "react-router-dom";
const Bottombar = () => {
const { pathname } = useLocation();
return (
<section className="bottom-bar">
{bottombarLinks.map((link) => {
const isActive = pathname === link.route;
return (
<Link
to={link.route}
key={link.label}
className={`${
isActive && "bg-primary-500 rounded-[10px]"
} flex-center flex-col gap-1 p-2 transition`}
>
<img
src={link.imgURL}
alt={link.label}
width={16}
height={16}
className={`${isActive && "invert-white"}`}
/>
<p className="tiny-medium text-light-2">{link.label}</p>
</Link>
);
})}
</section>
);
};
export default Bottombar;
10. Create Post11
I. Open & Modify the CreatePost.tsx
file
Located in src/_root/pages/CreatePost.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Source code: https://github.com/adrianhajdin/social_media_app
import PostForm from "@/components/forms/PostForm";
const CreatePost = () => {
return (
<div className="flex flex-1">
<div className="common-container">
<div className="max-w-5xl flex-start gap-3 justify-start w-full">
<img
src="/assets/icons/add-post.svg"
width={36}
height={36}
alt="add"
/>
<h2 className="h3-bold md:h2-bold text-left w-full">Create Post</h2>
</div>
<PostForm />
</div>
</div>
);
};
export default CreatePost;
II. Create a Component: PostForm.tsx
Located in src/components/forms/PostForm.tsx
and run rafce
. Delete import React from "react";
- Go to Shadcn-ui Components forms.
Follow the steps on the Shadcn-UI site and check the documentation for more details.
-
Install Textarea component from shadcn-ui
1
npx shadcn-ui@latest add textarea
The Components: Form
, input
& button
should be already install.
- If not, please install them:
1 2 3
npx shadcn-ui@latest add form npx shadcn-ui@latest add input
- Here is an example of a Basic form that could be use as a Form Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useForm } from "react-hook-form";
const formSchema = z.object({
username: z.string().min(2).max(50),
});
const FormTemplate = () => {
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
},
});
// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values);
}
return (
<div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
</div>
);
};
export default FormTemplate;
This is a basic but powerful form.
Modify the Component: PostForm.tsx
. Located in src/components/forms/PostForm.tsx
1
*************************** MODIFY POSTFORM BELOW ****************************
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Source code: https://github.com/adrianhajdin/social_media_app
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "../ui/textarea";
import FileUploader from "../shared/FileUploader";
import { PostValidation } from "@/lib/validation";
import { Models } from "appwrite";
import { useCreatePost } from "@/lib/react-query/queriesAndMutations";
import { useUserContext } from "@/context/AuthContext";
import { useToast } from "../ui/use-toast";
import { useNavigate } from "react-router-dom";
type PostFormProps = {
post?: Models.Document;
};
const PostForm = ({ post }: PostFormProps) => {
const {
mutateAsync: createPost,
// isPending: isLoadingCreate
} = useCreatePost();
// Hooks:
const { user } = useUserContext();
const { toast } = useToast();
const navigate = useNavigate();
// 1. Define your form.
const form = useForm<z.infer<typeof PostValidation>>({
resolver: zodResolver(PostValidation),
defaultValues: {
caption: post ? post?.caption : "",
file: [],
location: post ? post?.location : "",
tags: post ? post.tags.join(",") : "",
},
});
// 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof PostValidation>) {
const newPost = await createPost({
...values,
userId: user.id,
});
// if NO new post, show error
if (!newPost) {
toast({
title: "Please try again",
});
}
// if new post, redirect to home page
navigate("/");
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-9 w-full max-w-5xl"
>
<FormField
control={form.control}
name="caption"
render={({ field }) => (
<FormItem>
<FormLabel className="shad-form_label">Caption</FormLabel>
<FormControl>
<Textarea
className="shad-textarea custom-scrollbar"
{...field}
/>
</FormControl>
<FormMessage className="shad-form_message" />
</FormItem>
)}
/>
<FormField
control={form.control}
name="file"
render={({ field }) => (
<FormItem>
<FormLabel className="shad-form_label">Add Photos</FormLabel>
<FormControl>
<FileUploader
fieldChange={field.onChange}
mediaUrl={post?.imageUrl}
/>
</FormControl>
<FormMessage className="shad-form_message" />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel className="shad-form_label">Add Location</FormLabel>
<FormControl>
<Input type="text" className="shad-input" {...field} />
</FormControl>
<FormMessage className="shad-form_message" />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tags"
render={({ field }) => (
<FormItem>
<FormLabel className="shad-form_label">
Add Tags (separated by comma " , ")
</FormLabel>
<FormControl>
<Input
type="text"
className="shad-input"
placeholder="JS, React, NextJS"
{...field}
/>
</FormControl>
<FormMessage className="shad-form_message" />
</FormItem>
)}
/>
<div className="flex gap-4 items-center justify-end">
<Button type="button" className="shad-button_dark_4">
Cancel
</Button>
<Button
type="submit"
className="shad-button_primary whitespace-nowrap"
>
Submit
</Button>
</div>
</form>
</Form>
);
};
export default PostForm;
III. Create a Component: FileUploader.tsx
Located in src/_root/pages/CreatePost.tsx
and run rafce
. Delete import React from "react";
- Install React dropzone
1
npm install react-dropzone
Modify the Component: FileUploader.tsx
Located in src/components/shared/FileUploader.tsx
1
************************* MODIFY FILE UPLOADER BELOW *************************
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// Source code: https://github.com/adrianhajdin/social_media_app
import React, { useState, useCallback } from "react";
import { FileWithPath, useDropzone } from "react-dropzone";
import { Button } from "../ui/button";
type FileUploaderProps = {
fieldChange: (FILES: File[]) => void;
mediaUrl: string;
};
const FileUploader = ({ fieldChange, mediaUrl}: FileUploaderProps) => {
const [file, setFile] = useState<File[]>([]);
const [fileUrl, setFileUrl] = useState(mediaUrl);
const onDrop = useCallback(
(acceptedFiles: FileWithPath[]) => {
setFile(acceptedFiles);
fieldChange(acceptedFiles);
setFileUrl(URL.createObjectURL(acceptedFiles[0]));
},
[file]
);
const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept: { "image/*": [".png", ".jpg", ".jpeg", ".svg"] },
});
return (
<div
{...getRootProps()}
className="flex flex-center flex-col bg-dark-3 rounded-xl cursor-pointer"
>
<input {...getInputProps()} className="cursor-pointer" />
{fileUrl ? (
<>
<div className="flex flex-1 justify-center w-full p-5 lg:p-10">
<img
src={fileUrl}
alt="uploaded image"
className="file_uploader-img"
/>
</div>
<p className="file_uploader-label">Click or drag photo to replace</p>
</>
) : (
<div className="file_uploader-box">
<img
src="/assets/icons/file-upload.svg"
width={96}
height={77}
alt="file-upload"
/>
<h3 className="base-medium text-light-2 mb-2 mt-6">
Drag Photo here
</h3>
<p className="text-light-4 small-regular mb-6">SVG, PNG, JPG</p>
<Button className="shad-button_dark_4">Browse from computer</Button>
</div>
)}
</div>
);
};
export default FileUploader;
IV. Modify the Validation file: index.ts
Located in src/lib/validation/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Source code: https://github.com/adrianhajdin/social_media_app
import * as z from "zod";
export const SignupValidation = z.object({
name: z
.string()
.min(2, { message: "Name is too short" })
.max(50, { message: "Name is too long" }),
username: z
.string()
.min(2, { message: "Username is too short" })
.max(50, { message: "Username is too short" }),
email: z.string().email(),
password: z
.string()
.min(8, { message: "Password must be at least 8 characters" })
.max(60, { message: "Password is too long" }),
});
export const SigninValidation = z.object({
email: z.string().email(),
password: z
.string()
.min(8, { message: "Password must be at least 8 characters" })
.max(60, { message: "Password is too long. Maximum 60 caracters" }),
});
export const PostValidation = z.object({
caption: z
.string()
.min(5, { message: "Minimum 5 caracters" })
.max(2200, { message: "Maximum 2,200 caracters" }),
file: z.custom<File[]>(),
location: z
.string()
.min(1, { message: "This field is required" })
.max(100, { message: "This field is too long" }),
tags: z.string(),
});
V. Modify the api.ts
Located in src/lib/appwrite/api.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Source code: https://github.com/adrianhajdin/social_media_app
import { ID, Query } from "appwrite";
import { appwriteConfig, account, databases, avatars, storage } from "./config";
import { INewPost, INewUser } from "@/types";
// ============================================================
// AUTH
// ============================================================
// ============================== SIGN UP
export async function createUserAccount(user: INewUser) {
try {
const newAccount = await account.create(
ID.unique(),
user.email,
user.password,
user.name
);
if (!newAccount) throw Error;
const avatarUrl = avatars.getInitials(user.name);
const newUser = await saveUserToDB({
accountId: newAccount.$id,
email: newAccount.email,
name: newAccount.name,
username: user.username,
imageUrl: avatarUrl,
});
return newUser;
console.log("new user created: " + newUser);
} catch (error) {
console.log(error);
return error;
}
}
// ============================== SAVE USER TO DB
export async function saveUserToDB(user: {
accountId: string;
email: string;
name: string;
imageUrl: URL;
username?: string;
}) {
try {
const newUser = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
ID.unique(),
user
);
return newUser;
} catch (error) {
console.log(error);
}
}
// ============================== SIGN IN
export async function signInAccount(user: { email: string; password: string }) {
try {
const session = await account.createEmailSession(user.email, user.password);
return session;
} catch (error) {
console.log(error);
}
}
// ============================== GET USER
export async function getCurrentUser() {
try {
const currentAccount = await account.get();
if (!currentAccount) throw Error;
const currentUser = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
[Query.equal("accountId", currentAccount.$id)]
);
if (!currentUser) throw Error;
return currentUser.documents[0];
} catch (error) {
console.log(error);
return null;
}
}
// ============================== SIGN OUT
export async function signOutAccount() {
try {
const session = await account.deleteSession("current");
return session;
} catch (error) {
console.log(error);
}
}
// ============================================================
// POSTS
// ============================================================
// ============================== CREATE POST
export async function createPost(post: INewPost) {
try {
// *** Upload file to appwrite storage ***
const uploadedFile = await uploadFile(post.file[0]);
// If No file, throw error
if (!uploadedFile) throw Error;
// *** Get file url ***
const fileUrl = getFilePreview(uploadedFile.$id);
// If no file url, delete file from storage and throw error
if (!fileUrl) {
await deleteFile(uploadedFile.$id);
throw Error;
}
// *** Convert tags into array ***
const tags = post.tags?.replace(/ /g, "").split(",") || [];
// *** Create post ***
const newPost = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
ID.unique(),
{
creator: post.userId,
caption: post.caption,
imageUrl: fileUrl,
imageId: uploadedFile.$id,
location: post.location,
tags: tags,
}
);
// If No New POST, delete file from storage
if (!newPost) {
await deleteFile(uploadedFile.$id);
throw Error;
}
return newPost;
} catch (error) {
console.log(error);
}
}
// ============================== UPLOAD FILE
export async function uploadFile(file: File) {
try {
const uploadedFile = await storage.createFile(
appwriteConfig.storageId,
ID.unique(),
file
);
return uploadedFile;
} catch (error) {
console.log(error);
}
}
// ============================== GET FILE URL
export function getFilePreview(fileId: string) {
try {
const fileUrl = storage.getFilePreview(
appwriteConfig.storageId,
fileId,
2000,
2000,
"top",
100
);
if (!fileUrl) throw Error;
return fileUrl;
} catch (error) {
console.log(error);
}
}
// ============================== DELETE FILE
export async function deleteFile(fileId: string) {
try {
await storage.deleteFile(appwriteConfig.storageId, fileId);
return { status: "ok" };
} catch (error) {
console.log(error);
}
}
// ============================== GET POPULAR POSTS (BY HIGHEST LIKE COUNT)
export async function getRecentPosts() {
try {
const posts = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
[Query.orderDesc("$createdAt"), Query.limit(20)]
);
if (!posts) throw Error;
return posts;
} catch (error) {
console.log(error);
}
}
VI. Modify the queriesAndMutations.ts
file
Located in src/lib/react-query/queriesAndMutations.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Source code: https://github.com/adrianhajdin/social_media_app
import {
useQuery,
useMutation,
useQueryClient,
// useInfiniteQuery,
} from "@tanstack/react-query";
import { QUERY_KEYS } from "@/lib/react-query/queryKeys";
import {
createUserAccount,
signInAccount,
signOutAccount,
createPost,
getRecentPosts,
} from "@/lib/appwrite/api";
import { INewPost, INewUser } from "@/types";
// ============================================================
// AUTH QUERIES
// ============================================================
export const useCreateUserAccount = () => {
return useMutation({
mutationFn: (user: INewUser) => createUserAccount(user),
});
};
export const useSignInAccount = () => {
return useMutation({
mutationFn: (user: { email: string; password: string }) =>
signInAccount(user),
});
};
export const useSignOutAccount = () => {
return useMutation({
mutationFn: signOutAccount,
});
};
// ============================================================
// POST QUERIES
// ============================================================
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (post: INewPost) => createPost(post),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
},
});
};
export const useGetRecentPosts = () => {
return useQuery({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
queryFn: getRecentPosts,
});
};
VII. Create a queryKeys.ts
file
Located in src/lib/react-query/queryKeys.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export enum QUERY_KEYS {
// AUTH KEYS
CREATE_USER_ACCOUNT = "createUserAccount",
// USER KEYS
GET_CURRENT_USER = "getCurrentUser",
GET_USERS = "getUsers",
GET_USER_BY_ID = "getUserById",
// POST KEYS
GET_POSTS = "getPosts",
GET_INFINITE_POSTS = "getInfinitePosts",
GET_RECENT_POSTS = "getRecentPosts",
GET_POST_BY_ID = "getPostById",
GET_USER_POSTS = "getUserPosts",
GET_FILE_PREVIEW = "getFilePreview",
// SEARCH KEYS
SEARCH_POSTS = "getSearchPosts",
}
From Github gist
VIII. Appwrite Permissions
- Go to Appwrite > Storage > media > Settings > Permissions > any > all checkmarks > update.
IX. Test Create Post
X. Modify the Home.tsx
Located in src/_root/pages/Home.tsx
1
************************* MODIFY HOME BELOW *************************
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Loader from "@/components/shared/Loader";
import PostCard from "@/components/shared/PostCard";
import { useGetRecentPosts } from "@/lib/react-query/queriesAndMutations";
import { Models } from "appwrite";
const Home = () => {
// Hook:
const {
data: posts,
isPending: isPostLoading,
// isError: isErrorPosts,
} = useGetRecentPosts();
return (
<div className="flex flex-1">
<div className="home-container">
<div className="home-posts">
<h2 className="h3-bold md:h2-bold text-left w-full">Home Feed</h2>
{isPostLoading && !posts ? (
<Loader />
) : (
<ul className="flex flex-col flex-1 gap-9 w-full">
{posts?.documents.map((post: Models.Document) => (
// <li>{post.caption}</li>
<PostCard post={post} key={post.caption} />
))}
</ul>
)}
</div>
</div>
</div>
);
};
export default Home;
11. Post Card12
I. Create Component: PostCard.tsx
Located in src/components/shared/PostCard.tsx
and run rafce
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// import { formatDateString } from "@/lib/utils";
import { formatDistanceToNowStrict } from "date-fns";
import { Models } from "appwrite";
import { Link } from "react-router-dom";
import { useUserContext } from "@/context/AuthContext";
import PostStats from "./PostStats";
type PostCardProps = {
post: Models.Document;
};
const PostCard = ({ post }: PostCardProps) => {
const { user } = useUserContext();
// console.log("post:", post);
if (!post.creator) return;
return (
<div className="post-card">
<div className="flex-between">
<div className="flex items-center gap-3">
<Link to={`/profile/${post.creator.$id}`}>
<img
src={
post?.creator?.imageUrl ||
"/assets/icons/profile-placeholder.svg"
}
alt="creator"
className="rounded-full w-12 lg:h-12"
/>
</Link>
<div className="flex flex-col">
<p className="base-medium lg:body-bold text-light-1">
{post.creator.name}
</p>
<div className="flex-center gap-2 text-light-3">
<p className="subtle-semibold lg:small-regular">
{/* {formatDateString(post.$createdAt)} */}
{formatDistanceToNowStrict(new Date(post.$createdAt), {
addSuffix: true,
})}
</p>
-
<p className="subtle-semibold lg:small-regular">
{post.location}
</p>
</div>
</div>
</div>
<Link
to={`/update-post/${post.$id}`}
className={`${user.id !== post.creator.$id && "hidden"}`}
>
<img src="/assets/icons/edit.svg" alt="edit" width={20} height={20} />
</Link>
</div>
<Link to={`/posts/${post.$id}`}>
<div className="small-medium lg:base-medium py-5 ">
<p>{post.caption}</p>
<ul className="flex gap-1 mt-2">
{post.tags.map((tag: string) => (
<li key={tag} className="text-light-3">
#{tag}
</li>
))}
</ul>
</div>
<img
src={post.imageUrl || "/assets/icons/profile-placeholder.svg"}
alt="post image"
className="post-card_img"
/>
</Link>
<PostStats post={post} userId={user.id} />
</div>
);
};
export default PostCard;
A. Use ChatGPT or Copilot to create a function for you to modify the date string.
My Output:
It suggested me to…
- Install the date-fns library by running:
1
npm install date-fns --save
This will modify the string of date to a more human friendly readable date.
- Then, you can import the
formatDistanceToNow
function and use it to format thepost.$createdAt
date. Here’s how you can modify your code:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import { formatDistanceToNow } from 'date-fns'; // ... return ( <div className="post-card"> {/* ... */} <p className="subtle-semibold lg:small-regular"> {formatDistanceToNowStrict(new Date(post.$createdAt), { addSuffix: true })} </p> {/* ... */} </div> ); // Input Date String = 2023-11-09T15:39:36.442+00:00 // Output 3 hours ago
B. Or Just use the code from utils.ts
Located in src/lib/utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Code Source: https://gist.github.com/adrianhajdin/4d2500bf5af601bbd9f4f596298d33ac
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export const convertFileToUrl = (file: File) => URL.createObjectURL(file);
export function formatDateString(dateString: string) {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "short",
day: "numeric",
};
const date = new Date(dateString);
const formattedDate = date.toLocaleDateString("en-US", options);
const time = date.toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
});
return `${formattedDate} at ${time}`;
}
//
export const multiFormatDateString = (timestamp: string = ""): string => {
const timestampNum = Math.round(new Date(timestamp).getTime() / 1000);
const date: Date = new Date(timestampNum * 1000);
const now: Date = new Date();
const diff: number = now.getTime() - date.getTime();
const diffInSeconds: number = diff / 1000;
const diffInMinutes: number = diffInSeconds / 60;
const diffInHours: number = diffInMinutes / 60;
const diffInDays: number = diffInHours / 24;
switch (true) {
case Math.floor(diffInDays) >= 30:
return formatDateString(timestamp);
case Math.floor(diffInDays) === 1:
return `${Math.floor(diffInDays)} day ago`;
case Math.floor(diffInDays) > 1 && diffInDays < 30:
return `${Math.floor(diffInDays)} days ago`;
case Math.floor(diffInHours) >= 1:
return `${Math.floor(diffInHours)} hours ago`;
case Math.floor(diffInMinutes) >= 1:
return `${Math.floor(diffInMinutes)} minutes ago`;
default:
return "Just now";
}
};
export const checkIsLiked = (likeList: string[], userId: string) => {
return likeList.includes(userId);
};
II. Create PostStats.tsx
Located in src/components/shared/PostStats.tsx
and run rafce
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import React, { useState, useEffect } from "react";
import {
useLikePost,
useSavePost,
useDeleteSavedPost,
useGetCurrentUser,
} from "@/lib/react-query/queriesAndMutations";
import { Models } from "appwrite";
import { checkIsLiked } from "@/lib/utils";
import Loader from "./Loader";
type PostStatsProps = {
post: Models.Document;
userId: string;
};
const PostStats = ({ post, userId }: PostStatsProps) => {
const likesList = post.likes.map((user: Models.Document) => user.$id); // list of user ids who liked the post
// console.log("likesList:", likesList);
// useState:
const [likes, setLikes] = useState<string[]>(likesList); // list of user ids who liked the post
const [isSaved, setIsSaved] = useState(false); // is the post saved by the current user?
const { mutate: likePost } = useLikePost();
const { mutate: savePost, isPending: isSavingPost } = useSavePost();
const { mutate: deleteSavePost, isPending: isDeletingSaved } =
useDeleteSavedPost();
// who is the current user?
const { data: currentUser } = useGetCurrentUser();
const savedPostRecord = currentUser?.save.find(
(record: Models.Document) => record.post.$id === post.$id
);
useEffect(() => {
setIsSaved(savedPostRecord ? true : false); // or: setIsSaved(!!savedPostRecord);
// { Saved: true } => !savedPostRecord => !false = true
// { Saved: false } => !savedPostRecord => !true = false
}, [currentUser]);
const handleLikePost = (
e: React.MouseEvent<HTMLImageElement, MouseEvent>
) => {
e.stopPropagation();
let likesArray = [...likes];
const hasLiked = likesArray.includes(userId);
if (hasLiked) {
likesArray = likesArray.filter((Id) => Id !== userId);
} else {
likesArray.push(userId);
}
setLikes(likesArray);
likePost({ postId: post.$id, likesArray });
};
const handleSavePost = (
e: React.MouseEvent<HTMLImageElement, MouseEvent>
) => {
e.stopPropagation();
if (savedPostRecord) {
setIsSaved(false);
return deleteSavePost(savedPostRecord.$id);
}
savePost({ userId: userId, postId: post.$id });
setIsSaved(true);
};
const containerStyles = location.pathname.startsWith("/profile")
? "w-full"
: "";
return (
<div
className={`flex justify-between items-center z-20 ${containerStyles}`}
>
<div className="flex gap-2 mr-5">
<img
src={`${
checkIsLiked(likes, userId)
? "/assets/icons/liked.svg"
: "/assets/icons/like.svg"
}`}
alt={checkIsLiked(likes, userId) ? "liked" : "like"}
width={20}
height={20}
onClick={(e) => handleLikePost(e)}
className="cursor-pointer"
/>
<p className="small-medium lg:base-medium">{likes.length}</p>
</div>
<div className="flex gap-2">
{isSavingPost || isDeletingSaved ? (
<Loader />
) : (
<img
src={isSaved ? "/assets/icons/saved.svg" : "/assets/icons/save.svg"}
alt="share"
width={20}
height={20}
className="cursor-pointer"
onClick={handleSavePost}
/>
)}
</div>
</div>
);
};
export default PostStats;
III. Modify the api.ts
Located in src/lib/appwrite/api.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
// Source code: https://github.com/adrianhajdin/social_media_app
import { ID, Query } from "appwrite";
import { appwriteConfig, account, databases, avatars, storage } from "./config";
import { INewPost, INewUser } from "@/types";
// ============================================================
// AUTH
// ============================================================
// ============================== SIGN UP
export async function createUserAccount(user: INewUser) {
try {
const newAccount = await account.create(
ID.unique(),
user.email,
user.password,
user.name
);
if (!newAccount) throw Error;
const avatarUrl = avatars.getInitials(user.name);
const newUser = await saveUserToDB({
accountId: newAccount.$id,
email: newAccount.email,
name: newAccount.name,
username: user.username,
imageUrl: avatarUrl,
});
return newUser;
console.log("new user created: " + newUser);
} catch (error) {
console.log(error);
return error;
}
}
// ============================== SAVE USER TO DB
export async function saveUserToDB(user: {
accountId: string;
email: string;
name: string;
imageUrl: URL;
username?: string;
}) {
try {
const newUser = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
ID.unique(),
user
);
return newUser;
} catch (error) {
console.log(error);
}
}
// ============================== SIGN IN
export async function signInAccount(user: { email: string; password: string }) {
try {
const session = await account.createEmailSession(user.email, user.password);
return session;
} catch (error) {
console.log(error);
}
}
// ============================== GET USER
export async function getCurrentUser() {
try {
const currentAccount = await account.get();
if (!currentAccount) throw Error;
const currentUser = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
[Query.equal("accountId", currentAccount.$id)]
);
if (!currentUser) throw Error;
return currentUser.documents[0];
} catch (error) {
console.log(error);
return null;
}
}
// ============================== SIGN OUT
export async function signOutAccount() {
try {
const session = await account.deleteSession("current");
return session;
} catch (error) {
console.log(error);
}
}
// ============================================================
// POSTS
// ============================================================
// ============================== CREATE POST
export async function createPost(post: INewPost) {
try {
// *** Upload file to appwrite storage ***
const uploadedFile = await uploadFile(post.file[0]);
// If No file, throw error
if (!uploadedFile) throw Error;
// *** Get file url ***
const fileUrl = getFilePreview(uploadedFile.$id);
// If no file url, delete file from storage and throw error
if (!fileUrl) {
await deleteFile(uploadedFile.$id);
throw Error;
}
// *** Convert tags into array ***
const tags = post.tags?.replace(/ /g, "").split(",") || [];
// *** Create post ***
const newPost = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
ID.unique(),
{
creator: post.userId,
caption: post.caption,
imageUrl: fileUrl,
imageId: uploadedFile.$id,
location: post.location,
tags: tags,
}
);
// If No New POST, delete file from storage
if (!newPost) {
await deleteFile(uploadedFile.$id);
throw Error;
}
return newPost;
} catch (error) {
console.log(error);
}
}
// ============================== UPLOAD FILE
export async function uploadFile(file: File) {
try {
const uploadedFile = await storage.createFile(
appwriteConfig.storageId,
ID.unique(),
file
);
return uploadedFile;
} catch (error) {
console.log(error);
}
}
// ============================== GET FILE URL
export function getFilePreview(fileId: string) {
try {
const fileUrl = storage.getFilePreview(
appwriteConfig.storageId,
fileId,
2000,
2000,
"top",
100
);
if (!fileUrl) throw Error;
return fileUrl;
} catch (error) {
console.log(error);
}
}
// ============================== DELETE FILE
export async function deleteFile(fileId: string) {
try {
await storage.deleteFile(appwriteConfig.storageId, fileId);
return { status: "ok" };
} catch (error) {
console.log(error);
}
}
// ============================== GET POPULAR POSTS (BY HIGHEST LIKE COUNT)
export async function getRecentPosts() {
try {
const posts = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
[Query.orderDesc("$createdAt"), Query.limit(20)]
);
if (!posts) throw Error;
return posts;
} catch (error) {
console.log(error);
}
}
// ============================== LIKE / UNLIKE POST
export async function likePost(postId: string, likesArray: string[]) {
try {
const updatedPost = await databases.updateDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
postId,
{
likes: likesArray,
}
);
// if there is no updated post, throw error
if (!updatedPost) throw Error;
return updatedPost;
} catch (error) {
console.log(error);
}
}
// ============================== SAVE POST
export async function savePost(userId: string, postId: string) {
try {
const updatedPost = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.savesCollectionId,
ID.unique(),
{
user: userId,
post: postId,
}
);
if (!updatedPost) throw Error;
return updatedPost;
} catch (error) {
console.log(error);
}
}
// ============================== DELETE SAVED POST
export async function deleteSavedPost(savedRecordId: string) {
try {
const statusCode = await databases.deleteDocument(
appwriteConfig.databaseId,
appwriteConfig.savesCollectionId,
savedRecordId
);
if (!statusCode) throw Error;
return { status: "Ok" };
} catch (error) {
console.log(error);
}
}
IV. Modify the queriesAndMutations.ts
file
Located in src/lib/react-query/queriesAndMutations.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Source code: https://github.com/adrianhajdin/social_media_app
import {
useQuery,
useMutation,
useQueryClient,
// useInfiniteQuery,
} from "@tanstack/react-query";
import { QUERY_KEYS } from "@/lib/react-query/queryKeys";
import {
createUserAccount,
signInAccount,
signOutAccount,
createPost,
getRecentPosts,
likePost,
savePost,
deleteSavedPost,
getCurrentUser,
} from "@/lib/appwrite/api";
import { INewPost, INewUser } from "@/types";
// ============================================================
// AUTH QUERIES
// ============================================================
export const useCreateUserAccount = () => {
return useMutation({
mutationFn: (user: INewUser) => createUserAccount(user),
});
};
export const useSignInAccount = () => {
return useMutation({
mutationFn: (user: { email: string; password: string }) =>
signInAccount(user),
});
};
export const useSignOutAccount = () => {
return useMutation({
mutationFn: signOutAccount,
});
};
// ============================================================
// POST QUERIES
// ============================================================
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (post: INewPost) => createPost(post),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
},
});
};
export const useGetRecentPosts = () => {
return useQuery({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
queryFn: getRecentPosts,
});
};
export const useLikePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
postId,
likesArray,
}: {
postId: string;
likesArray: string[];
}) => likePost(postId, likesArray),
onSuccess: (data) => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POST_BY_ID, data?.$id],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
});
},
});
};
export const useSavePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ userId, postId }: { userId: string; postId: string }) =>
savePost(userId, postId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
});
},
});
};
export const useDeleteSavedPost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (savedRecordId: string) => deleteSavedPost(savedRecordId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
});
},
});
};
// ============================================================
// USER QUERIES
// ============================================================
export const useGetCurrentUser = () => {
return useQuery({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
queryFn: getCurrentUser,
});
};
12. Post CRUD13
I. Modify the EditPost.tsx
Located in src/_root/Pages/EditPost.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import PostForm from "@/components/forms/PostForm";
import Loader from "@/components/shared/Loader";
import { useGetPostById } from "@/lib/react-query/queriesAndMutations";
import { useParams } from "react-router-dom";
const EditPost = () => {
const { id } = useParams();
const { data: post, isPending } = useGetPostById(id || "");
if (isPending) {
return <Loader />;
}
return (
<div className="flex flex-1">
<div className="common-container">
<div className="max-w-5xl flex-start gap-3 justify-start w-full">
<img
src="/assets/icons/add-post.svg"
width={36}
height={36}
alt="add"
/>
<h2 className="h3-bold md:h2-bold text-left w-full">Edit Post</h2>
</div>
<PostForm action="Update" post={post} />
</div>
</div>
);
};
export default EditPost;
II. Modify the CreatePost.tsx
Located in src/_root/Pages/CreatePost.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import PostForm from "@/components/forms/PostForm";
const CreatePost = () => {
return (
<div className="flex flex-1">
<div className="common-container">
<div className="max-w-5xl flex-start gap-3 justify-start w-full">
<img
src="/assets/icons/add-post.svg"
width={36}
height={36}
alt="add"
/>
<h2 className="h3-bold md:h2-bold text-left w-full">Create Post</h2>
</div>
<PostForm action="Create" />
</div>
</div>
);
};
export default CreatePost;
III. Modify the PostForm.tsx
Located in src/components/forms/PostForm.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "../ui/textarea";
import FileUploader from "../shared/FileUploader";
import { PostValidation } from "@/lib/validation";
import { Models } from "appwrite";
import {
useCreatePost,
useUpdatePost,
} from "@/lib/react-query/queriesAndMutations";
import { useUserContext } from "@/context/AuthContext";
import { useToast } from "../ui/use-toast";
import { useNavigate } from "react-router-dom";
type PostFormProps = {
post?: Models.Document;
action: "Create" | "Update";
};
const PostForm = ({ post, action }: PostFormProps) => {
const { mutateAsync: createPost, isPending: isLoadingCreate } =
useCreatePost();
const { mutateAsync: updatePost, isPending: isLoadingUpdate } =
useUpdatePost();
// Hooks:
const { user } = useUserContext();
const { toast } = useToast();
const navigate = useNavigate();
// 1. Define your form.
const form = useForm<z.infer<typeof PostValidation>>({
resolver: zodResolver(PostValidation),
defaultValues: {
caption: post ? post?.caption : "",
file: [],
location: post ? post?.location : "",
tags: post ? post.tags.join(",") : "",
},
});
// 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof PostValidation>) {
// if action is update, update post
if (action === "Update") {
const updatedPost = await updatePost({
...values,
postId: post?.$id,
imageId: post?.imageId,
imageUrl: post?.imageUrl,
});
// if NO updated post, show error
if (!updatedPost) {
toast({
title: "Please try again",
});
}
// if updated post, redirect to post page
return navigate("/posts/${post?.$id}");
}
// if it doesn't edit the post, create post
const newPost = await createPost({
...values,
userId: user.id,
});
// if NO new post, show error
if (!newPost) {
toast({
title: "Please try again",
});
}
// if new post, redirect to home page
navigate("/");
}
console.log(post?.imageUrl);
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-9 w-full max-w-5xl"
>
<FormField
control={form.control}
name="caption"
render={({ field }) => (
<FormItem>
<FormLabel className="shad-form_label">Caption</FormLabel>
<FormControl>
<Textarea
className="shad-textarea custom-scrollbar"
{...field}
/>
</FormControl>
<FormMessage className="shad-form_message" />
</FormItem>
)}
/>
<FormField
control={form.control}
name="file"
render={({ field }) => (
<FormItem>
<FormLabel className="shad-form_label">Add Photos</FormLabel>
<FormControl>
<FileUploader
fieldChange={field.onChange}
mediaUrl={post?.imageUrl}
/>
</FormControl>
<FormMessage className="shad-form_message" />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel className="shad-form_label">Add Location</FormLabel>
<FormControl>
<Input type="text" className="shad-input" {...field} />
</FormControl>
<FormMessage className="shad-form_message" />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tags"
render={({ field }) => (
<FormItem>
<FormLabel className="shad-form_label">
Add Tags (separated by comma " , ")
</FormLabel>
<FormControl>
<Input
type="text"
className="shad-input"
placeholder="JS, React, NextJS"
{...field}
/>
</FormControl>
<FormMessage className="shad-form_message" />
</FormItem>
)}
/>
<div className="flex gap-4 items-center justify-end">
<Button type="button" className="shad-button_dark_4">
Cancel
</Button>
<Button
type="submit"
className="shad-button_primary whitespace-nowrap"
disabled={isLoadingCreate || isLoadingUpdate}
>
{isLoadingCreate || (isLoadingUpdate && "Loading...")}
{action} Post
</Button>
</div>
</form>
</Form>
);
};
export default PostForm;
IV. Modify the api.ts
Located in src/lib/appwrite/api.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
// Source code: https://github.com/adrianhajdin/social_media_app
import { ID, Query } from "appwrite";
import { appwriteConfig, account, databases, avatars, storage } from "./config";
import { INewPost, INewUser, IUpdatePost } from "@/types";
// ============================================================
// AUTH
// ============================================================
// ============================== SIGN UP
export async function createUserAccount(user: INewUser) {
try {
const newAccount = await account.create(
ID.unique(),
user.email,
user.password,
user.name
);
if (!newAccount) throw Error;
const avatarUrl = avatars.getInitials(user.name);
const newUser = await saveUserToDB({
accountId: newAccount.$id,
email: newAccount.email,
name: newAccount.name,
username: user.username,
imageUrl: avatarUrl,
});
return newUser;
console.log("new user created: " + newUser);
} catch (error) {
console.log(error);
return error;
}
}
// ============================== SAVE USER TO DB
export async function saveUserToDB(user: {
accountId: string;
email: string;
name: string;
imageUrl: URL;
username?: string;
}) {
try {
const newUser = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
ID.unique(),
user
);
return newUser;
} catch (error) {
console.log(error);
}
}
// ============================== SIGN IN
export async function signInAccount(user: { email: string; password: string }) {
try {
const session = await account.createEmailSession(user.email, user.password);
return session;
} catch (error) {
console.log(error);
}
}
// ============================== GET USER
export async function getCurrentUser() {
try {
const currentAccount = await account.get();
if (!currentAccount) throw Error;
const currentUser = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
[Query.equal("accountId", currentAccount.$id)]
);
if (!currentUser) throw Error;
return currentUser.documents[0];
} catch (error) {
console.log(error);
return null;
}
}
// ============================== SIGN OUT
export async function signOutAccount() {
try {
const session = await account.deleteSession("current");
return session;
} catch (error) {
console.log(error);
}
}
// ============================================================
// POSTS
// ============================================================
// ============================== CREATE POST
export async function createPost(post: INewPost) {
try {
// *** Upload file to appwrite storage ***
const uploadedFile = await uploadFile(post.file[0]);
// If No file, throw error
if (!uploadedFile) throw Error;
// *** Get file url ***
const fileUrl = getFilePreview(uploadedFile.$id);
// If no file url, delete file from storage and throw error
if (!fileUrl) {
await deleteFile(uploadedFile.$id);
throw Error;
}
// *** Convert tags into array ***
const tags = post.tags?.replace(/ /g, "").split(",") || [];
// *** Create post ***
const newPost = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
ID.unique(),
{
creator: post.userId,
caption: post.caption,
imageUrl: fileUrl,
imageId: uploadedFile.$id,
location: post.location,
tags: tags,
}
);
// If No New POST, delete file from storage
if (!newPost) {
await deleteFile(uploadedFile.$id);
throw Error;
}
return newPost;
} catch (error) {
console.log(error);
}
}
// ============================== UPLOAD FILE
export async function uploadFile(file: File) {
try {
const uploadedFile = await storage.createFile(
appwriteConfig.storageId,
ID.unique(),
file
);
return uploadedFile;
} catch (error) {
console.log(error);
}
}
// ============================== GET FILE URL
export function getFilePreview(fileId: string) {
try {
const fileUrl = storage.getFilePreview(
appwriteConfig.storageId,
fileId,
2000,
2000,
"top",
100
);
if (!fileUrl) throw Error;
return fileUrl;
} catch (error) {
console.log(error);
}
}
// ============================== DELETE FILE
export async function deleteFile(fileId: string) {
try {
await storage.deleteFile(appwriteConfig.storageId, fileId);
return { status: "ok" };
} catch (error) {
console.log(error);
}
}
// ============================== GET POPULAR POSTS (BY HIGHEST LIKE COUNT)
export async function getRecentPosts() {
try {
const posts = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
[Query.orderDesc("$createdAt"), Query.limit(20)]
);
if (!posts) throw Error;
return posts;
} catch (error) {
console.log(error);
}
}
// ============================== LIKE / UNLIKE POST
export async function likePost(postId: string, likesArray: string[]) {
try {
const updatedPost = await databases.updateDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
postId,
{
likes: likesArray,
}
);
// if there is no updated post, throw error
if (!updatedPost) throw Error;
return updatedPost;
} catch (error) {
console.log(error);
}
}
// ============================== SAVE POST
export async function savePost(userId: string, postId: string) {
try {
const updatedPost = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.savesCollectionId,
ID.unique(),
{
user: userId,
post: postId,
}
);
if (!updatedPost) throw Error;
return updatedPost;
} catch (error) {
console.log(error);
}
}
// ============================== DELETE SAVED POST
export async function deleteSavedPost(savedRecordId: string) {
try {
const statusCode = await databases.deleteDocument(
appwriteConfig.databaseId,
appwriteConfig.savesCollectionId,
savedRecordId
);
if (!statusCode) throw Error;
return { status: "Ok" };
} catch (error) {
console.log(error);
}
}
// ============================== GET POST BY ID
export async function getPostById(postId?: string) {
if (!postId) throw Error;
try {
const post = await databases.getDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
postId
);
if (!post) throw Error;
return post;
} catch (error) {
console.log(error);
}
}
// ============================== UPDATE POST
export async function updatePost(post: IUpdatePost) {
const hasFileToUpdate = post.file.length > 0;
try {
let image = {
imageUrl: post.imageUrl,
imageId: post.imageId,
};
if (hasFileToUpdate) {
// Upload new file to appwrite storage
const uploadedFile = await uploadFile(post.file[0]);
if (!uploadedFile) throw Error;
// Get new file url
const fileUrl = getFilePreview(uploadedFile.$id);
if (!fileUrl) {
await deleteFile(uploadedFile.$id);
throw Error;
}
image = { ...image, imageUrl: fileUrl, imageId: uploadedFile.$id };
}
// Convert tags into array
const tags = post.tags?.replace(/ /g, "").split(",") || [];
// Update post
const updatedPost = await databases.updateDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
post.postId,
{
caption: post.caption,
imageUrl: image.imageUrl,
imageId: image.imageId,
location: post.location,
tags: tags,
}
);
// Failed to update
if (!updatedPost) {
// Delete new file that has been recently uploaded
if (hasFileToUpdate) {
await deleteFile(image.imageId);
}
// If no new file uploaded, just throw error
throw Error;
}
// Safely delete old file after successful update
if (hasFileToUpdate) {
await deleteFile(post.imageId);
}
return updatedPost;
} catch (error) {
console.log(error);
}
}
// ============================== DELETE POST
export async function deletePost(postId?: string, imageId?: string) {
if (!postId || !imageId) return;
try {
const statusCode = await databases.deleteDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
postId
);
if (!statusCode) throw Error;
await deleteFile(imageId);
return { status: "Ok" };
} catch (error) {
console.log(error);
}
}
V. Modify the queriesAndMutations.ts
file
Located in src/lib/react-query/queriesAndMutations.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// Source code: https://github.com/adrianhajdin/social_media_app
import {
useQuery,
useMutation,
useQueryClient,
// useInfiniteQuery,
} from "@tanstack/react-query";
import { QUERY_KEYS } from "@/lib/react-query/queryKeys";
import {
createUserAccount,
signInAccount,
signOutAccount,
createPost,
getRecentPosts,
likePost,
savePost,
deleteSavedPost,
getCurrentUser,
getPostById,
updatePost,
deletePost,
} from "@/lib/appwrite/api";
import { INewPost, INewUser, IUpdatePost } from "@/types";
// ============================================================
// AUTH QUERIES
// ============================================================
export const useCreateUserAccount = () => {
return useMutation({
mutationFn: (user: INewUser) => createUserAccount(user),
});
};
export const useSignInAccount = () => {
return useMutation({
mutationFn: (user: { email: string; password: string }) =>
signInAccount(user),
});
};
export const useSignOutAccount = () => {
return useMutation({
mutationFn: signOutAccount,
});
};
// ============================================================
// POST QUERIES
// ============================================================
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (post: INewPost) => createPost(post),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
},
});
};
export const useGetRecentPosts = () => {
return useQuery({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
queryFn: getRecentPosts,
});
};
export const useLikePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
postId,
likesArray,
}: {
postId: string;
likesArray: string[];
}) => likePost(postId, likesArray),
onSuccess: (data) => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POST_BY_ID, data?.$id],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
});
},
});
};
export const useSavePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ userId, postId }: { userId: string; postId: string }) =>
savePost(userId, postId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
});
},
});
};
export const useDeleteSavedPost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (savedRecordId: string) => deleteSavedPost(savedRecordId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
});
},
});
};
// ============================================================
// USER QUERIES
// ============================================================
export const useGetCurrentUser = () => {
return useQuery({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
queryFn: getCurrentUser,
});
};
export const useGetPostById = (postId?: string) => {
return useQuery({
queryKey: [QUERY_KEYS.GET_POST_BY_ID, postId],
queryFn: () => getPostById(postId),
enabled: !!postId,
});
};
export const useUpdatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (post: IUpdatePost) => updatePost(post),
onSuccess: (data) => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POST_BY_ID, data?.$id],
});
},
});
};
export const useDeletePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ postId, imageId }: { postId?: string; imageId: string }) =>
deletePost(postId, imageId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
},
});
};
13. Post Details14
I. Modify the PostDetails.tsx
Located in src/_root/Pages/PostDetails.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { useGetPostById } from "@/lib/react-query/queriesAndMutations";
import { useParams, Link } from "react-router-dom";
import PostStats from "@/components/shared/PostStats";
import { useUserContext } from "@/context/AuthContext";
import { formatDistanceToNowStrict } from "date-fns";
import Loader from "@/components/shared/Loader";
import { Button } from "@/components/ui/button";
const PostDetails = () => {
const { id } = useParams();
const { user } = useUserContext();
// fetch
const { data: post, isPending } = useGetPostById(id);
const handleDeletePost = () => {};
return (
<div className="post_details-container">
{isPending ? (
<Loader />
) : (
<div className="post_details-card">
<img src={post?.imageUrl} alt="post" className="post_details-img" />
<div className="post_details-info">
<div className="flex-between w-full">
<Link
to={`/profile/${post?.creator.$id}`}
className="flex items-center gap-3"
>
<img
src={
post?.creator.imageUrl ||
"/assets/icons/profile-placeholder.svg"
}
alt="creator"
className="w-8 h-8 lg:w-12 lg:h-12 rounded-full"
/>
<div className="flex gap-1 flex-col">
<p className="base-medium lg:body-bold text-light-1">
{post?.creator.name}
</p>
<div className="flex-center gap-2 text-light-3">
<p className="subtle-semibold lg:small-regular ">
{formatDistanceToNowStrict(
new Date(post?.$createdAt || ""),
{
addSuffix: true,
}
)}
</p>
•
<p className="subtle-semibold lg:small-regular">
{post?.location}
</p>
</div>
</div>
</Link>
<div className="flex-center">
<Link
to={`/update-post/${post?.$id}`}
className={`${user.id !== post?.creator.$id && "hidden"}`} // hide edit button if user is not the creator
>
<img
src={"/assets/icons/edit.svg"}
alt="edit"
width={24}
height={24}
/>
</Link>
<Button
onClick={handleDeletePost}
variant="ghost"
className={`ghost_details-delete_btn ${
user.id !== post?.creator.$id && "hidden" // hide delete button if user is not the creator
}`}
>
<img
src={"/assets/icons/delete.svg"}
alt="delete"
width={24}
height={24}
/>
</Button>
</div>
</div>
<hr className="border w-full border-dark-4/80" />
<div className="flex flex-col flex-1 w-full small-medium lg:base-regular">
<p>{post?.caption}</p>
<ul className="flex gap-1 mt-2">
{post?.tags.map((tag: string, index: string) => (
<li
key={`${tag}${index}`}
className="text-light-3 small-regular"
>
#{tag}
</li>
))}
</ul>
</div>
<div className="w-full">
<PostStats post={post} userId={user.id} />
</div>
</div>
</div>
)}
</div>
);
};
export default PostDetails;
II. Modify the PostStats.tsx
Located in src/_root/Pages/PostStats.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import React, { useState, useEffect } from "react";
import {
useLikePost,
useSavePost,
useDeleteSavedPost,
useGetCurrentUser,
} from "@/lib/react-query/queriesAndMutations";
import { Models } from "appwrite";
import { checkIsLiked } from "@/lib/utils";
import Loader from "./Loader";
type PostStatsProps = {
post: Models.Document;
userId: string;
};
const PostStats = ({ post, userId }: PostStatsProps) => {
const likesList = post.likes.map((user: Models.Document) => user.$id); // list of user ids who liked the post
// console.log("likesList:", likesList);
// useState:
const [likes, setLikes] = useState<string[]>(likesList); // list of user ids who liked the post
const [isSaved, setIsSaved] = useState(false); // is the post saved by the current user?
const { mutate: likePost } = useLikePost();
const { mutate: savePost, isPending: isSavingPost } = useSavePost();
const { mutate: deleteSavePost, isPending: isDeletingSaved } =
useDeleteSavedPost();
// who is the current user?
const { data: currentUser } = useGetCurrentUser();
const savedPostRecord = currentUser?.save.find(
(record: Models.Document) => record.post.$id === post.$id
);
useEffect(() => {
setIsSaved(savedPostRecord ? true : false); // or: setIsSaved(!!savedPostRecord);
// { Saved: true } => !savedPostRecord => !false = true
// { Saved: false } => !savedPostRecord => !true = false
}, [currentUser]);
const handleLikePost = (
e: React.MouseEvent<HTMLImageElement, MouseEvent>
) => {
e.stopPropagation();
let likesArray = [...likes];
const hasLiked = likesArray.includes(userId);
if (hasLiked) {
likesArray = likesArray.filter((Id) => Id !== userId);
} else {
likesArray.push(userId);
}
setLikes(likesArray);
likePost({ postId: post.$id, likesArray });
};
const handleSavePost = (
e: React.MouseEvent<HTMLImageElement, MouseEvent>
) => {
e.stopPropagation();
if (savedPostRecord) {
setIsSaved(false);
return deleteSavePost(savedPostRecord.$id);
}
savePost({ userId: userId, postId: post.$id });
setIsSaved(true);
};
const containerStyles = location.pathname.startsWith("/profile")
? "w-full"
: "";
return (
<div
className={`flex justify-between items-center z-20 ${containerStyles}`}
>
<div className="flex gap-2 mr-5">
<img
src={`${
checkIsLiked(likes, userId)
? "/assets/icons/liked.svg"
: "/assets/icons/like.svg"
}`}
alt={checkIsLiked(likes, userId) ? "liked" : "like"}
width={20}
height={20}
onClick={(e) => handleLikePost(e)}
className="cursor-pointer"
/>
<p className="small-medium lg:base-medium">{likes.length}</p>
</div>
<div className="flex gap-2">
{isSavingPost || isDeletingSaved ? (
<Loader />
) : (
<img
src={isSaved ? "/assets/icons/saved.svg" : "/assets/icons/save.svg"}
alt="share"
width={20}
height={20}
className="cursor-pointer"
onClick={handleSavePost}
/>
)}
</div>
</div>
);
};
export default PostStats;
14. Explore Page15
I. Modify the Explore.tsx
page
Located in src/_root/Pages/Explore.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import { useState } from "react";
import { Input } from "@/components/ui/input";
import SearchResults from "@/components/shared/SearchResults";
import GridPostList from "@/components/shared/GridPostList";
import {
useGetPosts,
useSearchPosts,
} from "@/lib/react-query/queriesAndMutations";
import useDebounce from "@/hooks/useDebounce";
import Loader from "@/components/shared/Loader";
const Explore = () => {
const { data: posts, fetchNextPage, hasNextPage } = useGetPosts();
// Seach Posts:
const [searchValue, setSearchValue] = useState("");
const debouncedValue = useDebounce(searchValue, 500);
const { data: searchedPosts, isFetching: isSearchFetching } =
useSearchPosts(debouncedValue);
// console.log(posts);
// posts are undefined on first render
if (!posts) {
return (
<div className="flex-center w-full h-full">
<Loader />
</div>
);
}
const shouldShowSearchResults = searchValue !== "";
const shouldShowPosts =
!shouldShowSearchResults &&
posts.pages.every((item) => item.documents.length === 0);
return (
<div className="explore-container">
<div className="explore-inner_container">
<h2 className="h3-bold md:h2-bold w-full">Seach Posts</h2>
<div className="flex gap-1 px-4 w-full rounded-1g bg-dark-4">
<img
src=" /assets/icons/search.svg"
width={24}
height={24}
alt="search"
/>
<Input
type="text"
placeholder="Search"
className="explore-search"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
</div>
</div>
<div className="flex-between w-full max-w-5x1 mt-16 mb-7">
<h3 className="body-bold md:h3-bold">Popular Today</h3>
<div className="flex-center gap-3 bg-dark-3 rounded-xI px-4 py-2 cursor-pointer">
<p className=" small-medium md:base-medium text-light-2">All</p>
<img
src="/assets/icons/filter.svg"
width={20}
height={20}
alt="filter"
/>
</div>
</div>
<div className="flex flex-wrap gap-9 w-full max-w-5x1">
{shouldShowSearchResults ? (
<SearchResults />
) : shouldShowPosts ? (
<p className="text-light-4 mt-10 text-center w-full">End of posts</p>
) : (
posts.pages.map((item, index) => (
<GridPostList key={`page-${index}`} posts={item.documents} />
))
)}
</div>
</div>
);
};
export default Explore;
II. Create Component SearchResults.tsx
Located in src/components/shared/SearchResults.tsx
and run rafce
.
III. Create Component GridPostList.tsx
Located in src/components/shared/GridPostList.tsx
and run rafce
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { Models } from "appwrite";
import { Link } from "react-router-dom";
import { useUserContext } from "@/context/AuthContext";
import PostStats from "./PostStats";
type GridPostListProps = {
posts: Models.Document[];
showUser?: boolean;
showStats?: boolean;
};
const GridPostList = ({
posts,
showUser = true,
showStats = true,
}: GridPostListProps) => {
const { user } = useUserContext();
return (
<ul className="grid-container">
{posts.map((post) => (
<li key={post.$id} className="relative min-w-80 h-80">
<Link to={`/posts/${post.$id}`} className="grid-post_link">
<img
src={post.imageUrl}
alt="post"
className="h-full w-full object-cover"
/>
</Link>
<div className="grid-post_user">
{showUser && (
<div className="flex items-center justify-start gap-2 flex-1">
<img
src={
post?.creator?.imageUrl ||
"/assets/icons/profile-placeholder.svg"
}
alt="creator"
className="w-8 h-8 rounded-full"
/>
<p className="line-clamp-1">{post?.creator?.name}</p>
</div>
)}
{showStats && <PostStats post={post} userId={user.id} />}
</div>
</li>
))}
</ul>
);
};
export default GridPostList;
IV. Create the useDebounce.ts
Located in src/hooks/useDebounce.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Source code: https://gist.github.com/adrianhajdin/4d2500bf5af601bbd9f4f596298d33ac
import { useEffect, useState } from "react";
// https://codesandbox.io/s/react-query-debounce-ted8o?file=/src/useDebounce.js
export default function useDebounce<T>(value: T, delay: number): T {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
}, [value, delay]); // Only re-call effect if value or delay changes
return debouncedValue;
}
from Github gist
Learn More about Debouncing Method
V. Modify the api.ts
Located in src/lib/appwrite/api.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
// Source code: https://github.com/adrianhajdin/social_media_app
import { ID, Query } from "appwrite";
import { appwriteConfig, account, databases, avatars, storage } from "./config";
import { INewPost, INewUser, IUpdatePost } from "@/types";
// ============================================================
// AUTH
// ============================================================
// ============================== SIGN UP
export async function createUserAccount(user: INewUser) {
try {
const newAccount = await account.create(
ID.unique(),
user.email,
user.password,
user.name
);
if (!newAccount) throw Error;
const avatarUrl = avatars.getInitials(user.name);
const newUser = await saveUserToDB({
accountId: newAccount.$id,
email: newAccount.email,
name: newAccount.name,
username: user.username,
imageUrl: avatarUrl,
});
return newUser;
console.log("new user created: " + newUser);
} catch (error) {
console.log(error);
return error;
}
}
// ============================== SAVE USER TO DB
export async function saveUserToDB(user: {
accountId: string;
email: string;
name: string;
imageUrl: URL;
username?: string;
}) {
try {
const newUser = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
ID.unique(),
user
);
return newUser;
} catch (error) {
console.log(error);
}
}
// ============================== SIGN IN
export async function signInAccount(user: { email: string; password: string }) {
try {
const session = await account.createEmailSession(user.email, user.password);
return session;
} catch (error) {
console.log(error);
}
}
// ============================== GET USER
export async function getCurrentUser() {
try {
const currentAccount = await account.get();
if (!currentAccount) throw Error;
const currentUser = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.userCollectionId,
[Query.equal("accountId", currentAccount.$id)]
);
if (!currentUser) throw Error;
return currentUser.documents[0];
} catch (error) {
console.log(error);
return null;
}
}
// ============================== SIGN OUT
export async function signOutAccount() {
try {
const session = await account.deleteSession("current");
return session;
} catch (error) {
console.log(error);
}
}
// ============================================================
// POSTS
// ============================================================
// ============================== CREATE POST
export async function createPost(post: INewPost) {
try {
// *** Upload file to appwrite storage ***
const uploadedFile = await uploadFile(post.file[0]);
// If No file, throw error
if (!uploadedFile) throw Error;
// *** Get file url ***
const fileUrl = getFilePreview(uploadedFile.$id);
// If no file url, delete file from storage and throw error
if (!fileUrl) {
await deleteFile(uploadedFile.$id);
throw Error;
}
// *** Convert tags into array ***
const tags = post.tags?.replace(/ /g, "").split(",") || [];
// *** Create post ***
const newPost = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
ID.unique(),
{
creator: post.userId,
caption: post.caption,
imageUrl: fileUrl,
imageId: uploadedFile.$id,
location: post.location,
tags: tags,
}
);
// If No New POST, delete file from storage
if (!newPost) {
await deleteFile(uploadedFile.$id);
throw Error;
}
return newPost;
} catch (error) {
console.log(error);
}
}
// ============================== UPLOAD FILE
export async function uploadFile(file: File) {
try {
const uploadedFile = await storage.createFile(
appwriteConfig.storageId,
ID.unique(),
file
);
return uploadedFile;
} catch (error) {
console.log(error);
}
}
// ============================== GET FILE URL
export function getFilePreview(fileId: string) {
try {
const fileUrl = storage.getFilePreview(
appwriteConfig.storageId,
fileId,
2000,
2000,
"top",
100
);
if (!fileUrl) throw Error;
return fileUrl;
} catch (error) {
console.log(error);
}
}
// ============================== DELETE FILE
export async function deleteFile(fileId: string) {
try {
await storage.deleteFile(appwriteConfig.storageId, fileId);
return { status: "ok" };
} catch (error) {
console.log(error);
}
}
// ============================== GET POPULAR POSTS (BY HIGHEST LIKE COUNT)
export async function getRecentPosts() {
try {
const posts = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
[Query.orderDesc("$createdAt"), Query.limit(20)]
);
if (!posts) throw Error;
return posts;
} catch (error) {
console.log(error);
}
}
// ============================== LIKE / UNLIKE POST
export async function likePost(postId: string, likesArray: string[]) {
try {
const updatedPost = await databases.updateDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
postId,
{
likes: likesArray,
}
);
// if there is no updated post, throw error
if (!updatedPost) throw Error;
return updatedPost;
} catch (error) {
console.log(error);
}
}
// ============================== SAVE POST
export async function savePost(userId: string, postId: string) {
try {
const updatedPost = await databases.createDocument(
appwriteConfig.databaseId,
appwriteConfig.savesCollectionId,
ID.unique(),
{
user: userId,
post: postId,
}
);
if (!updatedPost) throw Error;
return updatedPost;
} catch (error) {
console.log(error);
}
}
// ============================== DELETE SAVED POST
export async function deleteSavedPost(savedRecordId: string) {
try {
const statusCode = await databases.deleteDocument(
appwriteConfig.databaseId,
appwriteConfig.savesCollectionId,
savedRecordId
);
if (!statusCode) throw Error;
return { status: "Ok" };
} catch (error) {
console.log(error);
}
}
// ============================== GET POST BY ID
export async function getPostById(postId?: string) {
if (!postId) throw Error;
try {
const post = await databases.getDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
postId
);
if (!post) throw Error;
return post;
} catch (error) {
console.log(error);
}
}
// ============================== UPDATE POST
export async function updatePost(post: IUpdatePost) {
const hasFileToUpdate = post.file.length > 0;
try {
let image = {
imageUrl: post.imageUrl,
imageId: post.imageId,
};
if (hasFileToUpdate) {
// Upload new file to appwrite storage
const uploadedFile = await uploadFile(post.file[0]);
if (!uploadedFile) throw Error;
// Get new file url
const fileUrl = getFilePreview(uploadedFile.$id);
if (!fileUrl) {
await deleteFile(uploadedFile.$id);
throw Error;
}
image = { ...image, imageUrl: fileUrl, imageId: uploadedFile.$id };
}
// Convert tags into array
const tags = post.tags?.replace(/ /g, "").split(",") || [];
// Update post
const updatedPost = await databases.updateDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
post.postId,
{
caption: post.caption,
imageUrl: image.imageUrl,
imageId: image.imageId,
location: post.location,
tags: tags,
}
);
// Failed to update
if (!updatedPost) {
// Delete new file that has been recently uploaded
if (hasFileToUpdate) {
await deleteFile(image.imageId);
}
// If no new file uploaded, just throw error
throw Error;
}
// Safely delete old file after successful update
if (hasFileToUpdate) {
await deleteFile(post.imageId);
}
return updatedPost;
} catch (error) {
console.log(error);
}
}
// ============================== DELETE POST
export async function deletePost(postId?: string, imageId?: string) {
if (!postId || !imageId) return;
try {
const statusCode = await databases.deleteDocument(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
postId
);
if (!statusCode) throw Error;
await deleteFile(imageId);
return { status: "Ok" };
} catch (error) {
console.log(error);
}
}
// ============================== GET POSTS
export async function searchPosts(searchTerm: string) {
try {
const posts = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
[Query.search("caption", searchTerm)]
);
if (!posts) throw Error;
return posts;
} catch (error) {
console.log(error);
}
}
export async function getInfinitePosts({ pageParam }: { pageParam: number }) {
const queries: any[] = [Query.orderDesc("$updatedAt"), Query.limit(10)];
if (pageParam) {
queries.push(Query.cursorAfter(pageParam.toString()));
}
try {
const posts = await databases.listDocuments(
appwriteConfig.databaseId,
appwriteConfig.postCollectionId,
queries
);
if (!posts) throw Error;
return posts;
} catch (error) {
console.log(error);
}
}
VI. Modify the queriesAndMutations.ts
file
Located in src/lib/react-query/queriesAndMutations.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Source code: https://github.com/adrianhajdin/social_media_app
import {
useQuery,
useMutation,
useQueryClient,
useInfiniteQuery,
} from "@tanstack/react-query";
import { QUERY_KEYS } from "@/lib/react-query/queryKeys";
import {
createUserAccount,
signInAccount,
signOutAccount,
createPost,
getRecentPosts,
likePost,
savePost,
deleteSavedPost,
getCurrentUser,
getPostById,
updatePost,
deletePost,
searchPosts,
getInfinitePosts,
} from "@/lib/appwrite/api";
import { INewPost, INewUser, IUpdatePost } from "@/types";
// ============================================================
// AUTH QUERIES
// ============================================================
export const useCreateUserAccount = () => {
return useMutation({
mutationFn: (user: INewUser) => createUserAccount(user),
});
};
export const useSignInAccount = () => {
return useMutation({
mutationFn: (user: { email: string; password: string }) =>
signInAccount(user),
});
};
export const useSignOutAccount = () => {
return useMutation({
mutationFn: signOutAccount,
});
};
// ============================================================
// POST QUERIES
// ============================================================
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (post: INewPost) => createPost(post),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
},
});
};
export const useGetRecentPosts = () => {
return useQuery({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
queryFn: getRecentPosts,
});
};
export const useLikePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
postId,
likesArray,
}: {
postId: string;
likesArray: string[];
}) => likePost(postId, likesArray),
onSuccess: (data) => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POST_BY_ID, data?.$id],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
});
},
});
};
export const useSavePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ userId, postId }: { userId: string; postId: string }) =>
savePost(userId, postId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
});
},
});
};
export const useDeleteSavedPost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (savedRecordId: string) => deleteSavedPost(savedRecordId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POSTS],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
});
},
});
};
// ============================================================
// USER QUERIES
// ============================================================
export const useGetPosts = () => {
return useInfiniteQuery({
queryKey: [QUERY_KEYS.GET_INFINITE_POSTS],
queryFn: getInfinitePosts as any,
getNextPageParam: (lastPage: any) => {
// If there's no data, there are no more pages.
if (lastPage && lastPage.documents.length === 0) {
return null;
}
// Use the $id of the last document as the cursor.
const lastId = lastPage.documents[lastPage.documents.length - 1].$id;
return lastId;
},
});
};
export const useSearchPosts = (searchTerm: string) => {
return useQuery({
queryKey: [QUERY_KEYS.SEARCH_POSTS, searchTerm],
queryFn: () => searchPosts(searchTerm),
enabled: !!searchTerm,
});
};
export const useGetCurrentUser = () => {
return useQuery({
queryKey: [QUERY_KEYS.GET_CURRENT_USER],
queryFn: getCurrentUser,
});
};
export const useGetPostById = (postId?: string) => {
return useQuery({
queryKey: [QUERY_KEYS.GET_POST_BY_ID, postId],
queryFn: () => getPostById(postId),
enabled: !!postId,
});
};
export const useUpdatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (post: IUpdatePost) => updatePost(post),
onSuccess: (data) => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_POST_BY_ID, data?.$id],
});
},
});
};
export const useDeletePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ postId, imageId }: { postId?: string; imageId: string }) =>
deletePost(postId, imageId),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.GET_RECENT_POSTS],
});
},
});
};
15. Search Results16
I. Modify Component SearchResults.tsx
Located in src/components/shared/SearchResults.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Models } from "appwrite";
import Loader from "./Loader";
import GridPostList from "./GridPostList";
type SearchResultsProps = {
isSearchFetching: boolean;
searchedPosts: Models.Document[];
};
const SearchResults = ({
isSearchFetching,
searchedPosts,
}: SearchResultsProps) => {
if (isSearchFetching) return <Loader />;
if (searchedPosts && searchedPosts.documents.length > 0) {
return <GridPostList posts={searchedPosts.documents} />;
}
return (
<p className="text-light-4 mt-10 text-center w-full">No results found</p>
);
};
export default SearchResults;
II. Install React Intersection Observer
By running:
1
npm install react-intersection-observer --save
III. Modify the Explore.tsx
page
Located in src/_root/Pages/Explore.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import { useState, useEffect } from "react";
import { Input } from "@/components/ui/input";
import SearchResults from "@/components/shared/SearchResults";
import GridPostList from "@/components/shared/GridPostList";
import {
useGetPosts,
useSearchPosts,
} from "@/lib/react-query/queriesAndMutations";
import useDebounce from "@/hooks/useDebounce";
import Loader from "@/components/shared/Loader";
import { useInView } from "react-intersection-observer";
const Explore = () => {
const { ref, inView } = useInView({ threshold: 0.5 });
const { data: posts, fetchNextPage, hasNextPage } = useGetPosts();
// Seach Posts:
const [searchValue, setSearchValue] = useState("");
const debouncedValue = useDebounce(searchValue, 500);
const { data: searchedPosts, isFetching: isSearchFetching } =
useSearchPosts(debouncedValue);
// infinite scroll
useEffect(() => {
if (inView && !searchValue) fetchNextPage();
}, [inView, searchValue]);
// console.log(posts);
// posts are undefined on first render
if (!posts) {
return (
<div className="flex-center w-full h-full">
<Loader />
</div>
);
}
const shouldShowSearchResults = searchValue !== "";
const shouldShowPosts =
!shouldShowSearchResults &&
posts.pages.every((item) => item.documents.length === 0);
return (
<div className="explore-container">
<div className="explore-inner_container">
<h2 className="h3-bold md:h2-bold w-full">Seach Posts</h2>
<div className="flex gap-1 px-4 w-full rounded-1g bg-dark-4">
<img
src=" /assets/icons/search.svg"
width={24}
height={24}
alt="search"
/>
<Input
type="text"
placeholder="Search"
className="explore-search"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
</div>
</div>
<div className="flex-between w-full max-w-5x1 mt-16 mb-7">
<h3 className="body-bold md:h3-bold">Popular Today</h3>
<div className="flex-center gap-3 bg-dark-3 rounded-xI px-4 py-2 cursor-pointer">
<p className=" small-medium md:base-medium text-light-2">All</p>
<img
src="/assets/icons/filter.svg"
width={20}
height={20}
alt="filter"
/>
</div>
</div>
<div className="flex flex-wrap gap-9 w-full max-w-5x1">
{shouldShowSearchResults ? (
<SearchResults
isSearchFetching={isSearchFetching}
searchedPosts={searchedPosts}
/>
) : shouldShowPosts ? (
<p className="text-light-4 mt-10 text-center w-full">End of posts</p>
) : (
posts.pages.map((item, index) => (
<GridPostList key={`page-${index}`} posts={item.documents} />
))
)}
</div>
{/* Infinite Scroll: */}
{hasNextPage && !searchValue && (
<div ref={ref} className="mt-10">
<Loader />
</div>
)}
</div>
);
};
export default Explore;
The infinite Scrolling was implemented here. Check an example provided by React Query on Infinite Scroll.
16. Active Lesson17
Link
17. Deployment18
This project will be deploy using:
- Go to their website and login with Github.
- In the Vercel Dashboard, click on
Add New
>Project
- Select the Github Repo from the list
- Under Configure Project > Environment Variables
Add the Environment Variables to Vercel by coping the code from
.env.local
- Click
Deploy
Deployment is Done but Trow Error
The page is up but there is an error due to the CORS policy.
How to Fix The CORS Policy Error?
- Go to the
Project
in Appwrite > and add the new plataform- Project:
SocialMedia
- Add a Platform:
web
>+
- Project:
- Under
Register your hostname
- Name:
socialmedia
- Hostname:
*.vercel.app
- Click
Next
- Click
Skip optional Steps
- Name:
License 📜
Distributed under the MIT License. See LICENSE.txt
for more information.
Projects 🚀
Courses & Certifications
For more information regarding my completed courses and certificates, please click on:
Acknowledgments 📚
Resources list that I find helpful and would like to give credit to.
- Tailwindcss.com
- Tailwind CSS Cheat Sheet
- Query Key
- Query Function
- Infinite Query
- Appwrite Pagination
- Appwrite Queries
- Appwrite List Documents
- React Intersection Observer