Initial import

This commit is contained in:
Flatlogic Bot 2026-03-03 12:36:05 +00:00
commit 3377c928dd
89 changed files with 7033 additions and 0 deletions

2
Tripzy-main/backend/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
.env

View File

@ -0,0 +1,44 @@
const express = require("express");
const morgan = require("morgan");
const cors = require("cors");
const errorController = require("./controllers/error-controller");
const tripRouter = require("./routes/trip-route");
const userRouter = require("./routes/user-route");
const coinsRouter = require("./routes/coins-route");
const enrolledTripsRouter = require("./routes/enrolled-trips-route");
const activityRouter = require("./routes/activity-route");
const itineraryRouter = require("./routes/itinerary-route");
const suggessionRouter = require("./routes/suggession-route");
const AppError = require("./utils/app-error");
const app = express();
app.use(cors());
app.options("*", cors());
app.use(express.json());
app.use(express.json({ limit: "10kb" }));
app.use(express.urlencoded({ extended: true, limit: "10kb" }));
if (process.env.NODE_ENV === "development") app.use(morgan("dev"));
// routes
app.use("/api/auth", userRouter);
app.use("/api/coins", coinsRouter);
app.use("/api/trips", tripRouter);
app.use("/api/activities", activityRouter);
app.use("/api/enrolledTrips", enrolledTripsRouter);
app.use("/api/itineraries", itineraryRouter);
app.use("/api/suggestions", suggessionRouter);
app.all("*", (req, res, next) => {
next(new AppError(`Can't find ${req.originalUrl} on this server`, 404));
});
// global error handler
app.use(errorController);
module.exports = app;

View File

