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
19 changes: 19 additions & 0 deletions api/main_endpoints/models/Visit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const VisitSchema = new Schema(
{
visitCount: {
type: Number,
default: 0
},
type: {
type: String,
enum: ['HOME_PAGE'],
default: 'HOME_PAGE'
}
},
{ collection: 'Visit' }
);

module.exports = mongoose.model('Visit', VisitSchema);
63 changes: 63 additions & 0 deletions api/main_endpoints/routes/Visit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const express = require('express');
const router = express.Router();
const Visit = require('../models/Visit');
const {
NOT_FOUND,
FORBIDDEN,
BAD_REQUEST,
OK,
INTERNAL_SERVER_ERROR
} = require('../../util/constants').STATUS_CODES;
const logger = require('../../util/logger');

const ALLOWED_ORIGIN = 'https://sce.sjsu.edu';
const VALID_TYPES = Visit.schema.path('type').enumValues;

router.post('/:type', async (req, res) => {
const { type } = req.params;

if (process.env.NODE_ENV === 'production') {
const origin = req.get('origin');
const referer = req.get('referer');
const isAllowed = origin === ALLOWED_ORIGIN || (referer && referer.startsWith(ALLOWED_ORIGIN));

if (!isAllowed) return res.sendStatus(FORBIDDEN);
}

if (!VALID_TYPES.includes(type)) {
return res.status(NOT_FOUND).send({ message: `Invalid page type: ${type}` });
}

try {
await Visit.findOneAndUpdate(
{ type },
{ $inc: { visitCount: 1 } },
{
new: true,
upsert: true,
setDefaultsOnInsert: true,
}
);

return res.sendStatus(OK);
} catch (error) {
logger.error(`Error incrementing visit for ${type}:`, error);
return res.sendStatus(INTERNAL_SERVER_ERROR);
}
});

router.get('/:type', async (req, res) => {
const { type } = req.params;

if (!VALID_TYPES.includes(type)) return res.sendStatus(NOT_FOUND);

try {
const record = await Visit.findOne({ type });
const visitCount = record ? record.visitCount : 0;
return res.status(OK).send({ visitCount });
} catch (error) {
return res.sendStatus(BAD_REQUEST);
}
});

module.exports = router;
42 changes: 42 additions & 0 deletions src/APIFunctions/Visit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ApiResponse } from './ApiResponses';
import { BASE_API_URL } from '../Enums';

