initial redhead implementation.
This commit is contained in:
commit
988a5eaa53
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Created by venv; see https://docs.python.org/3/library/venv.html
|
||||
|
||||
bin/
|
||||
include/
|
||||
lib/
|
||||
|
||||
__pycache__
|
||||
pyvenv.cfg
|
||||
64
README.md
Normal file
64
README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Reddit Clone
|
||||
|
||||
A responsive Reddit-like application with infinite scroll functionality that displays the latest Reddit articles.
|
||||
|
||||
## Features
|
||||
|
||||
- **Infinite Scroll**: Automatically loads more posts as you scroll down
|
||||
- **Responsive Design**: Works well on both desktop and mobile devices
|
||||
- **Image Preview**: Each post displays a picture frame with a placeholder image
|
||||
- **Modern UI**: Clean, attractive interface with hover effects and smooth transitions
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── index.html # Main HTML structure
|
||||
├── styles.css # Responsive styling
|
||||
└── app.js # JavaScript logic for posts and infinite scroll
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The application loads the first 10 Reddit posts when the page is initially loaded
|
||||
2. As users scroll down, it automatically loads more posts (10 at a time)
|
||||
3. Uses Intersection Observer API for efficient infinite scrolling
|
||||
4. Falls back to manual scroll detection for older browsers
|
||||
5. Each post displays:
|
||||
- A title
|
||||
- An image frame
|
||||
- A link to the original Reddit post
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- **Responsive Design**: Uses CSS Grid for adaptive layouts that work on mobile and desktop
|
||||
- **Performance**: Lazy loading of images and efficient scroll handling
|
||||
- **Mock Data**: Currently uses mock data for demonstration purposes (will be replaced with real API calls)
|
||||
- **User Experience**: Loading indicators and smooth animations
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Open `index.html` in a web browser
|
||||
2. Scroll down to see more posts load automatically
|
||||
3. Click on post titles to view them on Reddit
|
||||
|
||||
## Customization
|
||||
|
||||
To connect to a real Reddit API:
|
||||
1. Replace the mock data in `app.js` with actual API calls
|
||||
2. Update the `fetchRedditPosts()` and `fetchMorePosts()` functions to fetch from Reddit's API
|
||||
3. Adjust styling in `styles.css` to match desired aesthetics
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- HTML5
|
||||
- CSS3 (Grid, Flexbox, Media Queries)
|
||||
- JavaScript ES6+
|
||||
- Intersection Observer API for efficient scrolling
|
||||
|
||||
## Responsive Design
|
||||
|
||||
The application is fully responsive and will adapt to:
|
||||
- Desktop: Grid layout with 2-3 columns based on screen width
|
||||
- Tablet: Reduced grid columns
|
||||
- Mobile: Single column layout with appropriate spacing
|
||||
99
src/server/README.md
Normal file
99
src/server/README.md
Normal file
@ -0,0 +1,99 @@
|
||||
# Reddit Clone API Server
|
||||
|
||||
A Python Flask server that serves data for the Reddit clone application.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `GET /posts` - Get first 10 posts for initial load
|
||||
- `GET /posts/more?count=N` - Get next 10 posts, starting from index N
|
||||
- `GET /posts/total` - Get total number of posts available
|
||||
- `GET /` - Server status endpoint
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
1. Install Python 3 if you don't have it already
|
||||
2. Navigate to the server directory:
|
||||
```bash
|
||||
cd /Users/jariancottingham/Projects/redhead/src/server
|
||||
```
|
||||
3. Create a virtual environment (recommended):
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows use: venv\Scripts\activate
|
||||
```
|
||||
4. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
5. Run the server:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
6. The server will be available at http://localhost:5000
|
||||
|
||||
## Data Format
|
||||
|
||||
The server returns JSON data in the same format as used by the frontend:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"title": "This is a sample Reddit post title",
|
||||
"url": "https://example.com/sample-post",
|
||||
"image": "https://picsum.photos/400/300?random=1"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Usage with Frontend
|
||||
|
||||
Update the frontend's `fetchRedditPosts()` and `fetchMorePosts()` functions to call your server endpoints instead of using mock data:
|
||||
|
||||
```javascript
|
||||
// Replace URLs with your server endpoints
|
||||
function fetchRedditPosts() {
|
||||
return fetch('/posts')
|
||||
.then(response => response.json())
|
||||
}
|
||||
|
||||
function fetchMorePosts(currentCount) {
|
||||
return fetch(`/posts/more?count=${currentCount}`)
|
||||
.then(response => response.json())
|
||||
}
|
||||
```
|
||||
|
||||
## Example Data
|
||||
|
||||
The sample data is stored in `sample_posts.json`. You can replace this with your own data, or connect to a real Reddit API.
|
||||
|
||||
To add your own posts:
|
||||
1. Replace the content of `sample_posts.json` with your data
|
||||
2. Each post should follow this structure:
|
||||
```json
|
||||
{
|
||||
"id": "post_id",
|
||||
"title": "Post title",
|
||||
"url": "https://example.com/post-url",
|
||||
"image": "https://example.com/image-url"
|
||||
}
|
||||
```
|
||||
|
||||
## Running the Server
|
||||
|
||||
After installation, run the server with:
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
The server will start on port 5000 and listen for connections from localhost.
|
||||
|
||||
## Testing Endpoints
|
||||
|
||||
You can test the endpoints directly using curl or a browser:
|
||||
|
||||
- `http://localhost:5000/posts` - Get initial posts
|
||||
- `http://localhost:5000/posts/more?count=10` - Get more posts starting from index 10
|
||||
- `http://localhost:5000/posts/total` - Get total post count
|
||||
69
src/server/app.py
Normal file
69
src/server/app.py
Normal file
@ -0,0 +1,69 @@
|
||||
from flask import Flask, jsonify, request
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Get the directory where this script is located
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Import our archive parser
|
||||
from parse_archive import ArchiveParser
|
||||
|
||||
# Initialize the archive parser
|
||||
archive_parser = ArchiveParser()
|
||||
|
||||
|
||||
@app.route("/posts", methods=["GET"])
|
||||
def get_initial_posts():
|
||||
"""Get the first 10 posts for initial load"""
|
||||
try:
|
||||
# Use archive parser to fetch posts
|
||||
posts = archive_parser.get_posts(count=10, start_index=0)
|
||||
return jsonify(posts)
|
||||
except Exception as e:
|
||||
return jsonify({"error": "Failed to fetch posts"}), 500
|
||||
|
||||
|
||||
@app.route("/posts/more", methods=["GET"])
|
||||
def get_more_posts():
|
||||
"""Get next 10 posts based on the count parameter"""
|
||||
try:
|
||||
# Get the count parameter from query string, default to 10
|
||||
count = request.args.get("count", 10, type=int)
|
||||
|
||||
# Use archive parser to fetch posts
|
||||
posts = archive_parser.get_posts(count=10, start_index=count)
|
||||
|
||||
return jsonify(posts)
|
||||
except Exception as e:
|
||||
return jsonify({"error": "Failed to fetch more posts"}), 500
|
||||
|
||||
|
||||
@app.route("/posts/total", methods=["GET"])
|
||||
def get_total_posts():
|
||||
"""Get the total number of posts available"""
|
||||
try:
|
||||
# Get total from archive parser
|
||||
total = archive_parser.get_total_posts()
|
||||
return jsonify({"total": total})
|
||||
except Exception as e:
|
||||
return jsonify({"error": "Failed to fetch total count"}), 500
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
"""Home endpoint to verify server is running"""
|
||||
return jsonify(
|
||||
{
|
||||
"message": "Reddit Clone API Server is running",
|
||||
"endpoints": {
|
||||
"GET /posts": "Get first 10 posts",
|
||||
"GET /posts/more?count=N": "Get next 10 posts, starting from index N",
|
||||
"GET /posts/total": "Get total number of posts",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, host="0.0.0.0", port=6006)
|
||||
112
src/server/parse_archive.py
Normal file
112
src/server/parse_archive.py
Normal file
@ -0,0 +1,112 @@
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class ArchiveParser:
|
||||
def __init__(self):
|
||||
"""Initialize the ArchiveParser with archive_dir from environment variable."""
|
||||
self.archive_dir = os.environ.get("ARCHIVE_DIR", "/default/archive/path")
|
||||
|
||||
def get_posts(self, count: int, start_index: int = 0) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get posts from the archive using efficient shell commands.
|
||||
|
||||
For pagination:
|
||||
- First call: get_posts(count=10, start_index=0) gets first 10 latest posts
|
||||
- Next call: get_posts(count=10, start_index=N) gets next 10 posts after index N
|
||||
|
||||
Args:
|
||||
count (int): Number of posts to retrieve
|
||||
start_index (int): Starting index for retrieving posts
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: List of post dictionaries
|
||||
"""
|
||||
try:
|
||||
# Use ls with tail and head commands for efficient pagination
|
||||
# Get all directories, skip the first start_index, then get count number of entries
|
||||
cmd = f"ls -1 {self.archive_dir} | tail -n +{start_index + 1} | head -n {count}"
|
||||
print("Running cmd: " + cmd)
|
||||
result = subprocess.run(
|
||||
cmd, shell=True, capture_output=True, text=True, check=True, timeout=30
|
||||
)
|
||||
|
||||
# Parse the output (directory names)
|
||||
directories = [
|
||||
line.strip() for line in result.stdout.split("\n") if line.strip()
|
||||
]
|
||||
|
||||
# Extract posts from each directory
|
||||
posts = []
|
||||
for directory in directories:
|
||||
dir_path = os.path.join(self.archive_dir, directory)
|
||||
posts.extend(self._extract_posts_from_directory(dir_path))
|
||||
|
||||
return posts
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error executing command: {e}")
|
||||
return []
|
||||
except subprocess.TimeoutExpired:
|
||||
print("Command timed out")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Error retrieving posts: {e}")
|
||||
return []
|
||||
|
||||
def _extract_posts_from_directory(
|
||||
self, directory_path: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Extract posts from a single directory."""
|
||||
posts = []
|
||||
|
||||
# Check if there's an index.json in this directory
|
||||
index_file_path = os.path.join(directory_path, "index.json")
|
||||
if os.path.exists(index_file_path):
|
||||
try:
|
||||
with open(index_file_path, "r") as f:
|
||||
data = json.load(f)
|
||||
# Handle both single post and list of posts in the index.json
|
||||
if isinstance(data, list):
|
||||
posts.extend(data)
|
||||
else:
|
||||
posts.append(data)
|
||||
except Exception as e:
|
||||
print(f"Error reading index.json at {index_file_path}: {e}")
|
||||
else:
|
||||
# If no index.json, try to get individual post files (backward compatibility)
|
||||
try:
|
||||
for root, dirs, files in os.walk(directory_path):
|
||||
for file in files:
|
||||
if file.endswith(".json"):
|
||||
file_path = os.path.join(root, file)
|
||||
with open(file_path, "r") as f:
|
||||
data = json.load(f)
|
||||
posts.append(data)
|
||||
except Exception as e:
|
||||
print(f"Error reading post files: {e}")
|
||||
|
||||
return posts
|
||||
|
||||
def get_total_posts(self) -> int:
|
||||
"""Get the total number of directories (post series) in the archive."""
|
||||
try:
|
||||
# Use ls with wc -l to count directories efficiently
|
||||
cmd = f"ls -1 {self.archive_dir} | wc -l"
|
||||
|
||||
result = subprocess.run(
|
||||
cmd, shell=True, capture_output=True, text=True, check=True, timeout=30
|
||||
)
|
||||
|
||||
return int(result.stdout.strip())
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error executing count command: {e}")
|
||||
return 0
|
||||
except subprocess.TimeoutExpired:
|
||||
print("Count command timed out")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error getting total posts: {e}")
|
||||
return 0
|
||||
1
src/server/requirements.txt
Normal file
1
src/server/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
Flask==2.3.3
|
||||
122
src/server/sample_posts.json
Normal file
122
src/server/sample_posts.json
Normal file
@ -0,0 +1,122 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"title": "This is a sample Reddit post title",
|
||||
"url": "https://example.com/sample-post",
|
||||
"image": "https://picsum.photos/400/300?random=1"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"title": "Another interesting Reddit post about technology",
|
||||
"url": "https://example.com/tech-post",
|
||||
"image": "https://picsum.photos/400/300?random=2"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"title": "Amazing landscape photography from nature",
|
||||
"url": "https://example.com/nature-photography",
|
||||
"image": "https://picsum.photos/400/300?random=3"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"title": "How to build a React application from scratch",
|
||||
"url": "https://example.com/react-guide",
|
||||
"image": "https://picsum.photos/400/300?random=4"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"title": "The future of artificial intelligence in 2023",
|
||||
"url": "https://example.com/ai-future",
|
||||
"image": "https://picsum.photos/400/300?random=5"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"title": "Delicious recipes for vegetarian meals",
|
||||
"url": "https://example.com/vegetarian-recipes",
|
||||
"image": "https://picsum.photos/400/300?random=6"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"title": "Top programming languages to learn in 2023",
|
||||
"url": "https://example.com/programming-languages",
|
||||
"image": "https://picsum.photos/400/300?random=7"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"title": "Space exploration updates from NASA",
|
||||
"url": "https://example.com/space-exploration",
|
||||
"image": "https://picsum.photos/400/300?random=8"
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"title": "Tips for improving your sleep quality",
|
||||
"url": "https://example.com/sleep-tips",
|
||||
"image": "https://picsum.photos/400/300?random=9"
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"title": "Latest trends in web development",
|
||||
"url": "https://example.com/web-development-trends",
|
||||
"image": "https://picsum.photos/400/300?random=10"
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"title": "Understanding blockchain technology and its applications",
|
||||
"url": "https://example.com/blockchain-guide",
|
||||
"image": "https://picsum.photos/400/300?random=11"
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"title": "The health benefits of regular exercise",
|
||||
"url": "https://example.com/health-exercise",
|
||||
"image": "https://picsum.photos/400/300?random=12"
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"title": "Exploring the world's most ancient civilizations",
|
||||
"url": "https://example.com/ancient-civilizations",
|
||||
"image": "https://picsum.photos/400/300?random=13"
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"title": "Sustainable living practices for modern homes",
|
||||
"url": "https://example.com/sustainable-living",
|
||||
"image": "https://picsum.photos/400/300?random=14"
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
"title": "The art of coffee brewing: A beginner's guide",
|
||||
"url": "https://example.com/coffee-brewing",
|
||||
"image": "https://picsum.photos/400/300?random=15"
|
||||
},
|
||||
{
|
||||
"id": "16",
|
||||
"title": "Mental health awareness and self-care tips",
|
||||
"url": "https://example.com/mental-health",
|
||||
"image": "https://picsum.photos/400/300?random=16"
|
||||
},
|
||||
{
|
||||
"id": "17",
|
||||
"title": "The rise of renewable energy sources worldwide",
|
||||
"url": "https://example.com/renewable-energy",
|
||||
"image": "https://picsum.photos/400/300?random=17"
|
||||
},
|
||||
{
|
||||
"id": "18",
|
||||
"title": "Travel photography techniques for stunning landscapes",
|
||||
"url": "https://example.com/travel-photography",
|
||||
"image": "https://picsum.photos/400/300?random=18"
|
||||
},
|
||||
{
|
||||
"id": "19",
|
||||
"title": "Building meaningful relationships in the digital age",
|
||||
"url": "https://example.com/digital-relationships",
|
||||
"image": "https://picsum.photos/400/300?random=19"
|
||||
},
|
||||
{
|
||||
"id": "20",
|
||||
"title": "The impact of social media on modern society",
|
||||
"url": "https://example.com/social-media-impact",
|
||||
"image": "https://picsum.photos/400/300?random=20"
|
||||
}
|
||||
]
|
||||
22
src/server/test_parse_archive.py
Normal file
22
src/server/test_parse_archive.py
Normal file
@ -0,0 +1,22 @@
|
||||
import os
|
||||
import tempfile
|
||||
import json
|
||||
from parse_archive import ArchiveParser
|
||||
|
||||
|
||||
def test1():
|
||||
parser = ArchiveParser()
|
||||
posts = parser.get_posts(10)
|
||||
for p in posts:
|
||||
print(p)
|
||||
|
||||
|
||||
def test2():
|
||||
parser = ArchiveParser()
|
||||
posts = parser.get_posts(10, 10)
|
||||
for p in posts:
|
||||
print(p)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test1()
|
||||
250
src/website/app.js
Normal file
250
src/website/app.js
Normal file
@ -0,0 +1,250 @@
|
||||
// Mock Reddit data for demonstration purposes - in real app this would come from an API
|
||||
const mockRedditPosts = [
|
||||
{
|
||||
id: "1",
|
||||
title: "This is a sample Reddit post title",
|
||||
url: "https://example.com/sample-post",
|
||||
image: "https://picsum.photos/400/300?random=1",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Another interesting Reddit post about technology",
|
||||
url: "https://example.com/tech-post",
|
||||
image: "https://picsum.photos/400/300?random=2",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Amazing landscape photography from nature",
|
||||
url: "https://example.com/nature-photography",
|
||||
image: "https://picsum.photos/400/300?random=3",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "How to build a React application from scratch",
|
||||
url: "https://example.com/react-guide",
|
||||
image: "https://picsum.photos/400/300?random=4",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "The future of artificial intelligence in 2023",
|
||||
url: "https://example.com/ai-future",
|
||||
image: "https://picsum.photos/400/300?random=5",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
title: "Delicious recipes for vegetarian meals",
|
||||
url: "https://example.com/vegetarian-recipes",
|
||||
image: "https://picsum.photos/400/300?random=6",
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
title: "Top programming languages to learn in 2023",
|
||||
url: "https://example.com/programming-languages",
|
||||
image: "https://picsum.photos/400/300?random=7",
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
title: "Space exploration updates from NASA",
|
||||
url: "https://example.com/space-exploration",
|
||||
image: "https://picsum.photos/400/300?random=8",
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
title: "Tips for improving your sleep quality",
|
||||
url: "https://example.com/sleep-tips",
|
||||
image: "https://picsum.photos/400/300?random=9",
|
||||
},
|
||||
{
|
||||
id: "10",
|
||||
title: "Latest trends in web development",
|
||||
url: "https://example.com/web-development-trends",
|
||||
image: "https://picsum.photos/400/300?random=10",
|
||||
},
|
||||
{
|
||||
id: "11",
|
||||
title: "Health benefits of daily exercise",
|
||||
url: "https://example.com/health-exercise",
|
||||
image: "https://picsum.photos/400/300?random=11",
|
||||
},
|
||||
{
|
||||
id: "12",
|
||||
title: "Understanding blockchain technology explained",
|
||||
url: "https://example.com/blockchain-explained",
|
||||
image: "https://picsum.photos/400/300?random=12",
|
||||
},
|
||||
{
|
||||
id: "13",
|
||||
title: "Best travel destinations for summer 2023",
|
||||
url: "https://example.com/travel-destinations",
|
||||
image: "https://picsum.photos/400/300?random=13",
|
||||
},
|
||||
{
|
||||
id: "14",
|
||||
title: "Cooking tips from professional chefs",
|
||||
url: "https://example.com/chef-tips",
|
||||
image: "https://picsum.photos/400/300?random=14",
|
||||
},
|
||||
{
|
||||
id: "15",
|
||||
title: "The history of the internet and its evolution",
|
||||
url: "https://example.com/internet-history",
|
||||
image: "https://picsum.photos/400/300?random=15",
|
||||
},
|
||||
];
|
||||
|
||||
// For the real implementation, this would be replaced with an actual API call
|
||||
function fetchRedditPosts() {
|
||||
return new Promise((resolve) => {
|
||||
// Simulate network delay
|
||||
setTimeout(() => {
|
||||
// Return first 10 posts for initial load
|
||||
resolve(mockRedditPosts.slice(0, 10));
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// For the real implementation, this would be replaced with an actual API call for more posts
|
||||
function fetchMorePosts(currentCount) {
|
||||
return new Promise((resolve) => {
|
||||
// Simulate network delay
|
||||
setTimeout(() => {
|
||||
// Return next 10 posts
|
||||
const start = currentCount;
|
||||
const end = start + 10;
|
||||
resolve(mockRedditPosts.slice(start, end));
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
let isLoading = false;
|
||||
let currentPostCount = 10;
|
||||
|
||||
function createPostElement(post) {
|
||||
const postElement = document.createElement("div");
|
||||
postElement.className = "post";
|
||||
|
||||
// Create the image container
|
||||
const imageContainer = document.createElement("div");
|
||||
imageContainer.className = "post-image-container";
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.src = post.image;
|
||||
image.alt = post.title;
|
||||
image.className = "post-image";
|
||||
image.loading = "lazy";
|
||||
|
||||
imageContainer.appendChild(image);
|
||||
|
||||
// Create the content container
|
||||
const content = document.createElement("div");
|
||||
content.className = "post-content";
|
||||
|
||||
const title = document.createElement("a");
|
||||
title.href = post.url;
|
||||
title.target = "_blank";
|
||||
title.rel = "noopener noreferrer";
|
||||
title.className = "post-title";
|
||||
title.textContent = post.title;
|
||||
|
||||
content.appendChild(title);
|
||||
|
||||
// Assemble the complete post
|
||||
postElement.appendChild(imageContainer);
|
||||
postElement.appendChild(content);
|
||||
|
||||
return postElement;
|
||||
}
|
||||
|
||||
function renderPosts(posts) {
|
||||
const container = document.getElementById("posts-container");
|
||||
|
||||
posts.forEach((post) => {
|
||||
const postElement = createPostElement(post);
|
||||
container.appendChild(postElement);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadInitialPosts() {
|
||||
try {
|
||||
const posts = await fetchRedditPosts();
|
||||
renderPosts(posts);
|
||||
currentPostCount = posts.length;
|
||||
} catch (error) {
|
||||
console.error("Failed to load initial posts:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMorePosts() {
|
||||
if (isLoading || currentPostCount >= mockRedditPosts.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
document.getElementById("loading").classList.remove("hidden");
|
||||
|
||||
try {
|
||||
const morePosts = await fetchMorePosts(currentPostCount);
|
||||
|
||||
if (morePosts.length > 0) {
|
||||
renderPosts(morePosts);
|
||||
currentPostCount += morePosts.length;
|
||||
} else {
|
||||
// No more posts to load
|
||||
document.getElementById("no-more").classList.remove("hidden");
|
||||
document.getElementById("loading").classList.add("hidden");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load more posts:", error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
document.getElementById("loading").classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Infinite scroll implementation
|
||||
function setupInfiniteScroll() {
|
||||
const container = document.getElementById("posts-container");
|
||||
|
||||
// Use Intersection Observer API for efficient infinite scrolling
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (
|
||||
entries[0].isIntersecting &&
|
||||
!isLoading &&
|
||||
currentPostCount < mockRedditPosts.length
|
||||
) {
|
||||
loadMorePosts();
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: "20px",
|
||||
threshold: 0.1,
|
||||
},
|
||||
);
|
||||
|
||||
// Create a sentinel element to observe
|
||||
const sentinel = document.createElement("div");
|
||||
sentinel.id = "sentinel";
|
||||
container.appendChild(sentinel);
|
||||
|
||||
observer.observe(sentinel);
|
||||
}
|
||||
|
||||
// Initialize the app when the DOM is loaded
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
loadInitialPosts();
|
||||
setupInfiniteScroll();
|
||||
|
||||
// Also handle scroll manually for browsers that don't support Intersection Observer
|
||||
window.addEventListener("scroll", () => {
|
||||
if (
|
||||
window.innerHeight + window.scrollY >=
|
||||
document.body.offsetHeight - 1000 &&
|
||||
!isLoading &&
|
||||
currentPostCount < mockRedditPosts.length
|
||||
) {
|
||||
loadMorePosts();
|
||||
}
|
||||
});
|
||||
});
|
||||
32
src/website/index.html
Normal file
32
src/website/index.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reddit Clone</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>Reddit Clone</h1>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<div id="posts-container" class="posts-container">
|
||||
<!-- Posts will be loaded here -->
|
||||
</div>
|
||||
|
||||
<div id="loading" class="loading hidden">
|
||||
Loading more posts...
|
||||
</div>
|
||||
|
||||
<div id="no-more" class="no-more hidden">
|
||||
No more posts to load
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
167
src/website/styles.css
Normal file
167
src/website/styles.css
Normal file
@ -0,0 +1,167 @@
|
||||
/* Reset and base styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 15px 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
text-align: center;
|
||||
font-size: 1.8rem;
|
||||
color: #ff4500;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.posts-container {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.post {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.post:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.post-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 15px;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.post-title:hover {
|
||||
color: #ff4500;
|
||||
}
|
||||
|
||||
.post-image-container {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
overflow: hidden;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.post-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.post:hover .post-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
color: #999;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.post-image-container {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.post-image-container {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Grid layout for desktop */
|
||||
@media (min-width: 769px) {
|
||||
.posts-container {
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet view */
|
||||
@media (min-width: 481px) and (max-width: 768px) {
|
||||
.posts-container {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user