@ -0,0 +1,9 @@
var admin = require("firebase-admin");
var serviceAccount = require("./serviceAccount.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
module.exports = admin;

View File

@ -0,0 +1,12 @@
{
"type": "service_account",
"project_id": "tripzy-code-crafters",
"private_key_id": "f67c4b5caa381c0a1d1c0d8f120e31af10824a4e",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDQ39gy4DuEj9Nz\nxMJu0lmu65MuayEz5HntB2AszLiUJu9bhdGeRuBisaoBACNZiiumWF69bC5NwIlQ\n4XXNaCk4D8MRl2QxDSaZDfukDoWY9ptK9IFmIRUT1We1mqlM9PTm4FIn+A0vgkHn\nG+UK0QFMO6Kuh8QTz3Y9UU6g3jFWQ8vCz06K/gjEdB41RjwCTXUU7GZoQoKPyOEO\nB1tSmHY6oMzeFJbOpEZz51eB/2hnrIhISUWQd60vSank6MuieiqvZgOc5TE5+Tdv\nMwx2Rm8mF9J3nNcVsbdRAUZtNvG/Hf6uMM300V+eZDgb7Tomh6sOsH7wIx1Q1vL5\ntKLdab2PAgMBAAECgf92lqBaQkPXML2ry54iTV8ANecVadErJTRjBjlPhOvkbIVl\neu0vqtLiIje7z9hCG4kPQTCwR3Z4wjBH2PT/UXyzErYGZFoRziCvndRybhsJUUjG\nO9CeSQK4XY7JIF7rITwRsYWRGBMB8BAEf2WU3BU5KRjHw3StLSZ0gnA/byKAG6gX\nVZ2J6wk1YmZWYpz3ajTe/zdMxD+SL1G2hOVI2F6sWNDTHbqVIqCiKf6CWNJnxNE6\nZMZzCEyeih+YgVq4GyP2N3tMfrR5T8mFS76+meIc+1NSolEkXCp8ZDosFzbVg1kp\nt3nZxrMtw13oP7WLM1AWQ/PyWtYFYZZmnQsU22UCgYEA8zR6261+GrygG29AEdRC\nSKaZDQ/gNaAWq/2T5iLIQByYaqnRPrEHMZau0cIbBqMnxeOOagasJvrsNdUgTEP7\nU0/CVfTD9iBXExHgvslZW4DBK1SY2PufwTqg6g9uhI1flxMYL/EEwpcbWD3zhSS/\nMkK6Q+p7g1/mOVjWeMjdNG0CgYEA29z+up//fk2KUbWrnKfLu4rCBdZqjEp0JR8Y\n5chI3EIHYw2zs/ayl60m40StRA7xkY5tN/Hqc4LkTudROP7YPO+kHeETM5FfkLWl\ns2v17d/Vx1H3AsOW8JnDkTqbPSlq5TrJc8KX0gPTvBHdqOhV5oINVA1PPbDhKAbO\nNS97pGsCgYEAjbbK0C7sCFBZSyMsRjdU2FibXk0d7KF4FIgSIkuqPBFtjtmdH9av\nxmlzPK7KaLexeVH7rjRtI9mawlOKGmaSkB0ttECH32dA1c/ZEdLpyrPf24vT9LvK\nfyHWmgyb7YkjZjiuI2Fh0LGUMXsH51FeR78yIlkD162NzWTCtGb23pECgYARQqs4\nyYDMUJgQTBvZ445p/b23qZqZwuqVU3in6W5W5FQiIZw+/5oLsEtCQkz779RlIfJP\nFw3Z3afAzgYhXFhriECxG89fGAWRncERceNPtmfZCwVCUUqTPu8MgrZXOd4res7/\n6IH0udowhJKLRRohS4pyU80pwa4bb1VW9ZBWWwKBgFucjmyzxWavHTK/QZ3ELkFF\nOsbPfnoBeroxlZSt5O9YnxWK/Myds4IYU0qnJNd/cqJ2MUFape6of27SWLTIBh9s\nhgS+10c1A7IyiXaufB6I5IpE9JEFwlhBILNlZUW57h0f5aYgj4yhRzsxjizHKYpH\naDk3gg7X2AfdSmSG6rSG\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-qk5pq@tripzy-code-crafters.iam.gserviceaccount.com",
"client_id": "115053398061482683758",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-qk5pq%40tripzy-code-crafters.iam.gserviceaccount.com"
}

View File

@ -0,0 +1,99 @@
const Itinerary = require("../models/itinerary-model");
const catchAsync = require("../utils/catch-async");
const AppError = require("../utils/app-error");
const APIFeatures = require("../utils/api-features");
exports.createActivity = catchAsync(async (req, res, next) => {
const { itineraryId } = req.params;
const activity = await Itinerary.findOneAndUpdate(
{ _id: itineraryId },
{ $push: { activities: req.body } }
);
if (!activity) {
return next(new AppError("No itinerary found with this Id.", 404));
}
res.status(200).json({
status: "success",
data: activity,
});
});
exports.getAllActivities = catchAsync(async (req, res, next) => {
const { itineraryId } = req.params;
const features = new APIFeatures(
Itinerary.find({ _id: itineraryId }),
req.query
);
const activities = await features.query;
if (!activities) {
return next(new AppError("No itinerary found with this Id.", 404));
}
res.status(200).json({
status: "success",
results: activities.length,
data: activities,
});
});
exports.getActivityById = catchAsync(async (req, res, next) => {
const { itineraryId, activityId } = req.params;
const activity = await Itinerary.findOne({
_id: itineraryId,
"activities._id": activityId,
});
if (!activity) {
return next(new AppError("No activity found with this Id.", 404));
}
res.status(200).json({
status: "success",
data: activity,
});
});
exports.updateActivityById = catchAsync(async (req, res, next) => {
const { itineraryId, activityId } = req.params;
const activity = await Itinerary.findOneAndUpdate(
{
_id: itineraryId,
"activities._id": activityId,
},
req.body,
{
new: true,
runValidators: true,
}
);
if (!activity) {
return next(new AppError("No activity found with this Id.", 404));
}
res.status(200).json({
status: "success",
data: activity,
});
});
exports.deleteActivityById = catchAsync(async (req, res, next) => {
const { itineraryId, activityId } = req.params;
const activity = await Itinerary.findOneAndDelete({
_id: itineraryId,
"activities._id": activityId,
});
if (!activity) {
return next(new AppError("No activity found with this Id.", 404));
}
res.status(204).json({
status: "success",
});
});

View File

@ -0,0 +1,41 @@
const User = require("../models/user-model");
const catchAsync = require("../utils/catch-async");
const AppError = require("../utils/app-error");
const { addCoins } = require("../utils/coins");
exports.addCoins = catchAsync(async (req, res, next) => {
try {
const { delta } = req.body;
const { user } = req;
console.log(user.coins);
const result = await addCoins(user, delta);
if (!result.error) {
res.status(200).json({
status: "success",
data: {
coins: user.coins,
},
});
} else {
return next(new AppError(`Can't add coins`, 400));
}
} catch (error) {
console.log(error);
}
});
exports.getCoins = catchAsync(async (req, res, next) => {
try {
const { user } = req;
res.status(200).json({
status: "success",
data: {
coins: user.coins,
},
});
} catch (error) {
console.log(error);
return next(new AppError(`Can't get coins`, 400));
}
});

View File

@ -0,0 +1,53 @@
const EnrolledTripsModel = require("../models/enrolled-trips-model");
const AppError = require("../utils/app-error");
const catchAsync = require("../utils/catch-async");
const { addCoins } = require("../utils/coins");
exports.getAllEnrolledTrips = catchAsync(async (req, res, next) => {
const enrolled_trips = await EnrolledTripsModel.find({
userId: req.user.id,
});
res.status(200).json({
status: "success",
data: enrolled_trips,
});
});
exports.storeEnrolledTrip = catchAsync(async (req, res, next) => {
const { tripId } = req.body;
const check = await EnrolledTripsModel.findOne({
userId: req.user._id,
tripId: tripId,
});
if (check) {
return next(new AppError("You are already enrolled in this trip", 400));
}
const enrolledTrip = await EnrolledTripsModel.create({
userId: req.user._id,
tripId: tripId,
});
addCoins(req.user, 5);
res.status(200).json({
status: "success",
data: enrolledTrip,
});
});
exports.getEnrolledUsers = catchAsync(async (req, res, next) => {
const enrolled_users = await EnrolledTripsModel.find({
tripId: req.params.tripId,
})
.populate("userId")
.populate("tripId");
res.status(200).json({
status: "success",
data: {
enrolled_users,
},
});
});

View File

@ -0,0 +1,85 @@
const AppError = require("../utils/app-error");
const handleCastErrorDB = (error) => {
const message = `Invalid value ${error.value} for attribute ${error.path}`;
return new AppError(message, 400);
};
const handleDuplicateFieldsDB = (error) => {
const value = error.keyValue;
const message = `Duplicate field value(s) for ${JSON.stringify(
value
)}.Please use another value`;
return new AppError(message, 400);
};
const handleValidationErrorDB = (error) => {
const errors = Object.values(error.errors).map(
(ele, index) => `${index + 1}) ${ele.message}`
);
const message = `Invalid input data for: ${errors.join(". ")}`;
return new AppError(message, 400);
};
const handleJWTError = (error) =>
new AppError(`Invalid token! Please login in again`, 401);
const handleTokenExpiredError = (error) =>
new AppError(`Your token has been expired! please log in again`, 401);
const sendErrorDevelopment = (error, req, res) => {
return res.status(error.statusCode).json({
status: error.status,
message: error.message,
stack: error.stack,
error: error,
});
};
const sendErrorProduction = (error, req, res) => {
if (error.isOperational) {
return res.status(error.statusCode).json({
status: error.status,
message: error.message,
});
}
console.error(`ERROR 💣 ${error}`);
return res.status(500).json({
status: "error",
message: "Something went very wrong :(",
});
};
module.exports = (error, req, res, next) => {
error.statusCode = error.statusCode || 500;
error.status = error.status || "error";
if (process.env.NODE_ENV === "development") {
sendErrorDevelopment(error, req, res);
} else if (process.env.NODE_ENV === "production") {
let err = { ...error };
err.name = error.name;
err.message = error.message;
if (err.code === 11000) err = handleDuplicateFieldsDB(err);
switch (err.name) {
case "CastError":
err = handleCastErrorDB(err);
break;
case "TokenExpiredError":
err = handleTokenExpiredError(err);
break;
case "ValidationError":
err = handleValidationErrorDB(err);
break;
case "JsonWebTokenError":
err = handleJWTError(err);
break;
}
sendErrorProduction(err, req, res);
}
next();
};

View File

@ -0,0 +1,89 @@
const Itinerary = require("../models/itinerary-model");
const Trip = require("../models/trip-model");
const AppError = require("../utils/app-error");
const catchAsync = require("../utils/catch-async");
exports.createItinerary = catchAsync(async (req, res, next) => {
const { tripId } = req.params;
const itinerary = await Itinerary.create(req.body);
const trip = await Trip.findOneAndUpdate(
{ _id: tripId },
{ itinerary: itinerary._id },
{ new: true, runValidators: true }
);
if (!trip || !itinerary) {
return next(new AppError("No trip OR itinerary found with this Id.", 404));
}
res.status(200).json({
status: "success",
data: {
itinerary,
trip,
},
});
});
exports.getItinerary = catchAsync(async (req, res, next) => {
const { tripId } = req.params;
const trip = await Trip.findOne({ _id: tripId });
if (!trip) {
return next(new AppError("No trip found with this Id.", 404));
}
if (!trip.itinerary) {
return next(new AppError("No itinerary found with this Id.", 404));
}
const itinerary = await Itinerary.findOne({ _id: trip.itinerary });
res.status(200).json({
status: "success",
data: itinerary,
});
});
exports.updateItinerary = catchAsync(async (req, res, next) => {
const { tripId } = req.params;
const trip = await Trip.findOne({ _id: tripId });
if (!trip) {
return next(new AppError("No trip found with this Id.", 404));
}
if (!trip.itinerary) {
return next(new AppError("No itinerary found with this Id.", 404));
}
const itinerary = await Itinerary.findOneAndUpdate(
{ _id: trip.itinerary },
req.body,
{ new: true, runValidators: true }
);
res.status(200).json({
status: "success",
data: itinerary,
});
});
exports.deleteItinerary = catchAsync(async (req, res, next) => {
const { tripId } = req.params;
const trip = await Trip.findOne({ _id: tripId });
if (!trip) {
return next(new AppError("No trip found with this Id.", 404));
}
if (!trip.itinerary) {
return next(new AppError("No itinerary found with this Id.", 404));
}
await Itinerary.findOneAndDelete({ _id: trip.itinerary });
res.status(204).json({
status: "success",
});
});

View File

@ -0,0 +1,32 @@
const Suggession = require("../models/suggession-model");
const catchAsync = require("../utils/catch-async");
const AppError = require("../utils/app-error");
exports.createSuggession = catchAsync(async (req, res, next) => {
const { tripId } = req.params;
console.log(req.body);
req.body.userId = req.user._id;
req.body.status = "pending";
const suggession = await Suggession.create(req.body);
if (!suggession) {
return next(new AppError("No suggession found with this Id.", 404));
}
res.status(201).json({
status: "success",
data: suggession,
});
});
exports.getAllSuggessions = catchAsync(async (req, res, next) => {
const { tripId } = req.params;
const suggessions = await Suggession.find({ trip: tripId });
res.status(200).json({
status: "success",
results: suggessions.length,
data: suggessions,
});
});

View File

@ -0,0 +1,129 @@
const Trip = require("../models/trip-model");
const EnrolledTripsModel = require("../models/enrolled-trips-model");
const catchAsync = require("../utils/catch-async");
const AppError = require("../utils/app-error");
const APIFeatures = require("../utils/api-features");
const { addCoins } = require("../utils/coins");
const schedule = require("node-schedule");
const User = require("../models/user-model");
exports.createTrip = catchAsync(async (req, res, next) => {
console.log(req.body);
try {
req.body.createdBy = req.user._id;
const trip = await Trip.create(req.body);
addCoins(req.user, 50);
const endDate = new Date(trip.enddate);
const job = schedule.scheduleJob(endDate, function () {
EnrolledTripsModel.find({ tripId: trip._id })
.populate("UserId")
.then((enrolledUsers) => {
enrolledUsers.forEach(async (obj) => {
let user = obj.userId;
user = await User.findById(user._id);
console.log("Updating coins for user: " + user.email);
console.log("Previoud coins: " + user.coins);
addCoins(user, 10);
});
});
});
res.status(201).json({
status: "success",
data: trip,
});
} catch (error) {
console.log(error);
return next(new AppError(`Can't create trip`, 400));
}
});
exports.getTripsByName = catchAsync(async (req, res, next) => {
const { text } = req.query;
const regexTitle = new RegExp(text, "i");
console.log(regexTitle);
const features = new APIFeatures(Trip.find({ title: regexTitle }), req.query);
const trips = await features.query;
res.status(200).json({
status: "success",
results: trips.length,
data: trips,
});
});
exports.getTripById = catchAsync(async (req, res, next) => {
const { id } = req.params;
const trip = await Trip.findById(id);
if (!trip) {
return next(new AppError("No trip found with this Id.", 404));
}
res.status(200).json({
status: "success",
data: trip,
});
});
exports.getAllTrips = catchAsync(async (req, res, next) => {
// const features = new APIFeatures(Trip.find(), req.query)
// .filter()
// .sort()
// .fieldLimit()
// .pagination();
// const trips = await features.query;
const trips = await Trip.find().populate("createdBy");
res.status(200).json({
status: "success",
results: trips.length,
data: trips,
});
});
exports.updateTrip = catchAsync(async (req, res, next) => {
const { id } = req.params;
const updatedTrip = await Trip.findByIdAndUpdate(id, req.body, {
new: true,
runValidators: true,
});
if (!updatedTrip) {
return next(new AppError("No tour found with this Id.", 404));
}
res.status(200).json({
status: "success",
data: updatedTrip,
});
});
exports.deleteTrip = catchAsync(async (req, res, next) => {
const { id } = req.params;
const trip = await Trip.findByIdAndDelete(id);
if (!trip) {
return next(new AppError("No trip found with that Id", 404));
}
res.status(204).json({
status: "success",
});
});
exports.myTrips = catchAsync(async (req, res, next) => {
const trips = await Trip.find({ createdBy: req.user._id }).populate(
"createdBy"
);
res.status(200).json({
status: "success",
results: trips.length,
data: trips,
});
});

View File

@ -0,0 +1,66 @@
const User = require("../models/user-model");
const AppError = require("../utils/app-error");
const catchAsync = require("../utils/catch-async");
const admin = require("../config/firebase-config");
const { log } = require("console");
exports.protect = catchAsync(async (req, res, next) => {
try {
if (!req.headers.authorization) {
return next(new AppError("Please sign in to continue", 401));
}
const token = req.headers.authorization.split(" ")[1];
const decodeValue = await admin.auth().verifyIdToken(token);
if (!decodeValue) {
res.status(403).json({
status: "fail",
message: "Access token invalid! Please login again.",
});
}
const user = await User.findOne({ email: decodeValue.email });
req.user = user;
return next();
} catch (error) {
console.log(error);
return next(new AppError("Internal Error", 500));
}
});
exports.createUser = catchAsync(async (req, res, next) => {
const { token } = req.body;
const decodeValue = await admin.auth().verifyIdToken(token);
console.log(decodeValue);
if (decodeValue) {
const user = await User.findOne({ email: decodeValue.email });
if (user) {
return res.status(200).json({
status: "success",
data: {
id: user._id,
},
});
}
const newUser = await User.create({
name: decodeValue.name,
email: decodeValue.email,
phoneNumber: null,
photo: decodeValue.picture,
totalRatings: 0,
ratingsCount: 0,
});
res.status(200).json({
status: "success",
data: {
id: newUser["_id"],
},
});
}
});

View File

@ -0,0 +1,22 @@
const admin = require("../config/firebase-config");
const User = require("../models/user-model");
const decodeToken = async (req, res, next) => {
const token = req.headers.authorization.split(" ")[1];
try {
const decodeValue = await admin.auth().verifyIdToken(token);
console.log(decodeValue);
if (decodeValue) {
const user = await User.findOne({ email: decodeToken.email });
console.log(user);
req.user = user;
return next();
}
return res.json({ message: "Unauthorized" });
} catch (e) {
console.log(e);
return res.json({ message: "Internal Error" });
}
};
module.exports = decodeToken;

View File

@ -0,0 +1,33 @@
const mongoose = require("mongoose");
const ActivitySchema = new mongoose.Schema({
title: {
type: String,
required: [true, "Please enter a title for activity."],
},
description: {
type: String,
required: [true, "Please enter a description for activity."],
},
startTime: {
type: Date,
required: [true, "Please enter a start time for activity."],
},
endTime: {
type: Date,
required: [true, "Please enter a end time for activity."],
},
mapLink: {
type: String,
required: [true, "Please enter a map link for activity."],
},
itineraryId: {
type: mongoose.Schema.ObjectId,
ref: "Itinerary",
required: [true, "Please provide the itineraryId to create activity."],
},
});
const Activity = mongoose.model("Activity", ActivitySchema);
module.exports = { Activity, ActivitySchema };

View File

@ -0,0 +1,20 @@
const mongoose = require("mongoose");
const EnrolledTripsSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
tripId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Trip",
required: true,
},
date: {
type: Date,
default: Date.now(),
},
});
module.exports = mongoose.model("EnrolledTrips", EnrolledTripsSchema);

