-
CrossKey Communication
- {selectedArticle ? (
-
-
-
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+
- ) : (
-
- )}
-
+
+
+
);
}
diff --git a/frontend/src/Components/Comments.css b/frontend/src/Components/Comments.css
new file mode 100644
index 0000000..23098fc
--- /dev/null
+++ b/frontend/src/Components/Comments.css
@@ -0,0 +1,58 @@
+.comments {
+ border-top: 1px solid #ddd;
+ padding: 1rem;
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.comments-input {
+ width: 100%;
+ padding: 10px;
+ margin-bottom: 8px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+}
+
+.comment-button {
+ padding: 8px 12px;
+ margin-bottom: 12px;
+ border: none;
+ background-color: #333;
+ color: white;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.comments-list-container {
+ max-height: 250px;
+ overflow-y: auto;
+ padding-right: 10px;
+ border-top: 1px solid #ccc;
+ margin-top: 10px;
+ position: relative;
+}
+
+.comments-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.comments-item {
+ padding: 8px;
+ border-bottom: 1px solid #eee;
+}
+
+/* Optional: always show scroll bar for clarity */
+.comments-list-container::-webkit-scrollbar {
+ width: 8px;
+}
+
+.comments-list-container::-webkit-scrollbar-thumb {
+ background-color: #888;
+ border-radius: 4px;
+}
+
+.comments-list-container::-webkit-scrollbar-thumb:hover {
+ background-color: #555;
+}
diff --git a/frontend/src/Components/Comments.js b/frontend/src/Components/Comments.js
new file mode 100644
index 0000000..bce445e
--- /dev/null
+++ b/frontend/src/Components/Comments.js
@@ -0,0 +1,71 @@
+import React, { useState, useEffect } from 'react';
+import './Comments.css';
+
+const Comments = ({ articleId, refreshTrigger }) => {
+ const [comments, setComments] = useState([]);
+ const [input, setInput] = useState('');
+ const userName = "cthomas";//temp user
+ const userId = 1;
+
+ useEffect(() => {
+ fetch('http://localhost:8081/comments/fetch/all')
+ .then(res => res.json())
+ .then(data => {
+ const filtered = data.filter(c => c.article && c.article.id === articleId);
+ setComments(filtered);
+ })
+ .catch(err => console.error("Error fetching comments:", err));
+ }, [articleId, refreshTrigger]);
+
+const handleAddComment = () => {
+ if (!input.trim()) return;
+
+fetch('http://localhost:8081/comments/add/comment', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ text: input,
+ articleId: articleId,
+ userName: userName,
+ }),
+})
+ .then(res => res.json())
+ .then(newComment => {
+ setComments(prev => [newComment, ...prev]); // Show new comment at top
+ setInput('');
+ })
+ .catch(err => console.error("Error posting comment:", err));
+ };
+
+ return (
+
+ );
+};
+
+export default Comments;
diff --git a/frontend/src/Components/FavoritePage.js b/frontend/src/Components/FavoritePage.js
new file mode 100644
index 0000000..97cbdb1
--- /dev/null
+++ b/frontend/src/Components/FavoritePage.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import NewsCard from './NewsCard';
+import { useNavigate } from 'react-router-dom';
+import './NewsGrid.css';
+
+const favoriteArticles = [
+ {
+ id: 1093,
+ title: 'āThunderboltsā and āSinnersā top box office charts once more - AP News',
+ articleDescription: 'Marvelās Thunderbolts and Ryan Cooglerās Sinners dominated again...',
+ articleBody: 'Marvels Thunderbolts and Ryan Cooglers Sinners dominated the North American box office charts again this weekend. Now in their second and fourth weekends respectively, the two films had some new comā¦',
+ author: 'Lindsey Bahr',
+ thumbnail: 'https://dims.apnews.com/dims4/default/cbc8a31/2147483647/strip/true/crop/5786x3255+0+224/resize/1440x810!/quality/90/?url=https%3A%2F%2Fassets.apnews.com%2Fd5%2Fc9%2F31b3d78b036fa15e412ec0ec425d%2F013a17ee80db48febaf712ff3bc15517', // Replace with real image if you have one
+ publishedAt: '2025-05-12T00:00:00Z',
+ source: { name: 'AP News' }
+ },
+ {
+ id: 1094,
+ title: '2025 NFL schedule opens with Cowboys at Eagles on Thursday, September 4 - NBC Sports',
+ articleDescription: 'Cowboys-Eagles has officially been announced as the leagueās opener.',
+ articleBody: 'The Cowboys will head to Philadelphia for the first game of the 2025 NFL season. Cowboys-Eagles has officially been announced as the leagueās opener, with the game in Philadelphia for the traditionalā¦',
+ author: 'Michael David Smith',
+ thumbnail: 'https://nbcsports.brightspotcdn.com/dims4/default/9153f05/2147483647/strip/true/crop/4066x2287+0+0/resize/1440x810!/quality/90/?url=https%3A%2F%2Fnbc-sports-production-nbc-sports.s3.us-east-1.amazonaws.com%2Fbrightspot%2Fa4%2F5c%2F5758e8e64a268d15bab3467f0309%2Fhttps-delivery-gettyimages.com%2Fdownloads%2F2183990250', // Optional: swap for real NFL image
+ publishedAt: '2025-05-13T00:00:00Z',
+ source: { name: 'NBCSports.com' }
+ }
+];
+
+const FavoritePage = () => {
+ const navigate = useNavigate();
+
+ const handleCardClick = (article) => {
+ navigate(`/article/${article.id}`, { state: { article } });
+ };
+
+ return (
+
+ {favoriteArticles.map(article => (
+ handleCardClick(article)}
+ isInitiallyFavorite={true}
+ lockFavorite={true}
+ />
+ ))}
+
+ );
+};
+
+export default FavoritePage;
diff --git a/frontend/src/Components/Footer.css b/frontend/src/Components/Footer.css
new file mode 100644
index 0000000..6f7a290
--- /dev/null
+++ b/frontend/src/Components/Footer.css
@@ -0,0 +1,12 @@
+.footer {
+ background-color: #0c0200;
+ color: white;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 24px 10px;
+ text-align: center;
+ min-height: 100px;
+ font-size: 1.4rem;
+ flex-direction: column;
+}
diff --git a/frontend/src/Components/Footer.js b/frontend/src/Components/Footer.js
index 651ae52..5f287cb 100644
--- a/frontend/src/Components/Footer.js
+++ b/frontend/src/Components/Footer.js
@@ -1 +1,16 @@
-import React from 'react'
\ No newline at end of file
+import React from 'react';
+import './Footer.css';
+import { Link } from 'react-router-dom';
+
+const Footer = () => {
+ return (
+
+ );
+};
+
+export default Footer;
diff --git a/frontend/src/Components/Header.css b/frontend/src/Components/Header.css
new file mode 100644
index 0000000..73f95d5
--- /dev/null
+++ b/frontend/src/Components/Header.css
@@ -0,0 +1,113 @@
+/* --- Desktop Styles --- */
+.header {
+ background-color: #0c0200;
+ padding: 0px 20px;
+ color: white;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.header-left {
+ display: flex;
+ margin-bottom: 0;
+ align-items: center;
+}
+
+.header-left img {
+ height: 45px;
+ margin: 0;
+}
+
+.header-left h1 {
+ margin: 0;
+ font-size: 1.5rem;
+}
+
+.nav-links {
+ flex: 1;
+ text-align: center;
+}
+
+.nav-links a {
+ margin: 0 10px;
+ color: white;
+ text-decoration: none;
+}
+
+.nav-links a:hover {
+ text-decoration: underline;
+}
+
+.search-bar {
+ flex: 1;
+ text-align: right;
+}
+
+.search-bar input {
+ padding: 6px 10px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+}
+
+/* --- Hamburger menu hidden by default --- */
+.hamburger {
+ display: none;
+ font-size: 2rem;
+ cursor: pointer;
+ color: white;
+}
+
+/* --- Responsive at 640px and below --- */
+@media (max-width: 640px) {
+ .header {
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ padding: 10px 0;
+
+ }
+
+ .header-left {
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 5px;
+ }
+
+ .header-left h1 {
+ font-size: 1.2rem;
+ text-align: center;
+ margin-bottom: 5px;
+ }
+
+ .hamburger {
+ display: block;
+ font-size: 5rem;
+ cursor: pointer;
+ color: white;
+ margin: 10px auto;
+ }
+
+ .nav-links {
+ display: none;
+ flex-direction: column;
+ width: 100%;
+ margin-top: 10px;
+ }
+
+ .nav-links.active {
+ display: flex;
+ }
+
+ .nav-links a {
+ margin: 10px 0;
+ padding: 10px;
+ }
+
+ .search-bar {
+ width: 100%;
+ margin-top: 10px;
+ text-align: center;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/Components/Header.js b/frontend/src/Components/Header.js
index 651ae52..4910b8c 100644
--- a/frontend/src/Components/Header.js
+++ b/frontend/src/Components/Header.js
@@ -1 +1,41 @@
-import React from 'react'
\ No newline at end of file
+import React, { useState } from 'react';
+import './Header.css';
+import { Link } from 'react-router-dom';
+
+const Header = () => {
+ const [menuOpen, setMenuOpen] = useState(false);
+
+ const toggleMenu = () => setMenuOpen(!menuOpen);
+
+ return (
+
+
+

+
+
CrossKey Communication
+
+
+
+ {/* Hamburger icon */}
+
+ ā°
+
+
+ {/* Nav links */}
+
+
+
+
+ );
+};
+
+export default Header;
diff --git a/frontend/src/Components/Home.css b/frontend/src/Components/Home.css
new file mode 100644
index 0000000..c8d6999
--- /dev/null
+++ b/frontend/src/Components/Home.css
@@ -0,0 +1,65 @@
+.slider-layout {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 20px;
+ flex-wrap: wrap;
+ gap: 20px;
+}
+
+.ticker-box-group {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ max-width: 100%;
+}
+
+.ticker-box-group .ticker-slider {
+ height: 150px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 8px;
+}
+
+.main-slider {
+ flex: 2;
+ max-width: 800px;
+}
+.slider-row {
+ display: flex;
+ grid-template-columns: 350px 1fr;
+ align-items: flex-start;
+ gap: 20px; /* space between ticker and slider */
+ padding: 20px;
+ align-items: stretch;
+}
+
+.ticker-box {
+ flex: 1;
+ max-width: 350px;
+ min-width: 280px;
+}
+
+.ticker-box iframe {
+ width: 100%;
+ border-radius: 10px;
+}
+
+/* News section layout */
+.news-section {
+ width: 100%;
+ background-color: #f4f4f4;
+ box-sizing: border-box;
+}
+
+/* Hide the ticker on small screens and stack layout */
+@media (max-width: 768px) {
+ .slider-layout {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .ticker-box {
+ order: -1;
+ margin-bottom: 20px;
+ }
+}
diff --git a/frontend/src/Components/Home.js b/frontend/src/Components/Home.js
new file mode 100644
index 0000000..d3fd445
--- /dev/null
+++ b/frontend/src/Components/Home.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import ImageSlider from './ImageSlider';
+import NewsGrid from './NewsGrid';
+import TickerSlider from './TickerSlider';
+import './Home.css';
+
+const Home = () => {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+export default Home;
diff --git a/frontend/src/Components/ImageSlider.css b/frontend/src/Components/ImageSlider.css
new file mode 100644
index 0000000..18879cf
--- /dev/null
+++ b/frontend/src/Components/ImageSlider.css
@@ -0,0 +1,64 @@
+.image-slider-wrapper {
+ width: 100%;
+ margin: 0 auto;
+ padding: 0;
+}
+
+.image-slider-card {
+ position: relative;
+ cursor: pointer;
+ width: 100%;
+ height: 400px; /* Default height for desktops */
+ overflow: hidden;
+}
+
+.image-slider-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.image-slider-overlay {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ background: rgba(0, 0, 0, 0.6);
+ color: white;
+ padding: 10px 15px;
+ box-sizing: border-box;
+ text-shadow: 1px 1px 4px black;
+ z-index: 2;
+}
+
+.image-slider-title {
+ margin: 0;
+ font-size: 1.3rem;
+ font-weight: bold;
+}
+
+/* š± Responsive Styles */
+@media (max-width: 768px) {
+ .image-slider-card {
+ height: 250px; /* Reduce height on smaller screens */
+ }
+
+ .image-slider-title {
+ font-size: 1rem;
+ }
+
+ .image-slider-overlay {
+ padding: 8px 12px;
+ }
+}
+
+@media (max-width: 480px) {
+ .image-slider-card {
+ height: 200px;
+ }
+
+ .image-slider-title {
+ font-size: 0.95rem;
+ }
+}
diff --git a/frontend/src/Components/ImageSlider.js b/frontend/src/Components/ImageSlider.js
new file mode 100644
index 0000000..c1c2898
--- /dev/null
+++ b/frontend/src/Components/ImageSlider.js
@@ -0,0 +1,77 @@
+import React, { useState, useEffect } from 'react';
+import Slider from 'react-slick';
+import { useNavigate } from 'react-router-dom';
+import "slick-carousel/slick/slick.css";
+import "slick-carousel/slick/slick-theme.css";
+import './ImageSlider.css'; // Make sure this exists
+
+const ImageSlider = () => {
+ const [articles, setArticles] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ fetch('http://localhost:8081/articles')
+ .then((response) => response.json())
+ .then((data) => {
+ const filtered = data.filter(article =>
+ article.thumbnail &&
+ article.thumbnail.startsWith('http') &&
+ !article.thumbnail.endsWith('.svg')
+ );
+ setArticles(filtered.slice(1, 5));
+ setLoading(false);
+ })
+ .catch((error) => {
+ console.error('Error fetching articles:', error);
+ setLoading(false);
+ });
+ }, []);
+
+ const settings = {
+ dots: false,
+ infinite: true,
+ speed: 600,
+ slidesToShow: 1,
+ slidesToScroll: 1,
+ autoplay: true,
+ autoplaySpeed: 3000,
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {articles.map((article, index) => (
+ navigate(`/article/${article.id}`, { state: { article } })}
+ >
+

{ e.target.src = "/placeholder.jpg"; }}
+ alt={article.title}
+ className="image-slider-img"
+ loading="lazy"
+ />
+
+
+ {article.title || "Untitled Article"}
+
+
+
+ ))}
+
+
+ );
+};
+
+export default ImageSlider;
diff --git a/frontend/src/Components/NewsArticle.css b/frontend/src/Components/NewsArticle.css
new file mode 100644
index 0000000..1ca1f7d
--- /dev/null
+++ b/frontend/src/Components/NewsArticle.css
@@ -0,0 +1,58 @@
+.news-article {
+ max-width: 800px;
+ margin: 2rem auto;
+ padding: 1.5rem;
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ font-family: Arial, sans-serif;
+ line-height: 1.6;
+}
+
+.news-article img {
+ width: 100%;
+ height: auto;
+ border-radius: 8px;
+ margin-bottom: 1.5rem;
+ object-fit: cover;
+}
+
+.news-article h1 {
+ font-size: 2rem;
+ margin-bottom: 0.75rem;
+ color: #222;
+}
+
+.news-article-author {
+ font-style: italic;
+ font-size: 0.95rem;
+ color: gray;
+ margin-bottom: 1.2rem;
+}
+
+.news-article-description {
+ font-size: 1.2rem;
+ color: #444;
+ margin-bottom: 1rem;
+}
+
+.news-article-source {
+ font-size: 0.9rem;
+ color: #777;
+ margin-top: 2rem;
+ text-align: right;
+}
+.btn {
+ background-color: #2e2e2e; /* dark grey/black */
+ color: white;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 6px;
+ font-size: 14px;
+ cursor: pointer;
+ margin-bottom: 20px;
+}
+
+.btn:hover {
+ background-color: #444;
+}
\ No newline at end of file
diff --git a/frontend/src/Components/NewsArticle.js b/frontend/src/Components/NewsArticle.js
new file mode 100644
index 0000000..6c0e2e1
--- /dev/null
+++ b/frontend/src/Components/NewsArticle.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
+import './NewsArticle.css';
+import Comments from './Comments';
+import './Footer.css';
+
+function NewsArticle() {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const article = location.state?.article;
+
+ if (!article) {
+ return
No article data.
;
+ }
+
+ // Format the published date, if it exists
+ const formattedDate = article.publishedAt
+ ? new Date(article.publishedAt).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ })
+ : null;
+
+ return (
+
+
+
+ {article.thumbnail && (
+

+ )}
+
+
{article.title}
+
+
+ {article.author ? `By ${article.author}` : 'Unknown Author'}
+ {formattedDate && ` ⢠${formattedDate}`}
+
+
+
{article.articleDescription}
+
{article.articleBody}
+
+ {article.source?.name && (
+
Source: {article.source.name}
+ )}
+
+
+
+ );
+}
+
+export default NewsArticle;
diff --git a/frontend/src/Components/NewsCard.css b/frontend/src/Components/NewsCard.css
index 08281e7..b9f0257 100644
--- a/frontend/src/Components/NewsCard.css
+++ b/frontend/src/Components/NewsCard.css
@@ -44,4 +44,30 @@
font-size: 0.95rem;
color: #555;
margin: 0;
-}
\ No newline at end of file
+}
+.news-card {
+ position: relative;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.favorite-btn {
+ position: absolute;
+ bottom: 8px;
+ right: 8px;
+ background: transparent;
+ border: none;
+ font-size: 20px;
+ cursor: pointer;
+ color: #aaa; /* Outline color for ā */
+ transition: color 0.2s ease;
+}
+
+/* When favorited, show a half black/half yellow star look */
+.favorite-btn.favorited {
+ background: linear-gradient(to right, black 50%, gold 50%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ color: black;
+}
diff --git a/frontend/src/Components/NewsCard.js b/frontend/src/Components/NewsCard.js
index 9f1f5ca..fba9a99 100644
--- a/frontend/src/Components/NewsCard.js
+++ b/frontend/src/Components/NewsCard.js
@@ -1,22 +1,36 @@
-import React from 'react';
+import React, { useState } from 'react';
import './NewsCard.css';
-function NewsCard({ article, onClick }) {
+function NewsCard({ article, onClick, isInitiallyFavorite = false, lockFavorite = false }) {
+ const [isFavorite, setIsFavorite] = useState(isInitiallyFavorite);
+
+ const toggleFavorite = (e) => {
+ e.stopPropagation();
+ if (lockFavorite) return;
+ setIsFavorite(prev => !prev);
+ };
+
return (
-
onClick(article)}>
+
{article.title}
-
{article.description}
+
{article.articleDescription}
+
+
);
}
-export default NewsCard;
\ No newline at end of file
+export default NewsCard;
diff --git a/frontend/src/Components/NewsGrid.css b/frontend/src/Components/NewsGrid.css
index a1bef8c..42d35ad 100644
--- a/frontend/src/Components/NewsGrid.css
+++ b/frontend/src/Components/NewsGrid.css
@@ -3,5 +3,5 @@
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
padding: 3rem; /* adjust if needed */
- background-color: #f4f4f4; /* keep your style */
+ background-color: #f4f4f4;
}
\ No newline at end of file
diff --git a/frontend/src/Components/NewsGrid.js b/frontend/src/Components/NewsGrid.js
index eaea1a2..368c975 100644
--- a/frontend/src/Components/NewsGrid.js
+++ b/frontend/src/Components/NewsGrid.js
@@ -1,24 +1,29 @@
-import React, { useState, useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import NewsCard from './NewsCard';
import './NewsGrid.css';
+import { useNavigate } from 'react-router-dom';
+import Footer from './Footer.css';
-function NewsGrid({ onArticleClick }) {
+
+function NewsGrid() {
const [articles, setArticles] = useState([]);
+ const navigate = useNavigate();
useEffect(() => {
fetch('http://localhost:8081/articles/fetch')
.then(response => response.json())
- .then(data => {
- console.log('Fetched articles:', data);
- setArticles(data);
- })
- .catch(error => console.error('Error fetching articles:', error));
+ .then(data => setArticles(data))
+ .catch(err => console.error('Fetch error:', err));
}, []);
+ const handleClick = (article, index) => {
+ navigate(`/article/${index}`, { state: { article } });
+ };
+
return (
- {Array.isArray(articles) && articles.map((article, index) => (
-
+ {articles.map((article, index) => (
+ handleClick(article, index)} />
))}
);
diff --git a/frontend/src/Components/QrPage.js b/frontend/src/Components/QrPage.js
new file mode 100644
index 0000000..4d06e8b
--- /dev/null
+++ b/frontend/src/Components/QrPage.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+
+const QrPage = () => {
+ return (
+
+
Scan this QR Code
+
+

+
+
+ );
+};
+
+export default QrPage;
\ No newline at end of file
diff --git a/frontend/src/Components/SideBox.css b/frontend/src/Components/SideBox.css
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/Components/SideBox.js b/frontend/src/Components/SideBox.js
new file mode 100644
index 0000000..36ff1a5
--- /dev/null
+++ b/frontend/src/Components/SideBox.js
@@ -0,0 +1,47 @@
+//import React, { useEffect, useState } from 'react';
+//import './SideBox.css';
+//
+//function SideBox({ title }) {
+// const [items, setItems] = useState([]);
+//
+// useEffect(() => {
+// const fetchData = async () => {
+// if (title.includes('Stock')) {
+// const symbols = ['AAPL', 'GOOGL', 'MSFT'];
+// const token = 'd0ho26pr01ql9qu5umh0d0ho26pr01ql9qu5umhg';
+// const requests = symbols.map(symbol => {
+// return fetch(`https://finnhub.io/api/v1/quote?symbol=${symbol}&token=${token}`)
+// .then(res => res.json())
+// .then(data => ({ symbol, price: data.c }));
+// });
+// const results = await Promise.all(requests);
+// setItems(results);
+// } else if (title.includes('NFL')) {
+// const res = await fetch('https://www.thesportsdb.com/api/v1/json/1/eventspastleague.php?id=4391');
+// const json = await res.json();
+// setItems(json.events ? json.events.slice(0, 5) : []);
+// }
+// };
+//
+// fetchData();
+// }, [title]);
+//
+// return (
+//
+//
{title}
+//
+// {items.map((item, index) => (
+// title.includes('Stock') ? (
+// - {item.symbol}: ${item.price}
+// ) : (
+// -
+// {item.strHomeTeam} {item.intHomeScore} - {item.intAwayScore} {item.strAwayTeam}
+//
+// )
+// ))}
+//
+//
+// );
+//}
+//
+//export default SideBox;
diff --git a/frontend/src/Components/TickerBox.js b/frontend/src/Components/TickerBox.js
new file mode 100644
index 0000000..c7fb21b
--- /dev/null
+++ b/frontend/src/Components/TickerBox.js
@@ -0,0 +1,91 @@
+import React, { useEffect, useState } from 'react';
+import Slider from 'react-slick';
+
+const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
+
+const TickerBox = ({ symbols }) => {
+ const [chartImages, setChartImages] = useState([]);
+
+ useEffect(() => {
+ const fetchCharts = async () => {
+ const images = [];
+
+ for (let item of symbols) {
+ try {
+ const res = await fetch(`http://localhost:8081/api/stocks/${item.symbol}/live`);
+ const data = await res.json();
+ const prices = data.prices;
+
+ if (!prices || prices.length === 0) {
+ images.push({
+ src: 'https://via.placeholder.com/300x150?text=No+Data',
+ alt: `${item.label} (${item.symbol}) - No Data`,
+ });
+ await delay(1200);
+ continue;
+ }
+
+ const labels = prices.map((_, i) => `T-${i} min`);
+ const chartUrl = `https://quickchart.io/chart?c=${encodeURIComponent(JSON.stringify({
+ type: 'line',
+ data: {
+ labels: labels.reverse(),
+ datasets: [{
+ label: item.symbol,
+ data: prices.reverse(),
+ fill: false,
+ borderColor: 'blue',
+ borderWidth: 2
+ }]
+ },
+ options: {
+ plugins: { legend: { display: false } }
+ }
+ }))}`;
+
+ images.push({
+ src: chartUrl,
+ alt: `${item.label} (${item.symbol})`
+ });
+
+ } catch (error) {
+ images.push({
+ src: 'https://via.placeholder.com/300x150?text=Error',
+ alt: `${item.label} (${item.symbol}) - Error`
+ });
+ }
+
+ await delay(1200); // wait to avoid API rate limit
+ }
+
+ setChartImages(images);
+ };
+
+ fetchCharts();
+ }, [symbols]);
+
+ const settings = {
+ dots: false,
+ infinite: true,
+ speed: 500,
+ slidesToShow: 1,
+ slidesToScroll: 1,
+ arrows: false,
+ autoplay: true,
+ autoplaySpeed: 8000,
+ };
+
+ return (
+
+
+ {chartImages.map((img, idx) => (
+
+

+
+ ))}
+
+
+ );
+};
+
+export default TickerBox;
diff --git a/frontend/src/Components/TickerSlider.css b/frontend/src/Components/TickerSlider.css
new file mode 100644
index 0000000..8b8a4fc
--- /dev/null
+++ b/frontend/src/Components/TickerSlider.css
@@ -0,0 +1,35 @@
+/* Grid layout for all ticker boxes */
+.ticker-slider-grid {
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ flex-wrap: wrap;
+}
+
+/* Individual ticker card */
+.ticker-box {
+ width: 250px;
+ height: 180px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+}
+
+/* Chart image */
+.ticker-chart {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ display: block;
+}
+
+/* Optional label for symbol */
+.ticker-symbol-label {
+ text-align: right;
+ font-size: 1rem;
+ font-weight: bold;
+ font-family: Arial, sans-serif;
+ margin-bottom: 4px;
+ padding-right: 30px;
+}
diff --git a/frontend/src/Components/TickerSlider.js b/frontend/src/Components/TickerSlider.js
new file mode 100644
index 0000000..639d8e0
--- /dev/null
+++ b/frontend/src/Components/TickerSlider.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import TickerBox from './TickerBox'; // Make sure TickerBox.jsx exists
+import './TickerSlider.css';
+
+const symbols = [
+ { symbol: 'AAPL', label: 'Apple' },
+ { symbol: 'JPM', label: 'JPMorgan Chase' },
+ { symbol: 'JNJ', label: 'Johnson & Johnson' },
+ { symbol: 'XOM', label: 'ExxonMobil' },
+ { symbol: 'NKE', label: 'Nike' },
+ { symbol: 'BA', label: 'Boeing' }
+];
+
+// Helper to divide symbols into groups of 2
+const groupSymbols = (symbols, groupSize) => {
+ const result = [];
+ for (let i = 0; i < symbols.length; i += groupSize) {
+ result.push(symbols.slice(i, i + groupSize));
+ }
+ return result;
+};
+
+const TickerSlider = () => {
+const groupedSymbols = groupSymbols(symbols, 2).slice(0, 3);
+
+ return (
+
+ {groupedSymbols.map((pair, index) => (
+
+ ))}
+
+ );
+};
+
+export default TickerSlider;
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/Articles.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/Articles.class
index 17763d7..c20b44f 100644
Binary files a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/Articles.class and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/Articles.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/ArticlesController.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/ArticlesController.class
index 63ce085..46382f8 100644
Binary files a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/ArticlesController.class and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/ArticlesController.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/ArticlesSource.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/ArticlesSource.class
index a93605a..0ee1a09 100644
Binary files a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/ArticlesSource.class and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Articles/ArticlesSource.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/CkcApplication.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/CkcApplication.class
new file mode 100644
index 0000000..db3ca89
Binary files /dev/null and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/CkcApplication.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/Comments.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/Comments.class
index 610b449..31dc660 100644
Binary files a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/Comments.class and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/Comments.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/CommentsController.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/CommentsController.class
index a9ba7ab..bf7152e 100644
Binary files a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/CommentsController.class and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/CommentsController.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/CommentsDTO.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/CommentsDTO.class
new file mode 100644
index 0000000..9318d3a
Binary files /dev/null and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/Comments/CommentsDTO.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/UserConfig.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/UserConfig.class
new file mode 100644
index 0000000..60e75fc
Binary files /dev/null and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/UserConfig.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/UserController.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/UserController.class
new file mode 100644
index 0000000..f72b169
Binary files /dev/null and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/UserController.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/UserRepository.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/UserRepository.class
index 0b22ff7..13f76de 100644
Binary files a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/UserRepository.class and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/UserRepository.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/Users.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/Users.class
index bb2da47..692d91f 100644
Binary files a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/Users.class and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/User/Users.class differ
diff --git a/out/production/BrandonGrahamDay/rocks/zipcode/CKC/WebConfig.class b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/WebConfig.class
new file mode 100644
index 0000000..8a69555
Binary files /dev/null and b/out/production/BrandonGrahamDay/rocks/zipcode/CKC/WebConfig.class differ