export async function incrementVisitCount(type) {
let status = new ApiResponse();
try {
const url = new URL(`api/Visit/${type}`, BASE_API_URL);
await fetch (url.href, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (res.ok) {
status.responseData = result;
} else {
status.error = true;
}
} catch (err) {
status.responseData = err;
status.error = true;
}
return status;
}

export async function getVisitCount(type) {
let status = new ApiResponse();
try {
const url = new URL(`api/Visit/${type}`, BASE_API_URL);
const res = await fetch (url.href);
if (res.ok) {
const result = await res.json();
status.responseData = result;
} else {
status.error = true;
}
} catch (err) {
status.responseData = err;
status.error = true;
}
return status;
}
185 changes: 91 additions & 94 deletions src/Pages/Home/Home.js
Original file line number Diff line number Diff line change
@@ -1,126 +1,123 @@
import React, { useEffect, useState} from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { Link } from 'react-router-dom'; // Use Link for internal navigation
import Footer from '../../Components/Footer/Footer.js';
import './Home.css';

import { getAd } from '../../APIFunctions/Advertisement.js';
import { getHomeImage } from '../../APIFunctions/Image.js';
import { incrementVisitCount, getVisitCount } from '../../APIFunctions/Visit.js';
import './Home.css';

const Home = () => {
const DEFAULT_IMAGE = 'https://raw.githubusercontent.com/thebeninator/Clark/refs/heads/add_comp_homepage/public/images/compressed2.jpg';

const [message, setMessage] = useState('');
const [showMessage, setShowMessage] = useState(false);
const [showAll, setShowAll] = useState(false);
const [homeImageUrl, setHomeImageUrl] = useState('');
const [homeImageAlt, setHomeImageAlt] = useState('');

async function getMessage() {
try {
const messageData = await getAd();
const adsList = messageData.responseData;
setMessage(adsList['message']);
setShowMessage(true);
} catch {
setMessage('');
}
}

async function loadHomeImage() {
const DEFAULT_IMAGE = 'https://raw.githubusercontent.com/thebeninator/Clark/refs/heads/add_comp_homepage/public/images/compressed2.jpg';
const response = await getHomeImage();
let url = DEFAULT_IMAGE;
let alt = 'sce club image';
if (!response.error) {
url = response.responseData.url;
alt = response.responseData.alt;
}
setHomeImageUrl(url);
setHomeImageAlt(alt);
}
const Home = () => {
const [isLoaded, setIsLoaded] = useState(false);
const [data, setData] = useState({
message: '',
imageUrl: DEFAULT_IMAGE,
imageAlt: 'SCE Club Image',
visitCount: 0,
});

useEffect(() => {
getMessage();
loadHomeImage();
setTimeout(() => setShowAll(true), 100);
const fetchData = async () => {
try {
const [adRes, imgRes, _] = await Promise.all([
getAd(),
getHomeImage(),
incrementVisitCount('HOME_PAGE'),
]);

const visitRes = await getVisitCount('HOME_PAGE');

setData({
message: adRes.responseData?.message || '',
imageUrl: imgRes.error ? DEFAULT_IMAGE : imgRes.responseData.url,
imageAlt: imgRes.error ? 'SCE Club Image' : imgRes.responseData.alt,
visitCount: visitRes.responseData?.visitCount || 0,
});
} catch (err) {
} finally {
setIsLoaded(true);
}
};

fetchData();
}, []);

function isValidUrl(str) {
try {
new URL(str);
return true;
} catch (_) {
return false;
}
}

function renderMessageWithLinks(message) {
if (!message) {
return null;
}
return message.split(/(https?:\/\/[^\s]+)/g).map((part, index) => {
if (isValidUrl(part)) {
const renderedMessage = useMemo(() => {
if (!data.message) return null;

return data.message.split(/(https?:\/\/[^\s]+)/g).map((part, i) => {
const isUrl = /https?:\/\/[^\s]+/.test(part);
if (isUrl) {
return (
<a
key={index}
href={part}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 underline hover-grow"
>
<a key={i} href={part} target="_blank" rel="noopener" className="text-blue-400 underline hover:text-blue-200 transition-colors">
{part}
</a>
);
}
return <span key={index}>{part}</span>;
return <span key={i}>{part}</span>;
});
}
}, [data.message]);

// only include values passed in that evaluate to true
const getClassName = (...classes) => classes.filter(Boolean).join(' ');

return (
<div className="flex flex-col min-h-[calc(100vh-86px)] z-[-200] bg-gradient-to-r from-gray-800 to-gray-600">
<div className="flex flex-col flex-wrap items-center justify-center flex-1 h-full py-4 md:flex-row xl:my-0">
<div className="flex flex-col items-center justify-center w-full p-4 overflow-y-hidden xl:w-2/5">
<div className="flex flex-col mb-8 xl:ml-10">
<h1
className={`slide-in-top hover-grow my-4 text-3xl font-bold text-center text-white opacity-75 md:text-5xl xl:text-left${showAll ? ' show' : ''}`}>
The Software and Computer Engineering Society
<div className="flex flex-col min-h-[calc(100vh-86px)] bg-gradient-to-r from-gray-800 to-gray-600 overflow-x-hidden">
<main className="flex flex-col md:flex-row items-center justify-center flex-1 py-8 container mx-auto">
<section className="flex flex-col items-center xl:items-start w-full p-6 xl:w-2/5">
<div className={getClassName(
'transition-all duration-1000 transform',
isLoaded ? 'translate-y-0 opacity-100' : '-translate-y-10 opacity-0'
)}>
<h1 className="hover-grow text-4xl md:text-6xl font-extrabold text-white text-center xl:text-left mb-4 leading-tight cursor-default">
The Software and Computer <br /> Engineering Society
</h1>

<h3
className={`slide-in-right hover-grow text-base text-gray-400 text-center xl:text-left md:text-2xl${showAll ? ' show' : ''}`}>
SJSU's Largest Engineering Club
<h3 className="hover-grow text-xl md:text-2xl text-gray-400 text-center xl:text-left mb-8 cursor-default">
SJSU's Largest Engineering Club
</h3>
</div>

<div className={`slide-in-bottom flex justify-center space-x-3${showAll ? ' show' : ''}`}>
<a href="/about" rel="nofollow noreferrer" target="_blank">
<button className="btn btn-outline text-lg text-blue-400 hover:bg-blue-700 hover:!text-white hover-grow">
Learn More
</button>
</a>

<a href="/register">
<button className="btn btn-outline text-lg btn-accent hover:!text-white hover-grow">
Join Us!
</button>
</a>
<div className={getClassName(
'flex space-x-4 transition-all delay-500 duration-1000',
isLoaded ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'
)}>
<Link to="/about" className="btn btn-outline hover-grow text-blue-400 border-blue-400 hover:bg-blue-400 hover:text-white px-8 text-lg">
Learn More
</Link>
<Link to="/register" className="btn btn-outline hover-grow btn-accent hover:!text-white px-8 text-lg">
Join Us!
</Link>
</div>
</div>

</section>

<div className={`fade-in-img w-full h-full p-6 md:p-12 overflow-visible xl:w-3/5${showAll ? ' show' : ''}`}>
<section className={getClassName(
'relative w-full p-6 xl:w-3/5 transition-opacity duration-1000 delay-300',
isLoaded ? 'opacity-100' : 'opacity-0'
)}>
<div className="relative max-w-4xl mx-auto">
<img
className="w-full aspect-video object-cover mx-auto rounded-xl shadow-2xl hover-grow"
src={homeImageUrl}
alt={homeImageAlt}
className="w-full aspect-video object-cover rounded-2xl shadow-2xl transition-transform duration-500 hover-grow"
src={data.imageUrl}
alt={data.imageAlt}
/>

<div className={`absolute -top-4 -right-4 md:right-0 md:-top-2 z-10 fade-scale-in${showMessage ? ' show' : ''}`}>
<div className="minecraft-styling text-yellow-400 drop-shadow-lg transform rotate-[-20deg] text-sm md:text-2xl whitespace-nowrap">
{renderMessageWithLinks(message)}
{data.message && (
<div className={getClassName('absolute -top-4 -right-4 md:right-0 md:-top-2 z-10 fade-scale-in', isLoaded && 'show')}>
<div className="minecraft-styling text-yellow-400 drop-shadow-lg transform rotate-[-20deg] text-sm md:text-2xl whitespace-nowrap">
{renderedMessage}
</div>
</div>
</div>
)}
</div>
</div>
</section>
</main>

<div className={getClassName(
'pr-6 pb-6 text-base text-gray-400 text-right transition-all duration-700',
isLoaded ? 'translate-x-0 opacity-100' : 'translate-x-10 opacity-0'
)}>
Visit Count: <span className="font-mono">{data.visitCount.toLocaleString()}</span>
</div>

<Footer />
Expand Down