View File

@ -0,0 +1,21 @@
const mongoose = require("mongoose");
const { ActivitySchema } = require("./activity-model");
const itinerarySchema = new mongoose.Schema({
activities: {
type: [ActivitySchema],
required: [true, "Please provide list of activities."],
},
startDate: {
type: Date,
required: [true, "Please provide start date for the trip!"],
},
endDate: {
type: Date,
required: [true, "Please provide end date for the trip!"],
},
});
const Itinerary = mongoose.model("Itinerary", itinerarySchema);
module.exports = Itinerary;

View File

@ -0,0 +1,26 @@
const mongoose = require("mongoose");
const SuggessionSchema = new mongoose.Schema({
tripId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Trip",
required: [true, "Please provide trip id"],
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: [true, "Please provide user id"],
},
suggession: {
type: String,
required: [true, "Please provide suggession"],
},
status: {
type: String,
enum: ["pending", "approved", "rejected"],
default: "pending",
},
});
const Suggession = mongoose.model("Suggession", SuggessionSchema);
module.exports = Suggession;

View File

@ -0,0 +1,48 @@
const mongoose = require("mongoose");
const tripSchema = new mongoose.Schema({
title: {
type: String,
trim: true,
required: [true, "Please provide title for the trip."],
maxlength: [40, "A trip must have less or equal then 40 characters"],
},
description: {
type: String,
required: [true, "A tour must have a description"],
trim: true,
},
coverImage: {
type: String,
required: [true, "A tour must have a cover image"],
},
createdBy: {
type: mongoose.Schema.ObjectId,
ref: "User",
required: [true, "Please provide the userId to create trip."],
},
startDate: {
type: Date,
required: [true, "Please provide start date for the trip!"],
},
endDate: {
type: Date,
required: [true, "Please provide end date for the trip!"],
},
mapUrl: {
type: String,
required: [true, "Please provide the trip map url."],
},
itinerary: {
type: mongoose.Schema.ObjectId,
ref: "Itinerary",
},
price: {
type: Number,
required: [true, "Please provide the trip price."],
},
});
const Trip = mongoose.model("Trip", tripSchema);
module.exports = Trip;

View File

@ -0,0 +1,56 @@
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "Please tell us your name"],
trim: true,
maxlength: [30, "Name must have less or equal then 30 characters"],
validate: {
validator: function (value) {
return /[A-Za-z]/.test(value);
},
message: "Please enter a valid name",
},
},
email: {
type: String,
required: [true, "Please tell us your email address"],
trim: true,
unique: true,
lowercase: true,
validate: {
validator: function (value) {
return /^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$/.test(value);
},
message: "Please enter a valid email address",
},
},
phoneNumber: {
type: Number,
unique: false,
required: false,
maxlength: [10, "Name must have less or equal then 10 characters"],
minlength: [10, "Name must have less or equal then 10 characters"],
},
photo: {
type: String,
default: "default.jpg",
trim: true,
},
coins: {
type: Number,
default: 1,
},
totalRatings: {
type: Number,
default: 0,
},
ratingsCount: {
type: Number,
default: 0,
},
});
const User = mongoose.model("User", userSchema);
module.exports = User;

View File

@ -0,0 +1,26 @@
{
"name": "backend",
"version": "1.0.0",
"description": "A backend for tripzy",
"main": "server.js",
"repository": "https://github.com/Nishith-Savla/Tripzy.git",
"author": "tripzy",
"scripts": {
"dev": "nodemon server.js",
"start": "NODE_ENV=production node server.js"
},
"license": "MIT",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"firebase-admin": "^11.5.0",
"imagekit": "^4.1.3",
"mongoose": "5",
"morgan": "^1.10.0",
"node-schedule": "^2.1.1"
},
"devDependencies": {
"nodemon": "^2.0.22"
}
}

View File

@ -0,0 +1,19 @@
const router = require("express").Router();
const activityController = require("../controllers/activity-controller");
const userController = require("../controllers/user-controller");
router.use(userController.protect);
router
.route("/:itineraryId")
.post(activityController.createActivity)
.get(activityController.getAllActivities);
router
.route("/:itineraryId/:activityId")
.get(activityController.getActivityById)
.patch(activityController.updateActivityById)
.delete(activityController.deleteActivityById);
module.exports = router;

View File

