Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions api/main_endpoints/models/Advertisement.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ const AdvertisementSchema = new Schema(
required: true,
maxlength: [255, 'message must be at most 255 characters long']
},
expireDate: {
expireAt: {
type: Date,
default: undefined,
index: {expireAfterSeconds: 0}, // TTL only kicks in when expireAt is set
}
},
{ collection: 'Advertisements' }
{ collection: 'Advertisements', timestamps: { createdAt: true } }
);

module.exports = mongoose.model('Advertisement', AdvertisementSchema);
22 changes: 10 additions & 12 deletions api/main_endpoints/routes/Advertisement.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,23 @@ router.get('/', async (req, res) => {
res.status(OK).send(items || {});
})
.catch(error => {
logger.error('/api/Advertisement/ had an error', error);
res.sendStatus(BAD_REQUEST);
});
});


router.get('/getAllAdvertisements', async (req, res) => {
if (!checkIfTokenSent(req)) {
return res.sendStatus(FORBIDDEN);
} else if (!await decodeToken(req)) {
return res.sendStatus(UNAUTHORIZED);
}
Advertisement.find()
.sort({ createdAt: -1 })
.then(items => res.status(OK).send(items))
.catch(error => {
logger.error('/api/Advertisement/getAllAdvertisements/ had an error', error);
res.sendStatus(BAD_REQUEST);
});
});
Expand All @@ -48,7 +52,7 @@ router.post('/createAdvertisement', async (req, res) => {

const newAd = new Advertisement({
message: req.body.message,
expireDate: req.body.expireDate
expireAt: req.body.expireDate
});

try {
Expand All @@ -58,7 +62,7 @@ router.post('/createAdvertisement', async (req, res) => {
action: AuditLogActions.CREATE_AD,
details: {
message: createdAd.message,
expireDate: createdAd.expireDate,
expireAt: createdAd.expireAt,
advertisementId: createdAd._id
}
}).catch(logger.error);
Expand All @@ -83,15 +87,9 @@ router.post('/deleteAdvertisement', async (req, res) => {
}

try {
const adToDelete = await Advertisement.findById(req.body._id);

if (!adToDelete) {
return res.sendStatus(NOT_FOUND);
}

const deleteResult = await Advertisement.deleteOne({_id: req.body._id});
const deleteResult = await Advertisement.findOneAndDelete({ _id: req.body._id });

if(deleteResult.deletedCount < 1) {
if(!deleteResult) {
return res.sendStatus(NOT_FOUND);
}

Expand All @@ -100,8 +98,8 @@ router.post('/deleteAdvertisement', async (req, res) => {
action: AuditLogActions.DELETE_AD,
details: {
deletedAd: {
id: adToDelete._id,
message: adToDelete.message,
id: deleteResult._id,
message: deleteResult.message,
}
}
}).catch(logger.error);
Expand Down
200 changes: 116 additions & 84 deletions src/Pages/Advertisement/AdvertisementAdmin.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ export default function AdvertisementAdmin() {

const [ads, setAds] = useState([]);
const [message, setMessage] = useState('');
const [year, setYear] = useState();
const [month, setMonth] = useState();
const [day, setDay] = useState();
const [expireDate, setExpireDate] = useState();
const [expireButtonClicked, setExpiredButtonClicked] = useState(false);

async function getAdsFromDB() {
const adsFromDB = await getAds(user.token);
Expand All @@ -19,54 +18,129 @@ export default function AdvertisementAdmin() {
}
}

function isExpired() {
if (!expireDate) {
return false;
}
const currDate = new Date();
const expireDateObject = new Date(expireDate);
return expireDateObject < currDate;
}

async function createAdHandler() {
// make sure empty inputs are properly set as undefined if empty
if (year === '') {
setYear(undefined);
const newAd = { message };
if (expireDate) {
const asDateObject = new Date(expireDate);
newAd.expireDate = asDateObject.toISOString();
}
if (month === '') {
setMonth(undefined);
// expireDate is a string so we need to turn into date object
await createAd(newAd, user.token);

await getAdsFromDB();
}

function maybeRenderExpirationInput() {
if (!message) {
return null;
}
if (day === '') {
setDay(undefined);
if (!expireButtonClicked) {
return (
<>
<button
className="text-sm btn light:btn-gray dark:btn-neutral sm:text-base"
onClick={() => setExpiredButtonClicked(true)}
>
Set Expiration Date For Ad
</button>
</>
);
}
return (
<>
<input
className='text-sm input input-bordered sm:text-base'
type='datetime-local'
onChange={event => {
setExpireDate(event.target.value);
}}
/>
<button
className="text-sm btn btn-error sm:text-base"
onClick={() => {
setExpiredButtonClicked(false);
setExpireDate(undefined); // so that if the user decides to cancel after inputting a date, it will be N/A not prev input
}}
>
Remove Expiration
</button>
</>
);
}

let expireDate = new Date(year, month - 1, day);
if (isNaN(expireDate.getTime())) {
expireDate = undefined;
function getFormattedTime(maybeISOString = null) {
let date = new Date();
if (maybeISOString) {
date = new Date(maybeISOString);
}

await createAd({
message,
expireDate,
}, user.token);

await getAdsFromDB();
return date.toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: '2-digit',
hour12: true,
timeZoneName: 'short',
});
}

async function deleteExpiredAds() {
const adsFromDB = await getAds(user.token);
if (!adsFromDB.error) {
const currentDate = new Date();
const expiredAds = adsFromDB.responseData.filter(ad => {
if (ad.expireDate === undefined) return false;
return new Date(ad.expireDate) < currentDate;
});

for (const ad of expiredAds) {
await deleteAd(ad, user.token);
}
function maybeRenderCreateAdInputs() {
if (!message) {
return;
}
return (
<>
{message && <div>
<label className="w-full form-control">
<div className="label">
<span className="label-text text-md">
Optional: Type an expiration date for your ad. Time is in your local timezone ({Intl.DateTimeFormat().resolvedOptions().timeZone}).
</span>
</div>
</label>
</div>}
<div className="flex gap-2 mb-3">
{maybeRenderExpirationInput()}
<button
className="text-sm btn btn-primary sm:text-base"
onClick={() => {
createAdHandler();
}}
>
Create Ad
</button>
</div>
{
isExpired() && <div>

<label className="w-full form-control mb-3">
<div className="label flex flex-col items-start space-y-1">
<span className="text-red-600 dark:text-red-400 font-semibold">
Your selected expiration is considered behind the current time of {getFormattedTime()}.
</span>
<span className="text-red-600 dark:text-red-400 font-semibold">
Submitting an ad with this expiration will not save the ad in the database.
</span>
</div>
</label>
</div>
}
</>
);
}

useEffect(() => {
getAdsFromDB();

const intervalId = setInterval(async () => {
await deleteExpiredAds();
await getAdsFromDB();
}, 20);

}, []);

return (
Expand All @@ -83,65 +157,23 @@ export default function AdvertisementAdmin() {
<input
className="w-full text-sm input input-bordered sm:text-base"
type="text"
placeholder="Type at most 255 characters"
placeholder="Create a new ad here! (255 char max)"
maxlength="255"
onChange={event => {
setMessage(event.target.value);
}}
/>
</label>
</div>
<div>
<label className="w-full form-control">
<div className="label">
<span className="label-text text-md">
Optional: Type an expiration date for your ad. All inputs should be typed in numbers. ex. 2024-9-13
</span>
</div>
</label>
</div>
<div className="flex items-center space-x-4 mb-6">
<input
className="flex-1 text-sm input input-bordered sm:text-base"
type="text"
placeholder="Year"
onChange={event => {
setYear(event.target.value);
}}
/>
<input
className="flex-1 text-sm input input-bordered sm:text-base"
type="text"
placeholder="Month"
onChange={event => {
setMonth(event.target.value);
}}
/>
<input
className="flex-1 text-sm input input-bordered sm:text-base"
type="text"
placeholder="Day"
onChange={event => {
setDay(event.target.value);
}}
/>
<button
className="text-sm btn btn-primary sm:text-base"
onClick = {() => {
createAdHandler();
}}
>
Create Ad
</button>
</div>
{maybeRenderCreateAdInputs()}
<table className="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-6 py-3">
Advertisement Message
</th>
<th scope="col" className="px-6 py-3">
Expiriation Date
Expiration Date
</th>
<th scope="col" className="px-6 py-3">
Delete
Expand All @@ -156,12 +188,12 @@ export default function AdvertisementAdmin() {
{ad.message}
</th>
<td className="px-6 py-4">
{ad.expireDate === undefined ? 'N/A' : ad.expireDate}
{ad.expireAt ? getFormattedTime(ad.expireAt) : '-'}
</td>
<td className="px-6 py-4">
<button
className="text-sm btn btn-primary sm:text-base"
onClick = {async () => {
onClick={async () => {
await deleteAd(ad, user.token);
await getAdsFromDB();
}}
Expand Down