Sıfırdan ileriye ReactJs serisinin yeni konusuna hoş geldiniz. Geçen konuda hatırlarsanız Error sayfası, Ana sayfa ve React-Redux kurulumları yapmıştık. Bugün artık daha çok asenkron işlemlerine geçeceğiz. Veri tabanında veri yollama - veri çekme gibi işlemler yapacağız. Önceki 2 konu daha çok tasarımsal iken bu konuda artık daha çok veri tabanını kullanacağız iyi okumalar.
UserSlicer Reduxunu Hazırlama
Geçen konu temel olarak 3 tane slicer yazmıştık. Bugün UserSlicer bitireceğiz. UserSlicer içinde kullanıcıyı sisteme kayıt etme, giriş yapma, kullanıcı verilerini çekme ve kullanıcıyı silme gibi işlemleri yapacağız.
JavaScript:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import supabase from "../utils/supabase"
export const registerUser = createAsyncThunk(
"user/register",
async (user, thunkAPI) => {
try {
const { data, error } = await supabase.from("users").insert(user);
if (error) {
console.error("Supabase insert error:", error.message);
return thunkAPI.rejectWithValue(error.message);
}
return data;
} catch (err) {
console.error("Unexpected error:", err);
return thunkAPI.rejectWithValue("Beklenmedik bir hata oluştu");
}
}
);
export const loginUser = createAsyncThunk(
"user/login",
async (user, thunkAPI) => {
function createToken(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
try {
const { email, password } = user;
const { data, error } = await supabase.from("users").select("*").eq("email", email).single();
if (error || !data) {
return thunkAPI.rejectWithValue("User not found");
}
if (data.password !== password) {
return thunkAPI.rejectWithValue("Incorrect password");
}
const token = createToken(30);
localStorage.setItem("token", token);
localStorage.setItem("email", email);
localStorage.setItem("Role", data?.Role)
return data;
} catch (err) {
return thunkAPI.rejectWithValue("Unexpected error");
}
}
);
export const getUser = createAsyncThunk(
"user/getUser",
async (_, thunkAPI) => {
try {
const email = localStorage.getItem("email");
if (!email) {
return thunkAPI.rejectWithValue("No email found in localStorage");
}
const { data, error } = await supabase.from("users").select("*").eq("email", email).single();
if (error) {
return thunkAPI.rejectWithValue(error.message);
}
return data;
} catch (error) {
return thunkAPI.rejectWithValue("Unexpected error");
}
}
);
export const deleteUser = createAsyncThunk(
"user/deleteUser",
async (_, thunkAPI) => {
try {
const email = localStorage.getItem("email");
if (!email) {
return thunkAPI.rejectWithValue("No email found in localStorage");
}
const { data, error } = await supabase.from("users").delete().eq("email", email);
if (error) {
return thunkAPI.rejectWithValue(error.message);
}
return data;
} catch (error) {
return thunkAPI.rejectWithValue("Unexpected error");
}
}
)
const initialState = {
user: null,
loading: false,
error: null,
}
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(loginUser.fulfilled, (state, action) => {
state.user = action.payload;
state.loading = false
})
.addCase(loginUser.pending, (state) => {
state.loading = true
})
.addCase(loginUser.rejected, (state, action) => {
state.error = action.error.message;
state.loading = false;
})
.addCase(getUser.fulfilled, (state, action) => {
state.user = action.payload;
state.loading = false
})
.addCase(getUser.pending, (state) => {
state.loading = true
})
.addCase(getUser.rejected, (state, action) => {
state.error = action.error.message;
state.loading = false;
})
}
})
export const { } = userSlice.actions;
export default userSlice.reducer;
Şimdi detaylıca kodlara bakalım.
JavaScript:
export const registerUser = createAsyncThunk(
"user/register",
async (user, thunkAPI) => {
try {
const { data, error } = await supabase.from("users").insert(user);
if (error) {
console.error("Supabase insert error:", error.message);
return thunkAPI.rejectWithValue(error.message);
}
return data;
} catch (err) {
console.error("Unexpected error:", err);
return thunkAPI.rejectWithValue("Beklenmedik bir hata oluştu");
}
}
);
Slicer'a kullanıcıyı sisteme kayıt etme ile başlıyoruz.
createAsyncThunk ile asenkron bir thunk oluşturuyoruz bir nevi endpoint diyebiliriz. Daha sonra bu thunk'a bir isim veriyoruz. "user/register" adını verdikten sonra asenkron işlememize başlıyoruz. Bu işlem için user verisine ihtiyaç olacak.
supabase içinden users adlı tabloyu alıyoruz ve insert ile kullanıcıdan alınan verileri tabloya ekliyoruz. Eğer error gelirse bunu return ediyoruz. Herhangi bir sorun çıkmaz ise işlememiz tamamlanıyor.
Şimdi giriş yapma işlemine geçelim.
JavaScript:
export const loginUser = createAsyncThunk(
"user/login",
async (user, thunkAPI) => {
function createToken(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
try {
const { email, password } = user;
const { data, error } = await supabase.from("users").select("*").eq("email", email).single();
if (error || !data) {
return thunkAPI.rejectWithValue("User not found");
}
if (data.password !== password) {
return thunkAPI.rejectWithValue("Incorrect password");
}
const token = createToken(30);
localStorage.setItem("token", token);
localStorage.setItem("email", email);
localStorage.setItem("Role", data?.Role)
return data;
} catch (err) {
return thunkAPI.rejectWithValue("Unexpected error");
}
}
);
Yine bir user verisi alıyoruz en başta. Daha sonra createToken adlı bir token oluşturma fonksiyonu yapıyoruz. Normalde token işlemleri back-end ile yapılır ama back-end olmadan proje yaptığımız için bu şekilde geçici bir çözüm yapabiliriz.
user değişkeni içinden email ve password değerlerini alyoruz ve users tablosundaki kayıtlı email listesiyle alınan email değerini karşılaştırıyoruz. Eğer bu iki mail de uyuşuyorsa kullanıcının tüm verilerini alıyoruz.
Yazdığımız token fonksiyonunu kullanarak local storage içine token, email ve rol bilgilerini yazdırıyoruz. Bu şekilde kullanıcı giriş yapmış olacak.
Şimdi kullanıcı verilerini alacağız.
JavaScript:
export const getUser = createAsyncThunk(
"user/getUser",
async (_, thunkAPI) => {
try {
const email = localStorage.getItem("email");
if (!email) {
return thunkAPI.rejectWithValue("No email found in localStorage");
}
const { data, error } = await supabase.from("users").select("*").eq("email", email).single();
if (error) {
return thunkAPI.rejectWithValue(error.message);
}
return data;
} catch (error) {
return thunkAPI.rejectWithValue("Unexpected error");
}
}
);
Giriş yaparken local storage kayıt edilen email değerini local storage üzerinden alıp veri tabanında karşılaştırıyoruz. Eğer mailler uyuşur ise kullanıcı verilerini alıyoruz.
Şimdi ise var olan kullanıcıyı silelim.
JavaScript:
export const deleteUser = createAsyncThunk(
"user/deleteUser",
async (_, thunkAPI) => {
try {
const email = localStorage.getItem("email");
if (!email) {
return thunkAPI.rejectWithValue("No email found in localStorage");
}
const { data, error } = await supabase.from("users").delete().eq("email", email);
if (error) {
return thunkAPI.rejectWithValue(error.message);
}
return data;
} catch (error) {
return thunkAPI.rejectWithValue("Unexpected error");
}
}
)
Hali hazırda local storage üzerinde bulunan maili alıp veri tabanında karşılaştırma yapıyoruz. Eğer uyuşuyor ise hesabı tamamen siliyoruz.
Son olarak oluşturulan stateleri güncelleyelim.
JavaScript:
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(loginUser.fulfilled, (state, action) => {
state.user = action.payload;
state.loading = false
})
.addCase(loginUser.pending, (state) => {
state.loading = true
})
.addCase(loginUser.rejected, (state, action) => {
state.error = action.error.message;
state.loading = false;
})
.addCase(getUser.fulfilled, (state, action) => {
state.user = action.payload;
state.loading = false
})
.addCase(getUser.pending, (state) => {
state.loading = true
})
.addCase(getUser.rejected, (state, action) => {
state.error = action.error.message;
state.loading = false;
})
}
})
Son olarak ise yazdığımız endpointlere durumlar ekliyoruz. İsteğin başarılı olmasında istekten dönen yanıtları oluşturulan user state atıyoruz. Eğer istek yollanıyorsa loading state true yapıyoruz eğer istekte sorun oluştu ise error state güncelliyoruz.
Böylece artık UserSlicer için kullanacağımız fonksiyonları yazdık. Şimdi Yup kullanarak form şemaları oluşturacağız. Bunun için validation altındaki klasördeki dosyayı düzenleyeceğiz.
Yup ile Validation İşlemleri
JavaScript:
import * as Yup from "yup";
export const registerSchema = Yup.object().shape({
first_name: Yup.string().required("First Name is required"),
last_name: Yup.string().required("Last Name is required"),
email: Yup.string().email("Invalid email format").required("Email is required"),
age: Yup.number().min(18, "You must be at least 18 years old").required("Age is required"),
password: Yup.string().required("Password is required").min(5, "Password must be minimum 5 characters").max(18, "Password must be maximum 18 characters"),
rePassword: Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match").required("Please confirm your password"),
});
export const loginSchema = Yup.object().shape({
email: Yup.string().email("Invalid email format").required("Email is required"),
password: Yup.string().required("Password is required").min(5, "Password must be minimum 5 characters").max(18, "Password must be maximum 18 characters"),
})
export const addHotelSchema = Yup.object().shape({
name: Yup.string().required("Name is required"),
description: Yup.string().required("Description is required"),
price: Yup.number().required("Number is required"),
rate: Yup.number().required("Rate is required"),
location: Yup.string().required("Location is required"),
image: Yup.string().required("Image is required"),
})
registerSchema ile kayıt olma şemasını oluşturuyoruz. Ad, soyad email, yaş, şifre ve şifre tekrarı bilgilerini istiyoruz. Bunların hepsi zorunlu olduğu için boş geçilemez.
loginSchema ile giriş yapma şemasını oluşturuyoruz.
addHotelSchema ise buraya daha sonra geleceğiz ama şimdilik durabilir, bu şema ise hotel eklerken kullanacağız.
Kullanıcı Giriş Yapma Sayfası
Şimdi register sayfasının front-en sayfasını yazalım.
JavaScript:
import { useState } from "react";
import {
Box,
TextField,
Button,
Typography,
InputAdornment,
IconButton,
} from "@mui/material";
import { useNavigate } from "react-router-dom";
import { Visibility, VisibilityOff } from "@mui/icons-material";
import { Formik } from "formik";
import { registerSchema } from "../../validation/Yup";
import { registerUser } from "../../api/UserSlicer";
import { toast } from "react-toastify";
import { useDispatch } from 'react-redux'
import supabase from "../../utils/supabase";
function Register() {
const dispatch = useDispatch()
const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false);
const [showRePassword, setShowRePassword] = useState(false);
const togglePassword = () => setShowPassword(!showPassword);
const toggleRePassword = () => setShowRePassword(!showRePassword);
const checkEmailExists = async (email) => {
const { data, error } = await supabase
.from("users")
.select("email")
.eq("email", email);
if (error) {
console.error(error);
return false;
}
return data.length > 0;
};
const inputStyles = {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "#5bd85b",
borderRadius: "30px",
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "#3dc23d",
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: "#1f9e1f",
},
"& .MuiInputBase-input": {
caretColor: "#0a630a",
color: "#0a630a",
},
"& .MuiInputLabel-root": {
color: "black",
},
"& .MuiInputLabel-root.Mui-focused": {
color: "black",
},
};
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
backgroundColor: "#e8f5e9",
}}
>
<Formik
initialValues={{
first_name: "",
last_name: "",
email: "",
age: "",
password: "",
rePassword: "",
Role: "User"
}}
validationSchema={registerSchema}
onSubmit={async (values) => {
const { rePassword, ...userData } = values;
try {
const emailExists = await checkEmailExists(values.email);
if (emailExists) {
toast.error("This email is already registered!");
return;
}
else {
const result = await dispatch(registerUser(userData));
if (result.payload?.error) {
toast.error(result.payload.error);
return;
}
toast.success("Registration successful!");
navigate("/auth/login");
}
} catch (error) {
toast.error("Something went wrong!");
}
}}
>
{({
values,
handleChange,
handleBlur,
handleSubmit,
errors,
touched,
}) => (
<Box
component="form"
onSubmit={handleSubmit}
sx={{
width: 400,
p: 4,
borderRadius: 4,
backgroundColor: "#fff",
boxShadow: "0 4px 20px rgba(0,0,0,0.1)",
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<Typography
variant="h4"
textAlign="center"
fontWeight="bold"
color="#1b5e20"
>
Register
</Typography>
<TextField
label="Name"
name="first_name"
variant="outlined"
fullWidth
value={values.first_name}
onChange={handleChange}
onBlur={handleBlur}
error={touched.first_name && Boolean(errors.first_name)}
helperText={touched.first_name && errors.first_name}
sx={inputStyles}
/>
<TextField
label="Surname"
name="last_name"
variant="outlined"
fullWidth
value={values.last_name}
onChange={handleChange}
onBlur={handleBlur}
error={touched.last_name && Boolean(errors.last_name)}
helperText={touched.last_name && errors.last_name}
sx={inputStyles}
/>
<TextField
label="E-mail"
name="email"
type="email"
variant="outlined"
fullWidth
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
sx={inputStyles}
/>
<TextField
label="Age"
name="age"
type="number"
variant="outlined"
fullWidth
value={values.age}
onChange={handleChange}
onBlur={handleBlur}
error={touched.age && Boolean(errors.age)}
helperText={touched.age && errors.age}
sx={inputStyles}
/>
<TextField
label="Password"
name="password"
type={showPassword ? "text" : "password"}
variant="outlined"
fullWidth
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
error={touched.password && Boolean(errors.password)}
helperText={touched.password && errors.password}
sx={inputStyles}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={togglePassword} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
<TextField
label="Confirm Password"
name="rePassword"
type={showRePassword ? "text" : "password"}
variant="outlined"
fullWidth
value={values.rePassword}
onChange={handleChange}
onBlur={handleBlur}
error={touched.rePassword && Boolean(errors.rePassword)}
helperText={touched.rePassword && errors.rePassword}
sx={inputStyles}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={toggleRePassword} edge="end">
{showRePassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
<Typography
sx={{
color: "green",
fontSize: "0.9rem",
cursor: "pointer",
position: "relative",
"&::after": {
content: '""',
position: "absolute",
left: 0,
bottom: -2,
width: 0,
height: "1px",
backgroundColor: "green",
transition: "width 0.3s ease",
},
"&:hover::after": {
width: "55%",
},
}}
onClick={() => navigate("/auth/login")}
>
Already have an account?
</Typography>
<Button
type="submit"
variant="contained"
fullWidth
sx={{
mt: 1,
py: 1.2,
backgroundColor: "#2e7d32",
borderRadius: "30px",
fontSize: "1rem",
fontWeight: "bold",
textTransform: "none",
"&:hover": {
backgroundColor: "#43a047",
},
}}
>
Register
</Button>
</Box>
)}
</Formik>
</Box>
);
}
export default Register;
Şimdi kodları açıklayalım.
JavaScript:
const dispatch = useDispatch()
const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false);
const [showRePassword, setShowRePassword] = useState(false);
const togglePassword = () => setShowPassword(!showPassword);
const toggleRePassword = () => setShowRePassword(!showRePassword);
const checkEmailExists = async (email) => {
const { data, error } = await supabase
.from("users")
.select("email")
.eq("email", email);
if (error) {
console.error(error);
return false;
}
return data.length > 0;
};
const inputStyles = {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "#5bd85b",
borderRadius: "30px",
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "#3dc23d",
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: "#1f9e1f",
},
"& .MuiInputBase-input": {
caretColor: "#0a630a",
color: "#0a630a",
},
"& .MuiInputLabel-root": {
color: "black",
},
"& .MuiInputLabel-root.Mui-focused": {
color: "black",
},
};
Yönlendirme için navigate hook'u, alınan verileri redux'a yollamak için ise dispatch kullanacağız. Redux ise veri tabanına isteklerde bulunacak.
ShowPassword ve ShowRePassword stateleri ise parola inputlarında şifreyi gösterip kapatma için kullanılacak.
checkEmailExists fonksiyonu ile mail değeri alıyoruz ve alınan mailin veri tabanında zaten hali hazırda kayıtlı olup olmadığına bakıyoruz. Bunu aslında UserSlicer içinde de yapabilirdik tercih sizin.
inputStyles ile ise kendi input tasarımını yapıyoruz.
JavaScript:
<Formik
initialValues={{
first_name: "",
last_name: "",
email: "",
age: "",
password: "",
rePassword: "",
Role: "User"
}}
validationSchema={registerSchema}
onSubmit={async (values) => {
const { rePassword, ...userData } = values;
try {
const emailExists = await checkEmailExists(values.email);
if (emailExists) {
toast.error("This email is already registered!");
return;
}
else {
const result = await dispatch(registerUser(userData));
if (result.payload?.error) {
toast.error(result.payload.error);
return;
}
toast.success("Registration successful!");
navigate("/auth/login");
}
} catch (error) {
toast.error("Something went wrong!");
}
}}
>
{({
values,
handleChange,
handleBlur,
handleSubmit,
errors,
touched,
}) => (
<Box
component="form"
onSubmit={handleSubmit}
sx={{
width: 400,
p: 4,
borderRadius: 4,
backgroundColor: "#fff",
boxShadow: "0 4px 20px rgba(0,0,0,0.1)",
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<Typography
variant="h4"
textAlign="center"
fontWeight="bold"
color="#1b5e20"
>
Register
</Typography>
<TextField
label="Name"
name="first_name"
variant="outlined"
fullWidth
value={values.first_name}
onChange={handleChange}
onBlur={handleBlur}
error={touched.first_name && Boolean(errors.first_name)}
helperText={touched.first_name && errors.first_name}
sx={inputStyles}
/>
<TextField
label="Surname"
name="last_name"
variant="outlined"
fullWidth
value={values.last_name}
onChange={handleChange}
onBlur={handleBlur}
error={touched.last_name && Boolean(errors.last_name)}
helperText={touched.last_name && errors.last_name}
sx={inputStyles}
/>
<TextField
label="E-mail"
name="email"
type="email"
variant="outlined"
fullWidth
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
sx={inputStyles}
/>
<TextField
label="Age"
name="age"
type="number"
variant="outlined"
fullWidth
value={values.age}
onChange={handleChange}
onBlur={handleBlur}
error={touched.age && Boolean(errors.age)}
helperText={touched.age && errors.age}
sx={inputStyles}
/>
<TextField
label="Password"
name="password"
type={showPassword ? "text" : "password"}
variant="outlined"
fullWidth
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
error={touched.password && Boolean(errors.password)}
helperText={touched.password && errors.password}
sx={inputStyles}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={togglePassword} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
<TextField
label="Confirm Password"
name="rePassword"
type={showRePassword ? "text" : "password"}
variant="outlined"
fullWidth
value={values.rePassword}
onChange={handleChange}
onBlur={handleBlur}
error={touched.rePassword && Boolean(errors.rePassword)}
helperText={touched.rePassword && errors.rePassword}
sx={inputStyles}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={toggleRePassword} edge="end">
{showRePassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
initialValues kesinlike ama kesinlike tablo elemanları ile aynı olmalı. Bunları yazdıktan sonra validationSchema kısmına yup ile oluşturduğumuz şemayı import ediyoruz.
onSubmit ile asenkron işlem yapacağız. Bu yüzden values değerlerini alıyoruz. Bu değerler yup ile hazırlanan değerlerdir. Daha sonra bu verileri ikiye parçalıyoruz : rePassword ve userData.
Bunu yapma sebebimiz tabloda rePassword değeri yok bu yüzden bunu values değerlerinden ayırıyoruz geriye kalan değerleri ise userData olarak güncelliyoruz.
Try-catch ile önceden hazırladığımız mail fonksiyonu ile alınan values içindeki mail değerinin hali hazırda kayıtlı olup olmadığına bakıyoruz. Eğer kayıtlı ise error gönderiyoruz eğer mail kayıtlı değil ise dispatch ile hazırladığımız registerUser fonksiyonunu userData verileriyle reduxa yolluyoruz. Redux ise bu aldığı değerleri supabase tablosuna gönderecek. Duruma göre ise error veya kayıt başarılı dönütü dönecek ve kullanıcı login sayfasına yönlendireceğiz.
values,handleChange,handleBlur,handleSubmit,errors,touched değerlerini formik ile alıyoruz. Bunları inputta kullanacağız. Hepsinde aynı şekil kullanıldığı için madde madde olarak bakalım.
label = input görünen adı
name = input adı
variant = mui input border tasarımı
fullWidth = mui uzunluk boyutu
value = yup ile hazırlanan değişken değerleri
onChange = input anlık olarak güncelleme (böylece en son çıktıyı yollayacağız)
onBlur = o an seçili ve aktif olan elemanın terkedilmesi ve seçilmiş özelliğini kaybetmesi olayı
error = yup ile hazırlanan errorlar
helperText = yup ile hazırlanan yardımcı metinler
sx = hazırlanan input tasarımı
InputProps ile input'a ek özellikler ekleyebiliyoruz. Biz ikon ekleteceğiz ama InputProps içine ise input'un hangi kısmına ekleneceğini belirtmemiz lazım. Start, inputun sol tarafı iken end ise sağ tarafıdır. Biz göz ikonlarını sağ tarafa ekleyeceğiz.
IconButton ile ikonu buton haline çevirip oluşturduğumuz fonksiyonu ekliyoruz. Böylece eğer state true ise şifre gözükecek false ise gözükmeyecek.
Validation işlemlerinin sonucu
Şifre gösterme özelliği
Kayıt başarılı durumu
Mail zaten kayıtlı ise gelen error
Kayıt işlemi sonrası user tablosu
Bu şekilde kayıt sayfasını bitirdik. Şimdi giriş sayfasına geçelim.
Kullanıcı Giriş Sayfası
JavaScript:
import React, { useState } from "react";
import {
Box,
TextField,
Button,
Typography,
InputAdornment,
IconButton,
} from "@mui/material";
import { useNavigate } from "react-router-dom";
import { Visibility, VisibilityOff } from "@mui/icons-material";
import { Formik } from "formik";
import { loginSchema } from "../../validation/Yup";
import { toast } from "react-toastify";
import { loginUser } from "../../api/UserSlicer";
import { useDispatch } from "react-redux";
function Login() {
const navigate = useNavigate();
const dispatch = useDispatch();
const [showPassword, setShowPassword] = useState(false);
const togglePassword = () => setShowPassword(!showPassword);
const inputStyles = {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "#5bd85b",
borderRadius: "30px",
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "#3dc23d",
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: "#1f9e1f",
},
"& .MuiInputBase-input": {
caretColor: "#0a630a",
color: "#0a630a",
},
"& .MuiInputLabel-root": {
color: "black",
},
"& .MuiInputLabel-root.Mui-focused": {
color: "black",
},
};
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
backgroundColor: "#e8f5e9",
}}
>
<Formik
initialValues={{ email: "", password: "" }}
validationSchema={loginSchema}
onSubmit={async (values) => {
try {
const result = await dispatch(loginUser(values)).unwrap();
if (result && result.token) {
localStorage.setItem("token", result?.token);
toast.success("Login Successful!");
}
navigate("/");
} catch (error) {
toast.error(error);
console.log(error);
}
}}
>
{({
values,
handleChange,
handleBlur,
handleSubmit,
errors,
touched,
}) => (
<Box
component="form"
onSubmit={handleSubmit}
sx={{
width: 400,
p: 4,
borderRadius: 4,
backgroundColor: "#fff",
boxShadow: "0 4px 20px rgba(0,0,0,0.1)",
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<Typography
variant="h4"
textAlign="center"
fontWeight="bold"
color="#1b5e20"
>
Login
</Typography>
<TextField
label="E-mail"
name="email"
type="email"
variant="outlined"
fullWidth
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
error={touched.email && Boolean(errors.email)}
helperText={touched.email && errors.email}
sx={inputStyles}
/>
<TextField
label="Password"
name="password"
type={showPassword ? "text" : "password"}
variant="outlined"
fullWidth
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
error={touched.password && Boolean(errors.password)}
helperText={touched.password && errors.password}
sx={inputStyles}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={togglePassword} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
<Typography
sx={{
color: "green",
fontSize: "0.9rem",
cursor: "pointer",
position: "relative",
"&::after": {
content: '""',
position: "absolute",
left: 0,
bottom: -2,
width: 0,
height: "1px",
backgroundColor: "green",
transition: "width 0.3s ease",
},
"&:hover::after": {
width: "43%",
},
}}
onClick={() => navigate("/auth/register")}
>
You don't have an account
</Typography>
<Button
variant="contained"
fullWidth
type="submit"
sx={{
mt: 1,
py: 1.2,
backgroundColor: "#2e7d32",
borderRadius: "30px",
fontSize: "1rem",
fontWeight: "bold",
textTransform: "none",
"&:hover": {
backgroundColor: "#43a047",
},
}}
>
Login
</Button>
</Box>
)}
</Formik>
</Box >
);
}
export default Login;
Burada tek tek açıklama yapmayacağım çünkü giriş sayfasındaki aynı işlemleri yapıyoruz. Tek fark sadece email ve password değerleri var ve checkMail adlı bir fonksiyonumuz yok.
Veri tabanında olmayan mail girilince
Yanlış şifre girilince
Giriş yapıldıktan sonra localStorage içindeki değerler
Giriş sayfasını da yaptık. Şimdi Navbar componentini güncelleyeceğiz.
Navbar Güncelleme
Hatırlarsanız zaten hali hazırda navbarı yapmıştık. Şimdi bu navbarı güncelleyeceğiz çünkü kullanıcı giriş yaptıktan sonra ona özel menüler çıkacak. Navbarın ilk halinde bu yoktu çünkü giriş sayfasını daha yapmamıştık. Artık giriş sayfamız var bu yüzden bu componenti güncelleyelim.
JavaScript:
import React, { useEffect, useState } from "react";
import {
AppBar,
Box,
Toolbar,
IconButton,
Typography,
Container,
Avatar,
Button,
Tooltip,
Menu,
MenuItem,
Drawer,
List,
ListItem,
ListItemButton,
ListItemText,
} from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import { styled } from "@mui/system";
import Logo from "../assets/images/logo.png";
import useAuth from "../hooks/UseAuth";
import { useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { getUser } from "../api/UserSlicer";
const pages = [
{
id: 1,
navName: "Home",
navUrl: "/"
},
{
id: 2,
navName: "About Us",
navUrl: "/about-us"
},
{
id: 3,
navName: "Team",
navUrl: "/team"
},
{
id: 4,
navName: "Hotels",
navUrl: "/hotels"
},
]
const settings = ["Profile", "Logout"];
const adminSettings = ["Profile", "Add New Hotel", "Logout"];
const AuthButton = styled(Button)(({ theme }) => ({
color: "#fff",
background: "green",
fontWeight: 600,
textTransform: "none",
padding: "7px 25px",
borderRadius: "12px",
boxShadow: "0 4px 15px rgba(0,0,0,0.2)",
transition: "all 0.3s ease",
"&:hover": {
transform: "scale(1.05)",
background: "darkGreen",
boxShadow: "0 6px 20px rgba(0,0,0,0.3)",
},
}));
function Navbar() {
const Auth = useAuth();
const navigate = useNavigate();
const [anchorElUser, setAnchorElUser] = useState(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const handleOpenUserMenu = (event) => setAnchorElUser(event.currentTarget);
const handleCloseUserMenu = () => setAnchorElUser(null);
const toggleDrawer = (open) => () => setDrawerOpen(open);
const dispatch = useDispatch();
const logout = () => {
localStorage.clear();
handleCloseUserMenu();
navigate("/auth/login");
}
const user = useSelector(state => state.user.user);
useEffect(() => {
const email = localStorage.getItem("email");
if (email) {
dispatch(getUser());
}
}, [dispatch]);
const userId = user?.id;
const userRole = user?.Role;
return (
<AppBar
position="sticky"
sx={{
top: 0,
left: 0,
right: 0,
borderRadius: 0,
backgroundColor: "rgba(255, 255, 255, 0.95)",
backdropFilter: "blur(10px)",
boxShadow: "0 4px 20px rgba(0,0,0,0.1)",
maxHeight: 130,
}}
>
<Container maxWidth="xl">
<Toolbar sx={{ justifyContent: "space-between", alignItems: "center" }}>
<Box component="a" href="/" sx={{ display: "flex", alignItems: "center", height: "70px" }}>
<img
src={Logo}
alt="Logo"
style={{
maxHeight: 100,
objectFit: "contain",
transition: "all 0.3s ease",
}}
/>
</Box>
<Box
sx={{
flex: 1,
display: { xs: "none", md: "flex" },
justifyContent: "center",
gap: 4,
}}
>
{pages.map((page) => (
<Button
onClick={() => { navigate(page.navUrl) }}
key={page.id}
sx={{
color: "#333",
fontWeight: 600,
textTransform: "none",
fontSize: "16px",
fontFamily: "'Poppins', sans-serif",
position: "relative",
"&:after": {
content: '""',
position: "absolute",
width: "0%",
height: "2px",
bottom: -2,
left: 0,
bgcolor: "green",
transition: "0.3s",
},
"&:hover:after": {
width: "100%",
},
"&:hover": {
color: "darkGreen",
transform: "scale(1.05)",
transition: "0.3s",
},
}}
>
{page.navName}
</Button>
))}
</Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{Auth ? (
<Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar
sx={{
bgcolor: "#4caf50",
fontSize: 34,
border: "3px solid #c8e6c9",
}}
/> </IconButton>
</Tooltip>
) : (
<AuthButton onClick={() => navigate("/auth/login")}>Login</AuthButton>
)}
<Box sx={{ display: { xs: "flex", md: "none" } }}>
<IconButton onClick={toggleDrawer(true)}>
<MenuIcon />
</IconButton>
<Drawer anchor="right" open={drawerOpen} onClose={toggleDrawer(false)}>
<Box sx={{ width: 250 }} role="presentation" onClick={toggleDrawer(false)}>
<List>
{pages.map((item) => (
<ListItem key={item.id} disablePadding>
<ListItemButton onClick={() => navigate(item.navUrl)}>
<ListItemText primary={item.navName} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
</Drawer>
</Box>
<Menu
anchorEl={anchorElUser}
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
transformOrigin={{ vertical: "top", horizontal: "right" }}
>
{userRole == "Admin" ? (
adminSettings.map((setting) => (
<MenuItem
key={setting}
onClick={() => {
if (setting === "Logout") {
logout();
} else if (setting === "Profile") {
navigate(`/user-profile/profile/${userId}`);
}
else if (setting === "Add New Hotel") {
navigate("/admin/add-new-hotel")
}
handleCloseUserMenu();
}
}
>
<Typography textAlign="center">{setting}</Typography>
</MenuItem>
))
) : (
settings.map((setting) => (
<MenuItem
key={setting}
onClick={() => {
if (setting === "Logout") {
logout();
} else {
handleCloseUserMenu();
navigate(`/user-profile/profile/${userId}`);
}
}}
>
<Typography textAlign="center">{setting}</Typography>
</MenuItem>
))
)}
</Menu>
</Box>
</Toolbar>
</Container>
</AppBar >
);
}
export default Navbar;
Burada sadece bir kısmı güncelleyeceğiz onun detayına bakalım.
JavaScript:
const dispatch = useDispatch();
const user = useSelector(state => state.user.user);
useEffect(() => {
const email = localStorage.getItem("email");
if (email) {
dispatch(getUser());
}
}, [dispatch]);
const userId = user?.id;
const userRole = user?.Role;
Burada React-Redux içinde bulunan useDispatch hookunu aldık, user değerini hali hazırda state içinde bulunan user değerlerini aldık. Redux ile oluşturulan store içinde hali hazırda bulunan veriyi almak için useSelector kullanılır.
useEffect ile sayfa her render edildiğinde localstorage içine kayıt edilen email değerini kontrol eder. Eğer email geçerli ise kullanıcı bilgilerini çeker. userId ve userRole değerlerini ise direkt useSelector ile aldığımız user değişkeni içinden ulaşabiliriz.
Console.log yazınca gördüğünüz üzere kullanıcı verilerini çekmiş oluyoruz.
Konunun son kısmı olan kullanıcı profil sayfası bölümüne geçelim şimdi.
Kullanıcı Profil Sayfası
Bu sayfada çektiğimiz değerleri göstereceğiz, kullanıcıyı önce rezervasyon yaptığı url yönlendirme ve hesap silme işlemlerini buradan yapacağız.
JavaScript:
import { useDispatch, useSelector } from "react-redux";
import {
Avatar,
Box,
Typography,
Paper,
Stack,
Button,
Container
} from "@mui/material";
import EmailIcon from '@mui/icons-material/Email';
import CakeIcon from '@mui/icons-material/Cake';
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
import DeleteIcon from '@mui/icons-material/Delete';
import HistoryIcon from '@mui/icons-material/History';
import Loading from "../../components/Loading"
import { useNavigate } from "react-router-dom";
import { deleteUser } from "../../api/UserSlicer";
import { toast } from "react-toastify";
function UserProfilePage() {
const user = useSelector((state) => state.user.user);
const dispatch = useDispatch();
const navigate = useNavigate();
const userId = user?.id;
const deleteAccount = async () => {
try {
const respond = await dispatch(deleteUser());
if (respond) {
toast.success("Account Has Been Deleted");
localStorage.clear();
navigate("/auth/login")
}
} catch (error) {
toast.error(error)
}
}
if (!user) return (<Loading />);
return (
<Box sx={{ py: 6, backgroundColor: "#f5f5f5", minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }}>
<Container maxWidth="sm">
<Paper
elevation={3}
sx={{
p: 4,
borderRadius: 3,
backgroundColor: "#fff",
}}
>
<Stack alignItems="center" spacing={1.5} sx={{ mb: 4 }}>
<Avatar
sx={{
bgcolor: "#4caf50",
width: 80,
height: 80,
fontSize: 34,
border: "3px solid #c8e6c9",
}}
/>
<Typography variant="h5" sx={{ fontWeight: 600 }}>
{user?.first_name} {user?.last_name}
</Typography>
<Typography
variant="caption"
sx={{
backgroundColor: "#e8f5e9",
px: 2,
py: 0.4,
borderRadius: 10,
color: "#388e3c",
fontSize: "0.78rem",
marginBottom: "5px !important "
}}
>
User Profile
</Typography>
</Stack>
<Stack spacing={2} sx={{ marginBottom: "10px !important" }}>
<InfoRow icon={<EmailIcon />} label="E-mail" value={user?.email} />
<InfoRow icon={<CakeIcon />} label="Age" value={user?.age} />
<InfoRow
icon={<CalendarTodayIcon />}
label="Created"
value={new Date(user?.created_at).toLocaleDateString("tr-TR")}
/>
</Stack>
<Stack spacing={2} marginTop={4}>
<Button
variant="contained"
startIcon={<HistoryIcon />}
sx={{
backgroundColor: "#0ca125ff",
py: 1,
borderRadius: 2,
marginBottom: "10px !important",
"&:hover": { backgroundColor: "#086d19ff" },
}}
onClick={() => navigate(`/user-profile/history/${userId}`)}
>
Check History
</Button>
<Button
variant="contained"
startIcon={<DeleteIcon />}
sx={{
backgroundColor: "#e57373",
py: 1,
borderRadius: 2,
"&:hover": { backgroundColor: "#ef5350" },
}}
onClick={() => { deleteAccount() }}
>
Delete Account
</Button>
</Stack>
</Paper>
</Container>
</Box>
);
}
const InfoRow = ({ icon, label, value }) => (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
p: 2,
borderRadius: 2,
backgroundColor: "#f9fbe7",
"&:hover": { backgroundColor: "#f0f4c3" },
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ mr: 2, color: "#689f38" }}>{icon}</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">
{label}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{value}
</Typography>
</Box>
</Box>
</Box>
);
export default UserProfilePage;
Şimdi detaylara inelim.
JavaScript:
const user = useSelector((state) => state.user.user);
const dispatch = useDispatch();
const navigate = useNavigate();
const userId = user?.id;
const deleteAccount = async () => {
try {
const respond = await dispatch(deleteUser());
if (respond) {
toast.success("Account Has Been Deleted");
localStorage.clear();
navigate("/auth/login")
}
} catch (error) {
toast.error(error)
}
}
if (!user) return (<Loading />);
Burada useSelector ile state içindeki verileri alıyoruz dispatch ve navigate hooks import ediyoruz.
Kullanıcının id değerini userId değişkenine atıyoruz. Bu değişken yönlendirme işleminde lazım olacak.
deleteAccount asenkron fonksiyonu ile UserSlicer içinde yazdığımız deleteUser fonksiyonun çağırıp dispatch ile UserSlicer içindeki deleteUser'a gidiyor. deleteUser ise veri tabanına bağlanarak hesabı siliyor.
Hesap silme işlemi başarılı ise "hesap başarıyla silindi" geri bildirimini verip localstorage içindeki tüm değerleri silip kullanıcıyı login ekranına atıyor.
JavaScript:
const InfoRow = ({ icon, label, value }) => (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
p: 2,
borderRadius: 2,
backgroundColor: "#f9fbe7",
"&:hover": { backgroundColor: "#f0f4c3" },
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ mr: 2, color: "#689f38" }}>{icon}</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">
{label}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{value}
</Typography>
</Box>
</Box>
</Box>
);
Burada ise kendi tasarımımızı yapıyoruz.
JavaScript:
<Stack spacing={2} marginTop={4}>
<Button
variant="contained"
startIcon={<HistoryIcon />}
sx={{
backgroundColor: "#0ca125ff",
py: 1,
borderRadius: 2,
marginBottom: "10px !important",
"&:hover": { backgroundColor: "#086d19ff" },
}}
onClick={() => navigate(`/user-profile/history/${userId}`)}
>
Check History
</Button>
<Button
variant="contained"
startIcon={<DeleteIcon />}
sx={{
backgroundColor: "#e57373",
py: 1,
borderRadius: 2,
"&:hover": { backgroundColor: "#ef5350" },
}}
onClick={() => { deleteAccount() }}
>
Delete Account
</Button>
</Stack>
Check History butonu önceden rezervasyon yapılan sayfaya yönlendirecek butondur. Navigate urlsine bakarsanı /user-profile/history/userId değerini göreceksiniz. Bu yüzden kullanıcının id değerini aldık. Router işlemlerinde bu sayfa dinamik idi.
En sonda ise hesap silme butonu var. Oluşturduğumuz fonksiyonu burada kullanarak hesap silme işlemlerini yapıyoruz.
Kullanıcı profil sayfası
Hesap silme işleminden sonra gelen dönüt
Mevcut hesap tablodan da silindi
Bir konunun daha sonuna geldik. Yoğun bir konu oldu diyebilirim, gelecek konuda ise hotel işlemlerine geçeceğiz. Okuduğunuz için teşekkür ederim