@ -0,0 +1,11 @@
const express = require("express");
const CoinsController = require("../controllers/coins-controller");
const userController = require("../controllers/user-controller");
const router = express.Router();
router
.route("/addCoins")
.patch(userController.protect, CoinsController.addCoins);
router.route("/getCoins").get(userController.protect, CoinsController.getCoins);
module.exports = router;

View File

@ -0,0 +1,16 @@
const express = require("express");
const EnrolledTripsController = require("../controllers/enrolled-trips-controller");
const userController = require("../controllers/user-controller");
const router = express.Router();
router
.route("/")
.get(userController.protect, EnrolledTripsController.getAllEnrolledTrips)
.post(userController.protect, EnrolledTripsController.storeEnrolledTrip);
router
.route("/getEnrolledUsers/:tripId")
.get(EnrolledTripsController.getEnrolledUsers);
module.exports = router;

View File

@ -0,0 +1,14 @@
const router = require("express").Router();
const itineraryController = require("../controllers/itinerary-controller");
const userController = require("../controllers/user-controller");
router.use(userController.protect);
router
.route("/:tripId")
.post(itineraryController.createItinerary)
.get(itineraryController.getItinerary)
.patch(itineraryController.updateItinerary)
.delete(itineraryController.deleteItinerary);
module.exports = router;

View File

@ -0,0 +1,12 @@
const router = require("express").Router();
const suggessionController = require("../controllers/suggession-controller");
const userController = require("../controllers/user-controller");
router.use(userController.protect);
router
.route("/:tourId")
.get(suggessionController.getAllSuggessions)
.post(suggessionController.createSuggession);
module.exports = router;

View File

@ -0,0 +1,20 @@
const router = require("express").Router();
const tripController = require("../controllers/trip-controller");
const userController = require("../controllers/user-controller");
router.route("/myTrips").get(userController.protect, tripController.myTrips);
router
.route("/")
.post(userController.protect, tripController.createTrip)
.get(tripController.getAllTrips);
router.route("/search").get(tripController.getTripsByName);
router
.route("/:id")
.get(tripController.getTripById)
.patch(userController.protect, tripController.updateTrip)
.delete(userController.protect, tripController.deleteTrip);
module.exports = router;

View File

@ -0,0 +1,8 @@
const express = require("express");
const userController = require("../controllers/user-controller");
const router = express.Router();
router.route("/login").post(userController.createUser);
module.exports = router;

View File

@ -0,0 +1,48 @@
const mongoose = require("mongoose");
const dotevn = require("dotenv");
process.on("uncaughtException", (error) => {
console.log(`UNCAUGHT EXCEPTION | SHUTTING DOWN ...`);
console.log(error);
process.exit(1);
});
dotevn.config({
path: "./.env",
});
const database = process.env.MONGO_DATABASE_URL.replace(
"<PASSWORD>",
process.env.MONGO_DATABASE_PASSWORD
);
mongoose
.connect(database, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true,
useFindAndModify: false,
})
.then(() => console.log(`Database connected successfully!`));
const app = require("./app");
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
console.log(`App running on port ${PORT}`);
});
process.on("unhandledRejection", (error) => {
console.log(`UNHANDLED REJECTION | SHUTTING DOWN ...`);
console.log(error.name, error.message);
server.close(() => {
process.exit(1);
});
});
process.on("SIGTERM", () => {
console.log("👋 SIGTERM RECEIVED! Shutting down server!");
server.close(() => {
console.log("Process terminated");
});
});

View File

@ -0,0 +1,54 @@
class APIFeatures {
constructor(query, queryString) {
this.query = query;
this.queryString = queryString;
}
filter() {
const queryObjects = { ...this.queryString };
const excludedFields = ["page", "sort", "fields", "limit"];
excludedFields.forEach((ele) => delete queryObjects[ele]);
let queryString = JSON.stringify(queryObjects);
queryString = queryString.replace(
/\b(gte|gt|lte|lt|eq)\b/g,
(match) => `$${match}`
);
this.query = this.query.find(JSON.parse(queryString));
return this;
}
sort() {
if (this.queryString.sort) {
const sortBy = this.queryString.sort.split(",").join(" ");
this.query = this.query.sort(sortBy);
} else {
this.query = this.query.sort("-createdAt");
}
return this;
}
fieldLimit() {
if (this.queryString.fields) {
const fields = this.queryString.fields.split(",").join(" ");
this.query = this.query.select(fields);
} else {
this.query = this.query.select("-__v");
}
return this;
}
pagination() {
const page = this.queryString.page * 1 || 1;
const limit = this.queryString.limit * 1 || 100;
// page=3&limit=10, 1-10 page 1, 11-20 page 2, 21-30 page 3
const skip = (page - 1) * limit;
this.query = this.query.skip(skip).limit(limit);
return this;
}
}
module.exports = APIFeatures;

View File

@ -0,0 +1,13 @@
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
this.status = `${this.statusCode}`.startsWith("4") ? "fail" : "error";
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;

View File

@ -0,0 +1,7 @@
const catchAsync = (func) => {
return (req, res, next) => {
func(req, res, next).catch((error) => next(error));
};
};
module.exports = catchAsync;

View File

@ -0,0 +1,17 @@
const User = require("../models/user-model");
exports.addCoins = async (user, delta) => {
try {
if (user.coins) {
user.coins += delta ? delta : user.coins;
} else {
user.coins = delta ? delta : user.coins;
}
await user.save();
return { coins: user.coins };
} catch (error) {
console.log(error);
return { error: error };
}
};

View File

@ -0,0 +1,9 @@
const ImageKit = require("imagekit");
const imagekit = new ImageKit({
publicKey: "public_fCQF5nc2un6BY3+6xE6xp/D/c2g=",
privateKey: "private_yVbKoP1UEScfxTdq68Yu/MY6bFw=",
urlEndpoint: "https://ik.imagekit.io/fy9xpsxsm",
});
module.exports = imagekit;

File diff suppressed because it is too large Load Diff

27
Tripzy-main/frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.env

View File

@ -0,0 +1,8 @@
{
"tabWidth": 2,
"useTabs": true,
"singleQuote": false,
"semi": true,
"printWidth": 100,
"endOfLine": "lf"
}

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tripzy - Trips Made Easy</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp"
crossorigin="anonymous"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N"
crossorigin="anonymous"
></script>
</body>
</html>

View File

@ -0,0 +1,29 @@
{
"name": "tripzy-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^4.28.0",
"axios": "^1.3.4",
"firebase": "^9.19.1",
"imagekitio-react": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-firebase-hooks": "^5.1.1",
"react-router-dom": "^6.10.0",
"react-toastify": "^9.1.2",
"styled-components": "^5.3.9"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"vite": "^4.2.0"
}
}

View File

@ -0,0 +1,48 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import AddActivity from "./components/AddActivity";
import Dashboard from "./components/Dashboard";
import EditTrip from "./components/EditTrip";
import ViewTripDetails from "./components/ViewTripDetails";
import Navbar from "./components/Navbar";
import Signin from "./components/SignIn";
import AuthProvider from "./context/auth";
import "./css/palette.css";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
staleTime: 1000 * 30,
},
},
});
function App() {
return (
<>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Router>
<Navbar />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/signin" element={<Signin />} />
<Route path="/trips">
<Route path="new" element={<EditTrip />} />
<Route path=":id" element={<ViewTripDetails />} />
<Route path=":id/update" element={<EditTrip />} />
<Route path=":id/add-activity" element={<AddActivity />} />
</Route>
</Routes>
</Router>
</AuthProvider>
</QueryClientProvider>
<ToastContainer />
</>
);
}
export default App;

View File

@ -0,0 +1,5 @@
import { axiosInstance } from "../utils";
export const getEnrolledUsers = async (tripId) => {
return axiosInstance.get(`/enrolledTrips/getEnrolledUsers/${tripId}`).then((res) => res.data);
};

View File

@ -0,0 +1,57 @@
import { axiosInstance } from "../utils";
export function getTrips() {
return axiosInstance.get("/trips").then((res) => res.data);
}
export function getEnrolledTrips() {
return axiosInstance.get("/enrolledTrips").then((res) => res.data);
}
export function getTrip(id) {
return axiosInstance.get(`trips/${id}`).then((res) => res.data);
}
export function enrollTrip(tripId) {
return axiosInstance.post(`/enrolledTrips`, { tripId }).then((res) => res.data);
}
export function searchTrip(tripId, text) {
return axiosInstance.get("/trips/search", { params: { tripId, text } }).then((res) => res.data);
}
export function createTrip({
title,
description,
coverImage,
startDate,
endDate,
mapUrl,
itineraryId,
createdBy,
}) {
return axiosInstance
.post("/trips", {
title,
description,
coverImage,
startDate,
endDate,
mapUrl,
itineraryId,
createdBy,
})
.then((res) => res.data);
}
export function updateTrip(id, params) {
return axiosInstance
.patch(`/trips/${id}`, {
...params,
})
.then((res) => res.data);
}
export function deleteTrip(id) {
return axois.delete(`trips/${id}`).then((res) => res.data);
}

View File

@ -0,0 +1,7 @@
import { axiosInstance } from "../utils";
export function postUser({ user, token }) {
return axiosInstance.post("auth/login", { user, token }).then((res) => {
return res.data;
});
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,240 @@
import styled from "styled-components";
export const Section = styled.section`
background-color: #f5e9cf;
display: grid;
grid-template-columns: 1fr 1fr;
height: 100%;
width: 100%;
padding: 20px;
`;
export const Background = styled.div`
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 20px;
height: calc(100vh - 130px);
width: 100%;
padding: 20px;
background-size: cover;
background-repeat: no-repeat;
border-radius: 40px;
`;
export const Title = styled.h2`
font-size: ${(props) => props.fontSize || "34px"};
font-weight: 600;
color: ${(props) => props.color || "#fff"};
text-shadow: 1px 1px #000;
`;
export const Heading = styled.div`
font-size: ${(props) => props.fontSize || "30px"};
padding: 20px 20px 10px 20px;
font-weight: 600;
margin-bottom: 0px;
`;
export const Description = styled.div`
font-size: 22px;
color: #000;
font-weight: 400;
background-color: #f5e9cf95;
padding: 10px;
`;
export const Content = styled.div`
padding: 10px 15px;
display: flex;
justify-content: flex-start;
gap: 15px;
flex-direction: column;
width: 100%;
`;
export const Itinerary = styled.section`
background-color: #f5e9cf10;
display: grid;
grid-template-columns: 1fr;
height: 100%;
width: 100%;
padding: 20px;
`;
export const Day = styled.div`
display: block;
background-color: #f5e9cf80;
`;
export const Cards = styled.div`
display: flex;
flex-direction: row;
gap: 15px;
`;
export const Card = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 50vh;
width: 30vh;
padding: 20px;
background-size: cover;
background-repeat: no-repeat;
border-radius: 5px;
`;
export const Flex = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
background-color: ${(props) => props.bgColor || "#fbffb180"};
flex-direction: ${(props) => props.flexDirection || "row"};
gap: ${(props) => props.gap || "5px"};
`;
export const Box = styled.h4`
padding: 10px;
border-radius: 5px;
//background-color: ${(props) => props.bgColor || "#4D455D"};
color: ${(props) => props.color || "#000"};
display: flex;
align-items: flex-start;
flex-direction: column;
gap: 10px;
`;
export const Button = styled.button`
padding: 5px 20px;
color: #fff;
background-color: #fc7300;
border: none;
outline: none;
font-size: 24px;
`;
export const Activities = styled.section`
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
`;
export const Activity = styled.div`
margin-left: 40px;
background-color: #f5e9cf;
padding: 10px;
`;
export const ActivityImage = styled.img`
height: 100px;
width: 100px;
border-radius: 5px;
`;
export const ActivityFlex = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
`;
export const ActivityTitle = styled.h4`
font-size: 20px;
font-weight: 600;
color: #333;
`;
export const ActivityDescription = styled.p`
font-size: 16px;
font-weight: 400;
color: #555;
`;
export const ActivityDate = styled.p`
font-size: 18px;
font-weight: 00;
background-color: #4d455d;
color: #fff;
padding: 10px;
border-radius: 5px;
`;
export const Span = styled.span`
font-size: 16px;
font-weight: 500;
color: #333;
background-color: #fff;
padding: 5px;
border-radius: 5px;
margin-left: 10px;
`;
export const MapLink = styled.a`
font-size: 20px;
font-weight: 400;
color: #5611df;
text-align: ${(props) => props.textAlign || "left"};
`;
export const Suggestion = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
padding: 20px;
`;
export const Form = styled.form`
display: flex;
flex-direction: column;
gap: 10px;
`;
export const TextArea = styled.textarea`
padding: 10px;
border: 2px solid #333;
outline: none;
border-radius: 5px;
`;
export const Container = styled.div`
background-color: #f5e9cf;
height: 35vh;
width: 100%;
overflow-y: scroll;
margin-top: 20px;
padding: 10px;
`;
export const Message = styled.div`
padding: 20px;
background-color: #4d455d;
display: flex;
flex-direction: column;
gap: 10px;
`;
export const MessageFlex = styled.div`
display: flex;
flex-direction: column;
gap: 15px;
`;
export const MessageTitle = styled.h4`
font-size: 18px;
color: #bdbcbc;
`;
export const MessageAuthor = styled.p`
font-size: 20px;
font-weight: 600;
color: #fff;
`;
export const MessageStatus = styled.p`
font-size: 16px;
background-color: gray;
color: #fff;
padding: 10px;
border-radius: 5px;
`;

View File

@ -0,0 +1,82 @@
import React from "react";
import "../css/modal.css";
import Navbar from "./Navbar";
import "../css/createTrip.css";
function AddActivity() {
return (
<>
<section id="add-activity" className="d-flex dir-col">
<h2 className="text-center trip-title">Add Activity</h2>
<div className="d-flex justify-content-center">
<form encType="multipart/form-data" className="input-container" method="post">
<div className="d-flex g-3">
<div className="d-flex dir-col stretch">
<label className="input-label" htmlFor="title">
Title
</label>
<input
type="text"
id="activity-name"
name="title"
placeholder="Title for the trip"
/>
</div>
<div className="d-flex dir-col stretch">
<label className="input-label" htmlFor="image">
Upload Image URL
</label>
<input type="text" id="image" name="coverImage" placeholder="https://" />
</div>
</div>
<div>
<label className="input-label" htmlFor="description">
Description
</label>
<textarea
name="description"
id="description"
cols="auto"
rows={3}
placeholder="Description for the trip"
/>
</div>
<div className="d-flex g-3">
<div className="d-flex dir-col stretch">
<label className="input-label" htmlFor="start-time">
Start Time
</label>
<input type="time" id="start-time" name="startDate" />
</div>
<div className="d-flex dir-col stretch">
<label className="input-label" htmlFor="end-time">
End Time
</label>
<input type="time" id="end-time" name="endDate" />
</div>
</div>
<div className="d-flex dir-col stretch mb-1in">
<label className="input-label" htmlFor="map-url">
Map URL
</label>
<input
type="text"
id="map-url"
name="mapUrl"
placeholder="Enter the google maps link"
/>
</div>
<button
type="submit"
className="btn d-flex align-items-center justify-content-center mx-auto"
>
Save
</button>
</form>
</div>
</section>
</>
);
}
export default AddActivity;

View File

@ -0,0 +1,31 @@
import React from "react";
import "../css/dashboard.css";
function Card({ title, createdBy, startDate, endDate, memberCount, coverImage }) {
return (
<div
className="tour-card"
style={{
backgroundImage: "url(" + coverImage + ")",
}}
>
<div className="card-overlay"></div>
<div
className=" d-flex dir-col justify-content-between height-100 over-1"
style={{ display: "inline-flex" }}
>
<h2 className="host-name">Created By {createdBy?.name ?? "Anonymous"} </h2>
<div className="card-details">
<p className="tour-name p-0 m-0">{title}</p>
<span className="team">{memberCount}</span>
<br />
<p className="tour-date">
{startDate} - {endDate}
</p>
</div>
</div>
</div>
);
}
export default Card;

View File

@ -0,0 +1,90 @@
import { useQuery } from "@tanstack/react-query";
import React from "react";
import { NavLink } from "react-router-dom";
import { getEnrolledTrips, getTrips } from "../api/trips";
import "../css/dashboard.css";
import { formatDate } from "../utils";
import Card from "./card";
function Dashboard() {
const allTripsQuery = useQuery({
queryKey: ["trips"],
queryFn: getTrips,
});
const enrolledTripsQuery = useQuery({
queryKey: ["trips", localStorage.getItem("access_token")],
enabled: allTripsQuery.data !== undefined,
queryFn: getEnrolledTrips,
});
if (allTripsQuery.isLoading && allTripsQuery.fetchStatus !== "idle") {
return <div>Loading...</div>;
}
const { data: allTrips } = allTripsQuery.data;
const enrolledTripsQueryData = enrolledTripsQuery.data;
let enrolledTrips = null;
if (enrolledTripsQueryData) {
enrolledTrips = enrolledTripsQueryData.data;
}
return (
<div id="dashboard">
{enrolledTrips?.length ? (
<div>
<h3 style={{ alignContent: "center" }}>Your trips</h3>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
}}
>
{enrolledTrips
? enrolledTrips.map((trip) => {
return (
<Card
key={trip._id}
title={trip.title}
createdBy={trip.createdBy}
startDate={formatDate(trip.startDate ?? Date.now())}
endDate={formatDate(trip.endDate ?? Date.now())}
memberCount={trip.memberCount}
coverImage={trip.coverImage}
/>
);
})
: null}
</div>
</div>
) : null}
<div>
<h3>All trips</h3>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
}}
>
{allTrips?.map((trip) => {
return (
<NavLink to={`/trips/${trip._id}`}>
<Card
key={trip._id}
title={trip.title}
createdBy={trip.createdBy}
startDate={formatDate(trip.startDate)}
endDate={formatDate(trip.endDate)}
memberCount={trip.memberCount}
coverImage={trip.coverImage}
/>
</NavLink>
);
})}
</div>
</div>
</div>
);
}
export default Dashboard;

View File

@ -0,0 +1,124 @@
import { QueryClient, useMutation, useQuery } from "@tanstack/react-query";
import { useEffect, useRef } from "react";
import { createTrip, getTrip, updateTrip } from "../api/trips";
import "../css/CreateTrip.css";
import { formatDate } from "../utils";
import { useParams } from "react-router-dom";
const queryClient = new QueryClient();
function EditTrip() {
const { id } = useParams();
const tripQuery = useQuery({
queryKey: ["trips", id],
enabled: !!id,
queryFn: () => getTrip(id),
});
const editTripMutation = useMutation({
mutationFn: (data) => {
if (id) return updateTrip(id, data);
else return createTrip();
},
onSuccess: () => {
queryClient.invalidateQueries(["trips"]);
},
});
const formRef = useRef();
useEffect(() => {
if (tripQuery.data) {
const { title, description, startDate, endDate, mapUrl } = tripQuery.data?.data;
if (!!formRef.current) {
formRef.current.title.value = title ?? "";
formRef.current.description.value = description ?? "";
formRef.current.startDate.value = formatDate(startDate ?? Date.now());
formRef.current.endDate.value = formatDate(endDate ?? Date.now());
formRef.current.mapUrl.value = mapUrl ?? "";
}
}
}, [tripQuery.data]);
if (tripQuery.isLoading && tripQuery.fetchStatus !== "idle") return "Loading...";
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(formRef.current);
const data = Object.fromEntries(formData);
editTripMutation.mutate(data);
};
return (
<>
<section id="create-trip" className="d-flex dir-col">
<h2 className="text-center trip-title">Create Trip</h2>
<div className="d-flex justify-content-center">
<form className="input-container" ref={formRef} onSubmit={handleSubmit} method="post">
<div className="d-flex g-3">
<div className="d-flex dir-col stretch">
<label className="input-label" htmlFor="title">
Title
</label>
<input type="text" id="trip-name" name="title" placeholder="Title for the trip" />
</div>
<div className="d-flex dir-col stretch">
<label className="input-label" htmlFor="image">
Cover Image URL
</label>
<input type="text" id="image" name="coverImage" />
</div>
</div>
<div>
<label className="input-label" htmlFor="description">
Description
</label>
<textarea
name="description"
id="description"
cols="auto"
rows={3}
placeholder="Description for the trip"
/>
</div>
<div className="d-flex g-3">
<div className="d-flex dir-col stretch">
<label className="input-label" htmlFor="start-date">
Start Date
</label>
<input type="date" id="start-date" name="startDate" />
</div>
<div className="d-flex dir-col stretch">
<label className="input-label" htmlFor="end-date">
end Date
</label>
<input type="date" id="end-date" name="endDate" />
</div>
</div>
<div className="d-flex dir-col stretch mb-1in">
<label className="input-label" htmlFor="map-url">
Map URL
</label>
<input
type="text"
id="map-url"
name="mapUrl"
placeholder="Enter the google maps link"
/>
</div>
<button
type="submit"
className="btn d-flex align-items-center justify-content-center mx-auto"
>
Save
</button>
</form>
</div>
</section>
</>
);
}
export default EditTrip;

View File

@ -0,0 +1,132 @@
import React, { useContext, useEffect } from "react";
import { NavLink, useNavigate } from "react-router-dom";
import Dollar from "../assets/images/dollar.png";
import trpizyLogo from "../assets/logo/tripzy.png";
import { AuthContext } from "../context/auth";
import { logOut } from "../firebase/firebase";
const CoinChip = ({ coins }) => {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
maxWidth: "fit-content",
paddingTop: "0.5rem",
paddingBottom: "0.5rem",
paddingRight: "1.25rem",
paddingLeft: "1.25rem",
// maxHeight: "100px",
gap: "5px",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "black",
borderRadius: 100,
}}
>
<div>
<img
style={{
width: "20px",
height: "20px",
}}
src={Dollar}
alt="coin"
/>
</div>
<div>{coins}</div>
</div>
);
};
function Navbar() {
const { user, loading } = useContext(AuthContext);
const navigate = useNavigate();
useEffect(() => {
if (!user && !loading) {
navigate("/signin");
}
}, [user]);
const Display = (props) => {
if (user) {
return (
<div
style={{
display: "flex",
}}
>
<CoinChip coins={user?.coins || 23} />
<button
type="button"
style={{
padding: "5px 20px",
color: "#000",
backgroundColor: "#f1a90d",
height: "50px",
fontSize: "24px",
marginTop: "5px",
marginLeft: "10px",
}}
onClick={() => {
logOut();
}}
>
Logout
</button>
</div>
);
}
};
return (
<header className="d-flex align-items-center g-3">
<img src={trpizyLogo} width="60px" alt="" />
<h1 className="secondary-font page-title larger-2">Tripzy</h1>
<ul
className="navbar-nav me-auto mb-2 mb-lg-0 d-flex justify-content-evenly"
style={{ flexDirection: "row", width: "20%" }}
>
<li className="nav-item">
<NavLink className="nav-link active" aria-current="page" to="/">
Home
</NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link" to="/trips/new">
Host Trip
</NavLink>
</li>
</ul>
<form className="d-flex">
<input
className="form-control me-2"
type="search"
placeholder="Search"
aria-label="Search"
style={{ width: "500px", margin: "10px 30px 0px 10px" }}
/>
{/* <button
style={{
padding: "5px 20px",
color: "#fff",
backgroundColor: "#002B5B",
height: "50px",
fontSize: "24px",
marginTop: "5px",
}}
type="submit"
>
Search
</button> */}
<Display />
</form>
</header>
);
}
export default Navbar;

View File

@ -0,0 +1,64 @@
import React, { useContext, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import searchImage from "../assets/icons/search.png";
import img1 from "../assets/images/img1.jpeg";
import img2 from "../assets/images/img2.jpg";
import img6 from "../assets/images/img6.jpg";
import { AuthContext, useAuth } from "../context/auth";
import "../css/signin.css";
import "../css/style.css";
import "../css/util.css";
function Signup() {
const auth = useAuth();
const { user, loading } = useContext(AuthContext);
const navigate = useNavigate();
useEffect(() => {
if (user && !loading) {
navigate("/", { replace: true });
}
}, [user]);
return (
<section id="login-page">
<div className="d-flex justify-content-between p-5 container">
<div className="fb-48 d-flex justify-content-center mt-5">
{/* The carousel part */}
<div id="carouselExampleControls" className="carousel slide" data-bs-ride="carousel">
<div className="carousel-inner">
<div className="carousel-item">
<img src={img1} className="d-block w-100" alt="..." />
</div>
<div className="carousel-item active">
<img src={img2} className="d-block w-100" alt="..." />
</div>
<div className="carousel-item">
<img src={img6} className="d-block w-100" alt="..." />
</div>
</div>
</div>
</div>
<div className="vertical-line" />
<div className="fb-48 d-flex justify-content-center">
{/* The Form Part */}
<div className="input-container d-flex dir-col mt-5 align-items-center g-3">
<h3 className="text-center primary-font larger p-0 m-0 dark-purple">Sign in</h3>
<input type="text" placeholder="Email" />
<input type="password" placeholder="Password" />
<button className="btn btn-primary">Sign in</button>
<p className="large primary-font">---------- or ----------</p>
{/* or signin with google */}
<div className="d-flex justify-content-center">
<button className="btn btn-google d-flex align-items-center g-2" onClick={auth.login}>
<span>Sign in with Google</span>
<img src={searchImage} width="30px" alt="" />
</button>
</div>
</div>
</div>
</div>
</section>
);
}
export default Signup;

View File

@ -0,0 +1,186 @@
import React, { useRef } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { getTrip } from "../api/trips";
import {
Activities,
Activity,
ActivityDate,
ActivityDescription,
ActivityFlex,
ActivityImage,
ActivityTitle,
Background,
Box,
Button,
Container,
Content,
Day,
Description,
Flex,
Form,
Heading,
Itinerary,
MapLink,
Message,
MessageAuthor,
MessageFlex,
MessageStatus,
MessageTitle,
Section,
Span,
Suggestion,
TextArea,
Title,
} from "../components";
import { axiosInstance, formatDate } from "../utils";
const ViewTripDetails = () => {
const { id } = useParams();
const sug = useRef(null);
const tripQuery = useQuery({
queryKey: ["trips", id],
queryFn: () => getTrip(id),
});
function createSuggestion(id, dt) {
return axiosInstance
.post(`suggestions/${id}`, {
tripId: id,
dt,
})
.then((res) => res.dt);
}
const enrollTripMutation = useMutation({
mutationFn: () => {
return enrollTrip(id);
},
onSuccess: () => {
// queryClient.invalidateQueries(["trips"]);
console.log("Success");
},
});
if (tripQuery.isLoading && tripQuery.fetchStatus !== "idle") {
return <div>Loading...</div>;
}
const { data } = tripQuery.data;
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
// const data = Object.fromEntries(formData);
let dt = {
suggestion: sug?.current.value,
};
createSuggestionMutate.mutate(id, dt);
};
return (
<>
<Section>
<Background
style={{
backgroundImage: "url(" + data?.coverImage + ")",
}}
>
<Title>{data?.title}</Title>
<Button
onClick={() => {
enrollTripMutation.mutate();
}}
>
Join Now
</Button>
</Background>
<Content>
<Description>
<Heading>Description:</Heading>
<br />
{data?.description}
</Description>
<hr />
<Flex flexDirection="column" justify-content="left">
<Flex flexDirection="row">
<Box>Begins on</Box>
<Box>{formatDate(data?.startDate ?? Date.now())}</Box>
<Box>till</Box>
<Box>{formatDate(data?.endDate ?? Date.now())}</Box>
</Flex>
<Flex flexDirection="row" bgColor="#FC730080">
<Box>Members enrolled: </Box>
<Box>{data?.memberCount ?? 45}</Box>
</Flex>
</Flex>
<MapLink href={data?.mapUrl} target="_blank" textAlign="center">
Open in map
</MapLink>
</Content>
</Section>
<Itinerary>
<Title color="#000">Itinerary</Title>
<Day>
<Heading font-size="20px">Day 1</Heading>
<Activities>
<Activity>
<Flex>
<ActivityDate>
Start <Span>03-03-2023</Span>
</ActivityDate>
<ActivityDate>
End <Span>05-03-2023</Span>
</ActivityDate>
</Flex>
<Flex gap="10px">
<ActivityImage src="https://images.unsplash.com/photo-1543731068-7e0f5beff43a" />
<ActivityFlex>
<ActivityTitle>Activity Title</ActivityTitle>
<ActivityDescription>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quos facere iure
eligendi!
</ActivityDescription>
</ActivityFlex>
</Flex>
<MapLink href="https://maps.google.com/">Open in Map</MapLink>
</Activity>
</Activities>
</Day>
</Itinerary>
<Suggestion>
<div className="left-side">
<Title color="#000">Suggestions</Title>
<Form onSubmit={handleSubmit}>
<TextArea ref={sug} placeholder="Type your suggestion..." rows={5} />
<Button type="submit">Submit</Button>
</Form>
</div>
<div className="right-side">
<Title color="#000">Approvals</Title>
<Container>
<MessageFlex>
<Message>
<Flex bgColor="transparent">
<MessageAuthor>Hello World</MessageAuthor>
<MessageStatus>PENDING</MessageStatus>
</Flex>
<MessageTitle>Can we bring something in the trip.</MessageTitle>
<Flex bgColor="transparent">
<Button>Accept</Button>
<Button>Decline</Button>
</Flex>
</Message>
</MessageFlex>
</Container>
</div>
</Suggestion>
</>
);
};
export default ViewTripDetails;

View File

@ -0,0 +1,17 @@
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: import.meta.env.VITE_apiKey,
authDomain: import.meta.env.VITE_authDomain,
projectId: import.meta.env.VITE_projectId,
storageBucket: import.meta.env.VITE_storageBucket,
messagingSenderId: import.meta.env.VITE_messagingSenderId,
appId: import.meta.env.VITE_appId,
};
// Initialize Firebase
export const app = initializeApp(firebaseConfig);

View File

@ -0,0 +1,42 @@
import { createContext, useContext, useState, useEffect } from "react";
import { loginWithGoogle, auth } from "../firebase/firebase";
import { postUser } from "../api/user";
import { useAuthState } from "react-firebase-hooks/auth";
import { notify } from "../utils";
export const AuthContext = createContext();
const AuthProvider = (props) => {
const [user, setUser] = useState(null);
const [_, loading, error] = useAuthState(auth);
useEffect(() => {
auth.onAuthStateChanged(setUser);
}, []);
const login = async () => {
const { user, error } = await loginWithGoogle();
if (!user && !loading) {
console.log(error);
if (error?.code !== "auth/cancelled-popup-request") {
notify(error.message, "error");
}
return;
}
await postUser({ token: user.accessToken });
localStorage.setItem("access_token", user.accessToken);
console.log(localStorage.getItem("access_token"));
};
const value = { user, login, loading, error };
return (
<AuthContext.Provider value={value} {...props}>
{props.children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
export default AuthProvider;

View File

@ -0,0 +1,65 @@
/* This file will contain styles for the create trip form */
#create-trip, #add-activity {
min-height: 100vh;
background-color: var(--bg-beige);
}
.trip-title {
font-family: var(--secondary-font);
font-size: 3rem;
letter-spacing: 1mm;
text-align: center;
margin: 1em 0;
color: var(--dark-purple);
text-shadow: 0px 0px 5px rgba(77, 69, 93, 0.75);
}
.input-container {
display: flex;
flex-direction: column;
/* justify-content: center; */
align-items: stretch;
width: 70%;
}
input,
textarea {
width: 100%;
padding: 0.5em;
margin: 0.5em 0;
border: 1px solid var(--light-cyan);
font-family: var(--para-font);
border-radius: 5px;
font-size: 1.5rem;
transition: border 250ms ease-in-out;
background-color: whitesmoke;
}
textarea {
resize: vertical;
}
input:focus,
textarea:focus {
outline: none;
border: 3px solid var(--light-cyan);
}
input[type="text"]::placeholder,
textarea::placeholder {
transition: transform 500ms ease, opacity 500ms ease;
}
input[type="text"]:focus::placeholder {
transform: translate(50%);
opacity: 0;
}
.input-label {
font-family: var(--secondary-font);
font-size: 1.5rem;
font-weight: 700;
letter-spacing: 1mm;
color: var(--dark-purple);
text-shadow: 0px 0px 5px rgba(77, 69, 93, 0.75);
}

View File

@ -0,0 +1,85 @@
/* This page will only maintain styles for dashboard */
#dashboard{
min-height: 100vh;
background-color: var(--bg-beige);
}
.tour-card{
width: 400px;
height:350px ;
position: relative;
border-radius: 12px;
margin: .5em 1.5em;
display:inline-block;
transition: box-shadow 300ms ease-out;
/* background-image: url('https://travellersworldwide.com/wp-content/uploads/2022/06/shutterstock_712575202.jpg.webp'); */
background-size: cover;
}
.card-overlay{
position: absolute;
inset: 0;
z-index: 0;
border-radius: 12px;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.822) 20%, transparent 80%);
transition: background 400ms ease-in-out;
}
.over-1{
position: relative;
z-index: 1;
}
.host-name{
font-family: var(--secondary-font);
font-weight: 900;
font-size: 1rem;
margin: 0;
padding: 1em .5em;
color: var(--dark-purple);
background-color: white;
}
.card-details{
padding-left: .5em;
height: 35%;
}
.tour-name{
font-family: var(--primary-font-bold);
font-weight: 900;
font-size: 2rem;
/* padding: 1em; */
color: var(--bg-yellow);
}
.team{
font-family: Verdana, Geneva, Tahoma, sans-serif, Courier, monospace;
font-weight: 500;
font-size: 1rem;
display: inline-block;
margin-top: .5em;
color: var(--bg-beige);
}
.team::after{
content: "";
background-image: url('../assets/icons/team.png');
background-size: 25px;
background-repeat: no-repeat;
display: inline-block;
width: 30px;
height: 15px;
margin-left: .5em;
}
.tour-date{
font-family: var(--secondary-font);
font-weight: 500;
font-size: 1rem;
display: inline-block;
margin-top: .5em;
color: var(--bg-beige);
}

View File

View File

@ -0,0 +1,74 @@
@font-face {
font-family: 'Philosopher-Reg';
src: url('../assets/fonts/Philosopher/Philosopher-Regular.ttf');
/*
font-family: 'Philosopher-Ital';
src: url('../fonts/Philosopher/Philosopher-Italic.ttf'); */
}
@font-face {
font-family: 'Philosopher-Bold';
src: url('../assets/fonts/Philosopher/Philosopher-Bold.ttf');
}
@font-face {
font-family: 'SalmaPro';
src: url('../assets/fonts/SalmaPro/SalmaPro-Medium.otf');
/* font-family: 'SalmaPro-Narr';
src: url('../fonts/SalmaPro/SalmaPro-MediumNarrow.otf'); */
}
@font-face {
font-family: 'SimplyMono';
src: url('../assets/fonts/SimplyMono/SimplyMono-Book.ttf');
/* font-family: 'SimplyMono-Ital';
src: url('../fonts/SimplyMono/SimplyMono-BookOblique.ttf'); */
}
@font-face {
font-family: 'RobotoSlab';
src: url('../assets/fonts/RobotoSlab.ttf');
}
:root{
/* Color scheme */
--dark-purple : #4D455D;
--dark-purple-blur : #4d455d85;
--sky-pink : #E96479;
--bg-beige : #F5E9CF;
--bg-yellow : #fbffb1;
--light-cyan : #7DB9B6;
--orange : #FC7300;
--florecent : #BFDB38;
--dark-florecent : #1F8A70;
--light-pink : #FFACAC;
--dark-blue : #002B5B;
/* font families */
--primary-font : 'Philosopher-Reg';
--primary-font-bold : 'Philosopher-Bold';
--secondary-font : 'SalmaPro';
--para-font : 'RobotoSlab';
--title-font : 'SimplyMono';
}
.primary-font{
font-family: var(--primary-font);
}
.primary-font-bold{
font-family: var(--primary-font-bold);
}
.secondary-font{
font-family: var(--secondary-font);
}
.para-font{
font-family: var(--para-font);
}
.title-font{
font-family: var(--title-font);
}
.dark-purple{
color: var(--dark-purple);
}

View File

@ -0,0 +1,41 @@
/* This page will only maintain styles for login-page */
#login-page{
min-height: 100vh;
background-color: var(--bg-beige);
}
.vertical-line{
background-color: var(--dark-purple);
width: 4px;
height: 77vh;
}
.input-container{
width: 70%;
}
input[type="text"], input[type="password"]{
width: 100%;
padding: .5em;
margin: 0.5em 0;
border: 1px solid var(--light-cyan);
font-family: var(--para-font);
border-radius: 5px;
font-size: 1.5rem;
transition: border 250ms ease-in-out;
}
input:focus{
outline: none;
border: 5px solid var(--light-cyan);
}
input::placeholder{
transition: transform 500ms ease, opacity 500ms ease;
}
input:focus::placeholder{
transform: translate(50%);
opacity: 0;
}

View File

@ -0,0 +1,86 @@
/* this css will maintain styles common for all documents */
body, html{
padding: 0;
margin: 0;
}
*,*::after,*::before{
box-sizing: border-box;
}
/* styles under header */
header{
padding: 1em 3em ;
background-color: var(--light-cyan);
}
.page-title{
margin: 0;
padding: 0;
font-weight: bold;
color: var(--dark-purple);
text-shadow: 0px 0px 5px rgba(77, 69, 93, 0.75);
}
.btn{
padding: 0.5em 1em;
border-radius: 5px;
font-family: var(--primary-font);
font-size: 1.5rem;
font-weight: bold;
cursor: pointer;
background-color: var(--light-cyan);
color: var(--dark-purple);
border : 5px outset var(--dark-purple-blur);
transition: background-color 250ms ease-in-out;
}
.btn:active{
border : 5px inset var(--dark-purple-blur);
}
/* Carousel styles */
.carousel{
width: 90%;
height: 90%;
background-color: var(--bg-beige);
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border-radius: 5px;
}
.carousel-inner{
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.carousel.slide img{
width: 100%;
height: 100%;
object-fit: cover;
}
.carousel-item{
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 500ms ease-in-out;
}
.carousel-item.active{
opacity: 1;
}

View File

@ -0,0 +1,174 @@
/* This file maintains the styles that are unnecessarily repeated*/
.smaller {
font-size: 0.7rem;
}
.small {
font-size: 0.9rem;
}
.medium {
font-size: 1rem;
}
.large {
font-size: 1.5rem;
}
.larger {
font-size: 2.5rem;
}
.larger-2 {
font-size: 2.8rem;
}
.text-start {
text-align: start;
}
.text-center {
text-align: center;
}
.text-end {
text-align: end;
}
.text-justify {
text-align: justify;
}
.uppercase {
text-transform: uppercase;
}
.capitalize {
text-transform: capitalize;
}
.f-bold {
font-weight: bolder;
}
.border {
border: 1px solid black;
}
.b-radius-subtle {
border-radius: 5px;
}
.of-v {
overflow: visible;
}
.of-s {
overflow-x: auto;
}
.of-h {
overflow: hidden;
}
.d-none {
display: none;
}
.d-block {
display: block;
}
.d-in-block {
display: inline-block;
}
.d-flex {
display: flex;
}
.dir-row-rev {
flex-direction: row-reverse;
}
.dir-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.justify-content-between {
justify-content: space-between;
}
.justify-content-around {
justify-content: space-around;
}
.justify-content-center {
justify-content: center;
}
.align-items-center {
align-items: center;
}
.align-items-stretch {
align-items: stretch;
}
.align-items-top {
align-items: flex-start;
}
.fb-100 {
flex-basis: 100%;
}
.fb-80 {
flex-basis: 80%;
}
.fb-70 {
flex-basis: 70%;
}
.fb-60 {
flex-basis: 60%;
}
.fb-50 {
flex-basis: 50%;
}
.fb-48 {
flex-basis: 48%;
}
.fb-40 {
flex-basis: 40%;
}
.fb-30 {
flex-basis: 30%;
}
.fb-20 {
flex-basis: 20%;
}
.g-2{
gap: 0.5em;
}
.g-3 {
gap: 1em;
}
.g-4 {
gap: 1.5em;
}
.g-6 {
gap: 3em;
}
.g-1in {
gap: 1in;
}
.stretch {
width: 100%;
}
.height-100 {
height: 100%;
}
.bg-transparent {
background: transparent;
}
.bg-black {
background: black;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.ms-auto {
margin-left: auto;
}
.me-auto {
margin-right: auto;
}
.p-5 {
padding: 2.5em;
}
.p-0{
padding:0;
}
.m-0{
margin:0;
}
.mt-1in {
margin-top: 1in;
}
.mt-5 {
margin-top: 2.5em;
}

View File

@ -0,0 +1,20 @@
import { GoogleAuthProvider, getAuth, signInWithPopup, signOut } from "firebase/auth";
const provider = new GoogleAuthProvider();
provider.addScope("https://www.googleapis.com/auth/contacts.readonly");
export const auth = getAuth();
export const loginWithGoogle = async () => {
try {
const response = await signInWithPopup(auth, provider);
const user = response.user;
return { user };
} catch (error) {
return { error };
}
};
export const logOut = async () => {
signOut(auth);
};

View File

@ -0,0 +1,65 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
background-color: #f5e9cf;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #f5e9cf;
}
a:hover {
color: #747bff;
}
button {
background-color: #f5e9cf;
}
}

View File

@ -0,0 +1,11 @@
import "./config/firebase-config";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,27 @@
import axios from "axios";
import { toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
export const notify = (message, type) => {
toast(message, {
position: toast.POSITION.TOP_RIGHT,
autoClose: 5000,
pauseOnHover: true,
type: type,
});
};
export const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_backendURL,
headers: {
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
"ngrok-skip-browser-warning": true,
// timeout: 1000,
},
});
// format in yyyy-MM-dd
export const formatDate = (date) => {
console.log(date);
return new Date(date).toISOString().slice(0, 10);
};

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})

File diff suppressed because it is too large Load Diff