Compare commits
2 Commits
2c69edd489
...
ccdfc21f43
Author | SHA1 | Date |
---|---|---|
Oliver Schütz | ccdfc21f43 | |
Oliver Schütz | 0a4af50d08 |
|
@ -22,3 +22,9 @@
|
|||
### Layout
|
||||
|
||||
### Deployment
|
||||
|
||||
|
||||
|
||||
## Hilfsmittel
|
||||
- ChatGPT
|
||||
- https://chatgpt.com/share/671d735f-8f40-8006-a195-1834c70412df
|
||||
|
|
BIN
out/main.pdf
BIN
out/main.pdf
Binary file not shown.
|
@ -1,61 +1,134 @@
|
|||
# course_content_extractor.py
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
import shutil
|
||||
import tempfile
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
import unicodedata
|
||||
import logging
|
||||
|
||||
|
||||
class CourseContentExtractor:
|
||||
def __init__(self, download_dir, output_dir=None):
|
||||
def __init__(self, download_dir, root_dir):
|
||||
"""
|
||||
Initialize the CourseContentExtractor.
|
||||
|
||||
:param download_dir: Directory where ZIP files are downloaded
|
||||
:param root_dir: Root directory for organizing study materials
|
||||
"""
|
||||
self.download_dir = download_dir
|
||||
self.output_dir = output_dir or os.path.join(os.getcwd(), 'data')
|
||||
self.root_dir = root_dir # Read from environment variable
|
||||
|
||||
def extract_contents(self):
|
||||
# Ensure output_dir exists
|
||||
if not os.path.exists(self.output_dir):
|
||||
os.makedirs(self.output_dir)
|
||||
def extract_contents(self, courses):
|
||||
"""
|
||||
Extract and organize course contents based on the provided folder structure.
|
||||
|
||||
# Find all ZIP files in download_dir
|
||||
:param courses: List of course dictionaries containing 'Semester' and 'CourseName'
|
||||
"""
|
||||
# Ensure root_dir exists
|
||||
if not os.path.exists(self.root_dir):
|
||||
try:
|
||||
os.makedirs(self.root_dir)
|
||||
logging.info(f"Created root directory: {self.root_dir}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to create root directory '{self.root_dir}': {e}")
|
||||
return
|
||||
|
||||
# Loop through downloaded ZIP files
|
||||
zip_files = [f for f in os.listdir(self.download_dir) if f.endswith('.zip')]
|
||||
for filename in zip_files:
|
||||
zip_path = os.path.join(self.download_dir, filename)
|
||||
base_name = os.path.splitext(filename)[0]
|
||||
|
||||
# Use the base name as the course folder name
|
||||
course_name = base_name
|
||||
# Find the course info matching the ZIP file
|
||||
# Sanitize both course names to ensure matching
|
||||
course_info = next(
|
||||
(course for course in courses if self.sanitize_filename(course['CourseName']) == self.sanitize_filename(base_name)),
|
||||
None
|
||||
)
|
||||
if not course_info:
|
||||
print(f"No matching course found for {base_name}. Skipping.")
|
||||
logging.warning(f"No matching course found for {base_name}. Skipping.")
|
||||
continue
|
||||
|
||||
course_output_dir = os.path.join(self.output_dir, course_name)
|
||||
# Ensure course_output_dir exists
|
||||
if not os.path.exists(course_output_dir):
|
||||
os.makedirs(course_output_dir)
|
||||
# Build the folder structure
|
||||
semester = course_info['Semester']
|
||||
course_name = course_info['CourseName']
|
||||
|
||||
# Create a temporary directory for extraction
|
||||
course_output_dir = os.path.join(
|
||||
self.root_dir,
|
||||
semester,
|
||||
course_name
|
||||
)
|
||||
|
||||
# Create subfolders
|
||||
subfolders = ['Lectures', 'Notes', 'Summary', 'Tasks']
|
||||
for subfolder in subfolders:
|
||||
subfolder_path = os.path.join(course_output_dir, subfolder)
|
||||
try:
|
||||
os.makedirs(subfolder_path, exist_ok=True)
|
||||
logging.info(f"Created subfolder: {subfolder_path}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to create subfolder '{subfolder_path}': {e}")
|
||||
continue
|
||||
|
||||
# Extract and organize files
|
||||
with tempfile.TemporaryDirectory() as temp_extract_dir:
|
||||
# Extract ZIP file to temporary directory
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(temp_extract_dir)
|
||||
# Walk through the extracted files
|
||||
logging.info(f"Extracted ZIP file to temporary directory: {temp_extract_dir}")
|
||||
except zipfile.BadZipFile as e:
|
||||
logging.error(f"Bad ZIP file '{zip_path}': {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to extract ZIP file '{zip_path}': {e}")
|
||||
continue
|
||||
|
||||
for root, dirs, files in os.walk(temp_extract_dir):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
if file.lower().endswith('.pdf'):
|
||||
# Copy PDF files to course_output_dir
|
||||
shutil.copy2(file_path, course_output_dir)
|
||||
dest_folder = os.path.join(course_output_dir, 'Lectures')
|
||||
try:
|
||||
shutil.copy2(file_path, dest_folder)
|
||||
logging.info(f"Copied PDF file to Lectures: {file}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to copy PDF file '{file}' to '{dest_folder}': {e}")
|
||||
elif file.lower().endswith(('.ppt', '.pptx')):
|
||||
# Convert PowerPoint files to PDF
|
||||
self.convert_ppt_to_pdf(file_path, course_output_dir)
|
||||
try:
|
||||
self.convert_ppt_to_pdf(file_path, os.path.join(course_output_dir, 'Lectures'))
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to convert PPT file '{file}': {e}")
|
||||
else:
|
||||
# Skip unwanted file types
|
||||
logging.info(f"Skipped unsupported file type: {file}")
|
||||
|
||||
# Delete the ZIP file after processing
|
||||
try:
|
||||
os.remove(zip_path)
|
||||
print(f"All PDF and PowerPoint files have been extracted to {self.output_dir}")
|
||||
logging.info(f"Deleted ZIP file after extraction: {zip_path}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to delete ZIP file '{zip_path}': {e}")
|
||||
|
||||
print(f"All files have been extracted to {self.root_dir}")
|
||||
logging.info(f"All files have been extracted to {self.root_dir}")
|
||||
|
||||
def convert_ppt_to_pdf(self, ppt_path, output_dir):
|
||||
"""
|
||||
Convert PowerPoint files to PDF using LibreOffice.
|
||||
|
||||
:param ppt_path: Path to the PPT/PPTX file
|
||||
:param output_dir: Directory to save the converted PDF
|
||||
"""
|
||||
try:
|
||||
# Determine the command based on the operating system
|
||||
if sys.platform.startswith('win'):
|
||||
# Windows systems
|
||||
office_executable = 'soffice'
|
||||
office_executable = 'soffice' # Ensure LibreOffice is installed and in PATH
|
||||
else:
|
||||
# Linux and others
|
||||
office_executable = 'libreoffice'
|
||||
|
||||
# Prepare the command to convert PPT/PPTX to PDF using LibreOffice
|
||||
|
@ -68,12 +141,46 @@ class CourseContentExtractor:
|
|||
]
|
||||
# Execute the command
|
||||
subprocess.run(command, check=True)
|
||||
logging.info(f"Converted {os.path.basename(ppt_path)} to PDF.")
|
||||
print(f"Converted {os.path.basename(ppt_path)} to PDF.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Failed to convert {os.path.basename(ppt_path)} to PDF. Error: {e}")
|
||||
print(f"Failed to convert {os.path.basename(ppt_path)} to PDF. Error: {e}")
|
||||
# Optionally, copy the original PPT/PPTX file
|
||||
try:
|
||||
shutil.copy2(ppt_path, output_dir)
|
||||
logging.info(f"Copied original PPT/PPTX to {output_dir}")
|
||||
except Exception as ex:
|
||||
logging.error(f"Failed to copy PPT/PPTX file '{ppt_path}' to '{output_dir}': {ex}")
|
||||
except FileNotFoundError:
|
||||
logging.error(f"{office_executable} is not installed or not found in the system path.")
|
||||
print(f"{office_executable} is not installed or not found in the system path.")
|
||||
# Optionally, copy the original PPT/PPTX file
|
||||
try:
|
||||
shutil.copy2(ppt_path, output_dir)
|
||||
logging.info(f"Copied original PPT/PPTX to {output_dir}")
|
||||
except Exception as ex:
|
||||
logging.error(f"Failed to copy PPT/PPTX file '{ppt_path}' to '{output_dir}': {ex}")
|
||||
|
||||
def sanitize_filename(self, name):
|
||||
"""
|
||||
Sanitize the filename by removing invalid characters, replacing spaces with underscores,
|
||||
and truncating to a maximum length to prevent path issues.
|
||||
|
||||
:param name: Original filename
|
||||
:return: Sanitized filename
|
||||
"""
|
||||
# Normalize unicode characters
|
||||
name = unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore').decode('ASCII')
|
||||
# Remove invalid characters for filenames, including newlines
|
||||
sanitized = re.sub(r'[<>:"/\\|?*\n\r]+', '', name)
|
||||
# Replace spaces with underscores
|
||||
sanitized = re.sub(r'\s+', '_', sanitized)
|
||||
# Remove trailing underscores
|
||||
sanitized = sanitized.rstrip('_')
|
||||
# Truncate to a reasonable length (e.g., 200 characters)
|
||||
MAX_LENGTH = 200
|
||||
if len(sanitized) > MAX_LENGTH:
|
||||
sanitized = sanitized[:MAX_LENGTH]
|
||||
logging.warning(f"Filename truncated to {MAX_LENGTH} characters: '{sanitized}'")
|
||||
return sanitized
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
root_dir: ${STUDY_MATERIAL_ROOT_DIR} # Replace with the actual environment variable pointing to your root directory
|
||||
|
||||
<study_program>: # Placeholder for the root of your study program (e.g., Computational_and_Data_Science)
|
||||
<semester>: # The current semester (e.g., HS24)
|
||||
<course_name>: # The course identifier and title (e.g., cds-201_Programmierung und Prompt Engineering)
|
||||
Lectures: [] # Folder for lecture materials such as PDFs or recordings (relative to the user-specified root path, e.g., <root_path>/Computational_and_Data_Science/HS24/cds-201_Programmierung und Prompt Engineering/Lectures)
|
||||
Notes: [] # Folder for lecture or self-study notes (relative to the user-specified root path, e.g., <root_path>/Computational_and_Data_Science/HS24/cds-201_Programmierung und Prompt Engineering/Notes)
|
||||
Summary: [] # Folder for summarized notes or cheat sheets (relative to the user-specified root path, e.g., <root_path>/Computational_and_Data_Science/HS24/cds-201_Programmierung und Prompt Engineering/Summary)
|
||||
Tasks: [] # Folder where the user can make a coding project
|
|
@ -1,41 +1,76 @@
|
|||
# main.py
|
||||
# update_study_material.py
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from moodle_downloader import MoodleDownloader
|
||||
from course_content_extractor import CourseContentExtractor
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
filename='moodle_downloader.log',
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Get credentials from environment variables
|
||||
USERNAME = os.getenv('MOODLE_USERNAME')
|
||||
PASSWORD = os.getenv('MOODLE_PASSWORD')
|
||||
def main():
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG, # Changed from INFO to DEBUG for detailed logs
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
if not USERNAME or not PASSWORD:
|
||||
print("Please set the MOODLE_USERNAME and MOODLE_PASSWORD environment variables.")
|
||||
exit(1)
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
root_dir = os.getenv('STUDY_MATERIAL_ROOT_DIR')
|
||||
if not root_dir:
|
||||
print("Please set the STUDY_MATERIAL_ROOT_DIR environment variable.")
|
||||
logging.error("STUDY_MATERIAL_ROOT_DIR environment variable not set.")
|
||||
return
|
||||
|
||||
# Create an instance of MoodleDownloader
|
||||
downloader = MoodleDownloader(USERNAME, PASSWORD, headless=True)
|
||||
# Check if root_dir exists and is a directory
|
||||
if not os.path.isdir(root_dir):
|
||||
print(f"The specified STUDY_MATERIAL_ROOT_DIR does not exist or is not a directory: {root_dir}")
|
||||
logging.error(f"Invalid STUDY_MATERIAL_ROOT_DIR: {root_dir}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Login to Moodle
|
||||
# Treat root_dir as the study_program folder
|
||||
study_program = os.path.basename(os.path.normpath(root_dir))
|
||||
logging.info(f"Using root_dir as the study_program: {study_program}")
|
||||
|
||||
# Use system temporary directory for downloads
|
||||
with tempfile.TemporaryDirectory() as download_dir:
|
||||
logging.info(f"Using temporary download directory: {download_dir}")
|
||||
|
||||
# Load credentials from environment variables
|
||||
username = os.getenv('MOODLE_USERNAME')
|
||||
password = os.getenv('MOODLE_PASSWORD')
|
||||
|
||||
if not username or not password:
|
||||
print("Please set your Moodle credentials in environment variables.")
|
||||
logging.error("Moodle credentials not set in environment variables.")
|
||||
return
|
||||
|
||||
# Initialize downloader
|
||||
downloader = MoodleDownloader(username, password, download_dir=download_dir, headless=True)
|
||||
try:
|
||||
downloader.login()
|
||||
|
||||
# Retrieve courses
|
||||
downloader.get_courses()
|
||||
|
||||
# Download all courses
|
||||
downloader.download_all_courses()
|
||||
|
||||
# Extract course contents using the updated class
|
||||
extractor = CourseContentExtractor(downloader.download_dir)
|
||||
extractor.extract_contents()
|
||||
finally:
|
||||
# Close the browser
|
||||
finally:
|
||||
downloader.close()
|
||||
|
||||
# Assign study_program to each course
|
||||
for course in downloader.courses:
|
||||
course['StudyProgram'] = study_program
|
||||
|
||||
# Initialize extractor
|
||||
extractor = CourseContentExtractor(download_dir=download_dir, root_dir=root_dir)
|
||||
extractor.extract_contents(downloader.courses)
|
||||
|
||||
# Temporary directory is automatically cleaned up here
|
||||
logging.info("Temporary download directory has been cleaned up.")
|
||||
print("Study materials have been updated successfully.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# moodle_downloader.py
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
import unicodedata
|
||||
|
@ -13,20 +14,21 @@ from selenium.webdriver.support import expected_conditions as EC
|
|||
from selenium.common.exceptions import TimeoutException
|
||||
from selenium.webdriver.chrome.service import Service as ChromeService
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
import tempfile
|
||||
|
||||
|
||||
class MoodleDownloader:
|
||||
def __init__(self, username, password, download_dir=None, headless=False):
|
||||
def __init__(self, username, password, download_dir, headless=False):
|
||||
"""
|
||||
Initialize the MoodleDownloader.
|
||||
|
||||
:param username: Moodle username
|
||||
:param password: Moodle password
|
||||
:param download_dir: Directory to download ZIP files
|
||||
:param headless: Run browser in headless mode
|
||||
"""
|
||||
self.username = username
|
||||
self.password = password
|
||||
if download_dir:
|
||||
self.download_dir = download_dir
|
||||
self.cleanup_download_dir = False
|
||||
else:
|
||||
# Create a unique temporary directory
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.download_dir = self.temp_dir.name
|
||||
self.cleanup_download_dir = True
|
||||
self.download_dir = download_dir # Set externally to use system temp
|
||||
self.headless = headless
|
||||
self.driver = None
|
||||
self.courses = []
|
||||
|
@ -34,7 +36,9 @@ class MoodleDownloader:
|
|||
self.MY_COURSES_URL = 'https://moodle.fhgr.ch/my/courses.php'
|
||||
|
||||
def setup_driver(self):
|
||||
# Set up Chrome options
|
||||
"""
|
||||
Set up the Selenium WebDriver with Chrome options.
|
||||
"""
|
||||
chrome_options = Options()
|
||||
if self.headless:
|
||||
chrome_options.add_argument('--headless') # Headless mode
|
||||
|
@ -58,6 +62,9 @@ class MoodleDownloader:
|
|||
self.driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options)
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
Log in to Moodle.
|
||||
"""
|
||||
self.setup_driver()
|
||||
driver = self.driver
|
||||
try:
|
||||
|
@ -118,6 +125,9 @@ class MoodleDownloader:
|
|||
raise e
|
||||
|
||||
def get_courses(self):
|
||||
"""
|
||||
Retrieve the list of courses from Moodle.
|
||||
"""
|
||||
driver = self.driver
|
||||
try:
|
||||
# Navigate to "My Courses" page
|
||||
|
@ -140,27 +150,34 @@ class MoodleDownloader:
|
|||
existing_urls = set()
|
||||
for coursename_element in course_elements:
|
||||
try:
|
||||
# Get the text content
|
||||
full_text = coursename_element.text.strip()
|
||||
lines = [line.strip() for line in full_text.split('\n') if line.strip()]
|
||||
# Remove duplicates
|
||||
unique_lines = list(dict.fromkeys(lines))
|
||||
# Assume the last line is the actual course name
|
||||
course_name = unique_lines[-1]
|
||||
# Extract course name from the nested span
|
||||
course_name_element = coursename_element.find_element(By.CSS_SELECTOR, 'span.multiline span[aria-hidden="true"]')
|
||||
course_title = course_name_element.text.strip()
|
||||
logging.debug(f"Course title extracted: '{course_title}'")
|
||||
|
||||
# Extract course code and term
|
||||
short_name = self.extract_course_code_and_term(course_name)
|
||||
# Extract semester from the sibling div
|
||||
parent_div = coursename_element.find_element(By.XPATH, '..') # Navigate to parent div
|
||||
category_span = parent_div.find_element(By.CSS_SELECTOR, 'span.categoryname.text-truncate')
|
||||
semester = category_span.text.strip()
|
||||
logging.debug(f"Semester extracted: '{semester}'")
|
||||
|
||||
# Extract course info
|
||||
course_info = self.extract_course_info(course_title)
|
||||
|
||||
course_url = coursename_element.get_attribute('href')
|
||||
|
||||
# Check for duplicates
|
||||
if course_url in existing_urls:
|
||||
logging.info(f"Duplicate course found: {short_name} - {course_url}")
|
||||
logging.info(f"Duplicate course found: {course_info['course_name']} - {course_url}")
|
||||
continue
|
||||
existing_urls.add(course_url)
|
||||
|
||||
self.courses.append({'CourseName': short_name, 'URL': course_url})
|
||||
logging.info(f"Course found: {short_name} - {course_url}")
|
||||
self.courses.append({
|
||||
'Semester': self.sanitize_semester(semester),
|
||||
'CourseName': course_info['course_name'],
|
||||
'URL': course_url
|
||||
})
|
||||
logging.info(f"Course found: {course_info['course_name']} - {course_url}")
|
||||
except Exception as e:
|
||||
logging.warning(f"Error extracting course: {e}")
|
||||
continue
|
||||
|
@ -172,31 +189,46 @@ class MoodleDownloader:
|
|||
logging.error("An error occurred while retrieving courses.", exc_info=True)
|
||||
raise e
|
||||
|
||||
def extract_course_code_and_term(self, course_name):
|
||||
# Regular expression to match course code and term
|
||||
# Example course name: 'Mathematik I (cds-401) HS24'
|
||||
pattern = r'\(([^)]+)\)\s+(\w+\d*)'
|
||||
match = re.search(pattern, course_name)
|
||||
if match:
|
||||
course_code = match.group(1)
|
||||
term = match.group(2)
|
||||
# Sanitize and return
|
||||
return f"{self.sanitize_filename(course_code)}_{self.sanitize_filename(term)}"
|
||||
else:
|
||||
# If pattern doesn't match, return sanitized course name
|
||||
return self.sanitize_filename(course_name)
|
||||
def sanitize_semester(self, semester):
|
||||
"""
|
||||
Sanitize the semester name by replacing spaces with underscores and removing trailing underscores.
|
||||
|
||||
def sanitize_filename(self, name):
|
||||
# Normalize unicode characters
|
||||
name = unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore').decode('ASCII')
|
||||
# Remove invalid characters for filenames, including newlines
|
||||
sanitized = re.sub(r'[<>:"/\\|?*\n\r]+', '', name)
|
||||
# Replace spaces and other problematic characters with underscores
|
||||
sanitized = re.sub(r'[\s]+', '_', sanitized)
|
||||
# Truncate to a reasonable length (e.g., 100 characters)
|
||||
return sanitized[:100]
|
||||
:param semester: Original semester string
|
||||
:return: Sanitized semester string
|
||||
"""
|
||||
sanitized = re.sub(r'\s+', '_', semester).strip('_')
|
||||
logging.debug(f"Sanitized semester: '{sanitized}'")
|
||||
return sanitized
|
||||
|
||||
def extract_course_info(self, course_title):
|
||||
"""
|
||||
Extract course information from the course title.
|
||||
|
||||
:param course_title: Full course title string (e.g., 'Algorithmen und Datenstrukturen (cds-203) HS24')
|
||||
:return: Dictionary with 'course_name'
|
||||
"""
|
||||
# Remove the semester from the course title
|
||||
# Example: 'Algorithmen und Datenstrukturen (cds-203) HS24' -> 'Algorithmen und Datenstrukturen (cds-203)'
|
||||
pattern = r'^(.*?)\s*\(([^)]+)\)\s*\w+\d*$'
|
||||
match = re.search(pattern, course_title)
|
||||
if match:
|
||||
course_full_name = match.group(1).strip()
|
||||
course_code = match.group(2).strip()
|
||||
course_name = f"{course_full_name} ({course_code})"
|
||||
return {
|
||||
'course_name': course_name
|
||||
}
|
||||
else:
|
||||
# Handle cases where the pattern doesn't match
|
||||
sanitized_title = self.sanitize_filename(course_title)
|
||||
return {
|
||||
'course_name': sanitized_title
|
||||
}
|
||||
|
||||
def download_all_courses(self):
|
||||
"""
|
||||
Download all courses as ZIP files.
|
||||
"""
|
||||
if not self.courses:
|
||||
logging.warning("No courses to download.")
|
||||
return
|
||||
|
@ -206,6 +238,7 @@ class MoodleDownloader:
|
|||
# Ensure the download directory exists
|
||||
if not os.path.exists(self.download_dir):
|
||||
os.makedirs(self.download_dir)
|
||||
logging.info(f"Created download directory: {self.download_dir}")
|
||||
|
||||
for course in self.courses:
|
||||
course_name = course['CourseName']
|
||||
|
@ -262,23 +295,14 @@ class MoodleDownloader:
|
|||
response = session.post(download_url, data=post_data, headers=headers, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
# Attempt to extract filename from Content-Disposition header
|
||||
content_disposition = response.headers.get('Content-Disposition', '')
|
||||
filename = None
|
||||
if content_disposition:
|
||||
matches = re.findall('filename="(.+)"', content_disposition)
|
||||
if matches:
|
||||
filename = matches[0]
|
||||
if not filename:
|
||||
# If no filename in headers, use sanitized course name
|
||||
filename = f"{course_name}.zip"
|
||||
filename = self.sanitize_filename(filename)
|
||||
|
||||
# Determine filename
|
||||
filename = f"{self.sanitize_filename(course_name)}.zip"
|
||||
filepath = os.path.join(self.download_dir, filename)
|
||||
|
||||
# Overwrite existing files
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
logging.info(f"Overwriting existing file: {filepath}")
|
||||
|
||||
with open(filepath, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
|
@ -290,10 +314,33 @@ class MoodleDownloader:
|
|||
logging.error(f"Error downloading course '{course_name}': {e}", exc_info=True)
|
||||
continue
|
||||
|
||||
def sanitize_filename(self, name):
|
||||
"""
|
||||
Sanitize the filename by removing invalid characters, replacing spaces with underscores,
|
||||
and truncating to a maximum length to prevent path issues.
|
||||
|
||||
:param name: Original filename
|
||||
:return: Sanitized filename
|
||||
"""
|
||||
# Normalize unicode characters
|
||||
name = unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore').decode('ASCII')
|
||||
# Remove invalid characters for filenames, including newlines
|
||||
sanitized = re.sub(r'[<>:"/\\|?*\n\r]+', '', name)
|
||||
# Replace spaces with underscores
|
||||
sanitized = re.sub(r'\s+', '_', sanitized)
|
||||
# Remove trailing underscores
|
||||
sanitized = sanitized.rstrip('_')
|
||||
# Truncate to a reasonable length (e.g., 200 characters)
|
||||
MAX_LENGTH = 200
|
||||
if len(sanitized) > MAX_LENGTH:
|
||||
sanitized = sanitized[:MAX_LENGTH]
|
||||
logging.warning(f"Filename truncated to {MAX_LENGTH} characters: '{sanitized}'")
|
||||
return sanitized
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the Selenium WebDriver.
|
||||
"""
|
||||
if self.driver:
|
||||
logging.info("Closing the browser.")
|
||||
self.driver.quit()
|
||||
if self.cleanup_download_dir:
|
||||
logging.info("Cleaning up temporary download directory.")
|
||||
self.temp_dir.cleanup()
|
|
@ -11,7 +11,14 @@
|
|||
"plugin:react-hooks/recommended",
|
||||
"plugin:jsx-a11y/recommended"
|
||||
],
|
||||
"plugins": ["react", "unused-imports", "import", "@typescript-eslint", "jsx-a11y", "prettier"],
|
||||
"plugins": [
|
||||
"react",
|
||||
"unused-imports",
|
||||
"import",
|
||||
"@typescript-eslint",
|
||||
"jsx-a11y",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
|
@ -80,8 +87,8 @@
|
|||
],
|
||||
"padding-line-between-statements": [
|
||||
"warn",
|
||||
{"blankLine": "always", "prev": "*", "next": "return"},
|
||||
{"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"},
|
||||
{ "blankLine": "always", "prev": "*", "next": "return" },
|
||||
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
|
||||
{
|
||||
"blankLine": "any",
|
||||
"prev": ["const", "let", "var"],
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"endOfLine": "lf"
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Next.js App",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 9229,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "promptString",
|
||||
"id": "programPath",
|
||||
"description": "Path to the entry point of your Next.js application (e.g., app/page.tsx)"
|
||||
},
|
||||
{
|
||||
"type": "promptString",
|
||||
"id": "programPath",
|
||||
"description": "Path to the entry point of your Next.js application (e.g., app/page.tsx)"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,19 +1,19 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error
|
||||
reset: () => void
|
||||
error: Error;
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
/* eslint-disable no-console */
|
||||
console.error(error)
|
||||
}, [error])
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -27,5 +27,5 @@ export default function Error({
|
|||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
"use client";
|
||||
import { useState } from "react";
|
||||
import { Input, Button, Card, Spacer, Progress } from "@nextui-org/react";
|
||||
|
||||
// ...existing code...
|
||||
export default function FoodPage() {
|
||||
const [foodItems, setFoodItems] = useState<
|
||||
{ name: string; expirationDate: string }[]
|
||||
>([]);
|
||||
const [name, setName] = useState("");
|
||||
const [expirationDate, setExpirationDate] = useState("");
|
||||
|
||||
const addFoodItem = () => {
|
||||
if (name.trim() && expirationDate) {
|
||||
setFoodItems([...foodItems, { name: name.trim(), expirationDate }]);
|
||||
setName("");
|
||||
setExpirationDate("");
|
||||
}
|
||||
};
|
||||
|
||||
const getDaysRemaining = (dateString: string) => {
|
||||
const today = new Date();
|
||||
const expiration = new Date(dateString);
|
||||
const difference = expiration.getTime() - today.getTime();
|
||||
|
||||
return Math.ceil(difference / (1000 * 3600 * 24));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4">
|
||||
<h2>Food Glance</h2>
|
||||
<Spacer y={1} />
|
||||
<Card className="w-full max-w-md">
|
||||
<div>
|
||||
<Input
|
||||
fullWidth
|
||||
className="mb-4"
|
||||
placeholder="Food Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
fullWidth
|
||||
className="mb-4"
|
||||
type="date"
|
||||
value={expirationDate}
|
||||
onChange={(e) => setExpirationDate(e.target.value)}
|
||||
/>
|
||||
<Button className="w-full" onClick={addFoodItem}>
|
||||
Add Food
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Spacer y={1} />
|
||||
<div className="w-full max-w-md">
|
||||
{foodItems.map((item, index) => {
|
||||
const daysRemaining = getDaysRemaining(item.expirationDate);
|
||||
const totalDays =
|
||||
getDaysRemaining(new Date().toISOString().split("T")[0]) +
|
||||
daysRemaining;
|
||||
const progress = ((totalDays - daysRemaining) / totalDays) * 100;
|
||||
|
||||
return (
|
||||
<Card key={index} className="mb-2" variant="bordered">
|
||||
<div>
|
||||
<Text b>{item.name}</Text>
|
||||
<Text size={14}>
|
||||
Expires in {daysRemaining} day{daysRemaining !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
<Progress value={progress} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// ...existing code...
|
|
@ -37,16 +37,12 @@ export default function RootLayout({
|
|||
<body
|
||||
className={clsx(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
fontSans.variable,
|
||||
fontSans.variable
|
||||
)}
|
||||
>
|
||||
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
|
||||
<div className="relative flex flex-col h-screen">
|
||||
<main className="container mx-auto max-w-7xl pt-16 px-6 flex-grow">
|
||||
<Providers>
|
||||
{children}
|
||||
</main>
|
||||
<BottomNavbar />
|
||||
</div>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
"use client";
|
||||
import { Text } from "@nextui-org/react";
|
||||
|
||||
export default function App() {
|
||||
return <>Content</>;
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4">
|
||||
<Text h1>Welcome to Blackboard</Text>
|
||||
{/* Add additional content here */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import { NextUIProvider } from '@nextui-org/system'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import { ThemeProviderProps } from 'next-themes/dist/types'
|
||||
import * as React from "react";
|
||||
import { NextUIProvider } from "@nextui-org/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode
|
||||
themeProps?: ThemeProviderProps
|
||||
children: React.ReactNode;
|
||||
themeProps?: ThemeProviderProps;
|
||||
}
|
||||
|
||||
export function Providers({ children, themeProps }: ProvidersProps) {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<NextUIProvider navigate={router.push}>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
{...themeProps}
|
||||
>
|
||||
<NextUIProvider>
|
||||
{children}
|
||||
</NextUIProvider>
|
||||
)
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
"use client";
|
||||
import { Card, Text } from "@nextui-org/react";
|
||||
|
||||
// ...existing code...
|
||||
export default function StudyPage() {
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4">
|
||||
<Text h2>Study Rundown</Text>
|
||||
<Card className="w-full max-w-md mt-4">
|
||||
<Card.Body>
|
||||
<Text>
|
||||
{/* Add study-related content here */}
|
||||
Coming soon...
|
||||
</Text>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// ...existing code...
|
|
@ -0,0 +1,78 @@
|
|||
"use client";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
Checkbox,
|
||||
Card,
|
||||
Spacer
|
||||
} from "@nextui-org/react";
|
||||
|
||||
export default function TodoPage() {
|
||||
const [todos, setTodos] = useState<{ text: string; completed: boolean }[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
const addTodo = () => {
|
||||
if (input.trim()) {
|
||||
setTodos([...todos, { text: input.trim(), completed: false }]);
|
||||
setInput("");
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTodo = (index: number) => {
|
||||
setTodos(
|
||||
todos.map((todo, i) =>
|
||||
i === index ? { ...todo, completed: !todo.completed } : todo
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const removeTodo = (index: number) => {
|
||||
setTodos(todos.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4">
|
||||
<Text h2>Todo List</Text>
|
||||
<h2>Todo List</h2>
|
||||
<div className="flex w-full max-w-md">
|
||||
<Input
|
||||
clearable
|
||||
fullWidth
|
||||
placeholder="Add a new todo"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
/>
|
||||
<Button auto onClick={addTodo}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<Spacer y={1} />
|
||||
<div className="w-full max-w-md">
|
||||
{todos.map((todo, index) => (
|
||||
<Card key={index} variant="bordered" className="mb-2">
|
||||
<Card.Body className="flex items-center">
|
||||
<Checkbox
|
||||
isSelected={todo.completed}
|
||||
onChange={() => toggleTodo(index)}
|
||||
>
|
||||
<Text del={todo.completed} className="ml-2">
|
||||
<span className={`ml-2 ${todo.completed ? "line-through" : ""}`}>
|
||||
{todo.text}
|
||||
</span>
|
||||
<Spacer x={1} />
|
||||
<Button
|
||||
auto
|
||||
color="error"
|
||||
flat
|
||||
onClick={() => removeTodo(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
"use client";
|
||||
import { Card } from "@nextui-org/react";
|
||||
|
||||
// ...existing code...
|
||||
export default function WeatherPage() {
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4">
|
||||
<h2>Weather</h2>
|
||||
<Card className="w-full max-w-md mt-4">
|
||||
<Card>
|
||||
{/* Add weather-related content here */}
|
||||
Current weather data will be displayed here.
|
||||
</Card>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// ...existing code...
|
|
@ -1,14 +1,28 @@
|
|||
import Link from "next/link";
|
||||
import { Navbar, Text } from "@nextui-org/react";
|
||||
|
||||
import { siteConfig } from "../config/site";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
export default function BottomNavbar() {
|
||||
return (
|
||||
<footer className="w-full flex items-center justify-center py-3">
|
||||
{siteConfig.navItems.map((item) => (
|
||||
<Link key={item.href} href={item.href}>
|
||||
{item.label}
|
||||
<Navbar isBordered className="fixed bottom-0 left-0 w-full">
|
||||
<Navbar.Content justify="center">
|
||||
{siteConfig.navItems.map((item) => {
|
||||
const [icon, ...labelParts] = item.label.split(" ");
|
||||
const label = labelParts.join(" ");
|
||||
|
||||
return (
|
||||
<Navbar.Item key={item.href}>
|
||||
<Link href={item.href}>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-2xl mb-1">{icon}</span>
|
||||
<Text size={12}>{label}</Text>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</footer>
|
||||
</Navbar.Item>
|
||||
);
|
||||
})}
|
||||
</Navbar.Content>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@nextui-org/button'
|
||||
|
||||
export const Counter = () => {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<Button radius="full" onPress={() => setCount(count + 1)}>
|
||||
Count is {count}
|
||||
</Button>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import * as React from "react";
|
||||
|
||||
import { IconSvgProps } from '@/types'
|
||||
import { IconSvgProps } from "@/types";
|
||||
|
||||
export const Logo: React.FC<IconSvgProps> = ({
|
||||
size = 36,
|
||||
|
@ -22,7 +22,7 @@ export const Logo: React.FC<IconSvgProps> = ({
|
|||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const DiscordIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
|
@ -42,8 +42,8 @@ export const DiscordIcon: React.FC<IconSvgProps> = ({
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const TwitterIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
|
@ -63,8 +63,8 @@ export const TwitterIcon: React.FC<IconSvgProps> = ({
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const GithubIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
|
@ -86,8 +86,8 @@ export const GithubIcon: React.FC<IconSvgProps> = ({
|
|||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const MoonFilledIcon = ({
|
||||
size = 24,
|
||||
|
@ -109,7 +109,7 @@ export const MoonFilledIcon = ({
|
|||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const SunFilledIcon = ({
|
||||
size = 24,
|
||||
|
@ -131,7 +131,7 @@ export const SunFilledIcon = ({
|
|||
<path d="M12 22.96a.969.969 0 01-1-.96v-.08a1 1 0 012 0 1.038 1.038 0 01-1 1.04zm7.14-2.82a1.024 1.024 0 01-.71-.29l-.13-.13a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.984.984 0 01-.7.29zm-14.28 0a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a1 1 0 01-.7.29zM22 13h-.08a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zM2.08 13H2a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zm16.93-7.01a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a.984.984 0 01-.7.29zm-14.02 0a1.024 1.024 0 01-.71-.29l-.13-.14a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.97.97 0 01-.7.3zM12 3.04a.969.969 0 01-1-.96V2a1 1 0 012 0 1.038 1.038 0 01-1 1.04z" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const HeartFilledIcon = ({
|
||||
size = 24,
|
||||
|
@ -156,7 +156,7 @@ export const HeartFilledIcon = ({
|
|||
strokeWidth={1.5}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const SearchIcon = (props: IconSvgProps) => (
|
||||
<svg
|
||||
|
@ -184,10 +184,10 @@ export const SearchIcon = (props: IconSvgProps) => (
|
|||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export const NextUILogo: React.FC<IconSvgProps> = (props) => {
|
||||
const { width, height = 40 } = props
|
||||
const { width, height = 40 } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
|
@ -211,5 +211,5 @@ export const NextUILogo: React.FC<IconSvgProps> = (props) => {
|
|||
d="M17.5667 9.21729H18.8111V18.2403C18.8255 19.1128 18.6 19.9726 18.159 20.7256C17.7241 21.4555 17.0968 22.0518 16.3458 22.4491C15.5717 22.8683 14.6722 23.0779 13.6473 23.0779C12.627 23.0779 11.7286 22.8672 10.9521 22.4457C10.2007 22.0478 9.5727 21.4518 9.13602 20.7223C8.6948 19.9705 8.4692 19.1118 8.48396 18.2403V9.21729H9.72854V18.1538C9.71656 18.8298 9.88417 19.4968 10.2143 20.0868C10.5362 20.6506 11.0099 21.1129 11.5814 21.421C12.1689 21.7448 12.8576 21.9067 13.6475 21.9067C14.4374 21.9067 15.1272 21.7448 15.7169 21.421C16.2895 21.1142 16.7635 20.6516 17.0844 20.0868C17.4124 19.4961 17.5788 18.8293 17.5667 18.1538V9.21729ZM23.6753 9.21729V22.845H22.4309V9.21729H23.6753Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,53 +1,53 @@
|
|||
import { tv } from 'tailwind-variants'
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const title = tv({
|
||||
base: 'tracking-tight inline font-semibold',
|
||||
base: "tracking-tight inline font-semibold",
|
||||
variants: {
|
||||
color: {
|
||||
violet: 'from-[#FF1CF7] to-[#b249f8]',
|
||||
yellow: 'from-[#FF705B] to-[#FFB457]',
|
||||
blue: 'from-[#5EA2EF] to-[#0072F5]',
|
||||
cyan: 'from-[#00b7fa] to-[#01cfea]',
|
||||
green: 'from-[#6FEE8D] to-[#17c964]',
|
||||
pink: 'from-[#FF72E1] to-[#F54C7A]',
|
||||
foreground: 'dark:from-[#FFFFFF] dark:to-[#4B4B4B]',
|
||||
violet: "from-[#FF1CF7] to-[#b249f8]",
|
||||
yellow: "from-[#FF705B] to-[#FFB457]",
|
||||
blue: "from-[#5EA2EF] to-[#0072F5]",
|
||||
cyan: "from-[#00b7fa] to-[#01cfea]",
|
||||
green: "from-[#6FEE8D] to-[#17c964]",
|
||||
pink: "from-[#FF72E1] to-[#F54C7A]",
|
||||
foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]",
|
||||
},
|
||||
size: {
|
||||
sm: 'text-3xl lg:text-4xl',
|
||||
md: 'text-[2.3rem] lg:text-5xl leading-9',
|
||||
lg: 'text-4xl lg:text-6xl',
|
||||
sm: "text-3xl lg:text-4xl",
|
||||
md: "text-[2.3rem] lg:text-5xl leading-9",
|
||||
lg: "text-4xl lg:text-6xl",
|
||||
},
|
||||
fullWidth: {
|
||||
true: 'w-full block',
|
||||
true: "w-full block",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
size: "md",
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
color: [
|
||||
'violet',
|
||||
'yellow',
|
||||
'blue',
|
||||
'cyan',
|
||||
'green',
|
||||
'pink',
|
||||
'foreground',
|
||||
"violet",
|
||||
"yellow",
|
||||
"blue",
|
||||
"cyan",
|
||||
"green",
|
||||
"pink",
|
||||
"foreground",
|
||||
],
|
||||
class: 'bg-clip-text text-transparent bg-gradient-to-b',
|
||||
class: "bg-clip-text text-transparent bg-gradient-to-b",
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
export const subtitle = tv({
|
||||
base: 'w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full',
|
||||
base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full",
|
||||
variants: {
|
||||
fullWidth: {
|
||||
true: '!w-full',
|
||||
true: "!w-full",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
fullWidth: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import { FC } from 'react'
|
||||
import { VisuallyHidden } from '@react-aria/visually-hidden'
|
||||
import { SwitchProps, useSwitch } from '@nextui-org/switch'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useIsSSR } from '@react-aria/ssr'
|
||||
import clsx from 'clsx'
|
||||
import { FC } from "react";
|
||||
import { VisuallyHidden } from "@react-aria/visually-hidden";
|
||||
import { SwitchProps, useSwitch } from "@nextui-org/switch";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useIsSSR } from "@react-aria/ssr";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { SunFilledIcon, MoonFilledIcon } from '@/components/icons'
|
||||
import { SunFilledIcon, MoonFilledIcon } from "@/components/icons";
|
||||
|
||||
export interface ThemeSwitchProps {
|
||||
className?: string
|
||||
classNames?: SwitchProps['classNames']
|
||||
className?: string;
|
||||
classNames?: SwitchProps["classNames"];
|
||||
}
|
||||
|
||||
export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
||||
className,
|
||||
classNames,
|
||||
}) => {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const isSSR = useIsSSR()
|
||||
const { theme, setTheme } = useTheme();
|
||||
const isSSR = useIsSSR();
|
||||
|
||||
const onChange = () => {
|
||||
theme === 'light' ? setTheme('dark') : setTheme('light')
|
||||
}
|
||||
theme === "light" ? setTheme("dark") : setTheme("light");
|
||||
};
|
||||
|
||||
const {
|
||||
Component,
|
||||
|
@ -33,18 +33,18 @@ export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
|||
getInputProps,
|
||||
getWrapperProps,
|
||||
} = useSwitch({
|
||||
isSelected: theme === 'light' || isSSR,
|
||||
'aria-label': `Switch to ${theme === 'light' || isSSR ? 'dark' : 'light'} mode`,
|
||||
isSelected: theme === "light" || isSSR,
|
||||
"aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`,
|
||||
onChange,
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...getBaseProps({
|
||||
className: clsx(
|
||||
'px-px transition-opacity hover:opacity-80 cursor-pointer',
|
||||
"px-px transition-opacity hover:opacity-80 cursor-pointer",
|
||||
className,
|
||||
classNames?.base
|
||||
classNames?.base,
|
||||
),
|
||||
})}
|
||||
>
|
||||
|
@ -56,17 +56,17 @@ export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
|||
className={slots.wrapper({
|
||||
class: clsx(
|
||||
[
|
||||
'w-auto h-auto',
|
||||
'bg-transparent',
|
||||
'rounded-lg',
|
||||
'flex items-center justify-center',
|
||||
'group-data-[selected=true]:bg-transparent',
|
||||
'!text-default-500',
|
||||
'pt-px',
|
||||
'px-0',
|
||||
'mx-0',
|
||||
"w-auto h-auto",
|
||||
"bg-transparent",
|
||||
"rounded-lg",
|
||||
"flex items-center justify-center",
|
||||
"group-data-[selected=true]:bg-transparent",
|
||||
"!text-default-500",
|
||||
"pt-px",
|
||||
"px-0",
|
||||
"mx-0",
|
||||
],
|
||||
classNames?.wrapper
|
||||
classNames?.wrapper,
|
||||
),
|
||||
})}
|
||||
>
|
||||
|
@ -77,5 +77,5 @@ export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
|||
)}
|
||||
</div>
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Fira_Code as FontMono, Inter as FontSans } from 'next/font/google'
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
export const fontSans = FontSans({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-sans',
|
||||
})
|
||||
export const fontSans = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
export const fontMono = FontMono({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-mono',
|
||||
})
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
});
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
export type SiteConfig = typeof siteConfig
|
||||
export type SiteConfig = typeof siteConfig;
|
||||
|
||||
export const siteConfig = {
|
||||
name: 'Blackboard',
|
||||
description: 'Overview of the day',
|
||||
name: "Blackboard",
|
||||
description: "Overview of the day",
|
||||
navItems: [
|
||||
{
|
||||
label: '📝 Todo',
|
||||
href: '/todo',
|
||||
label: "📝 Todo",
|
||||
href: "/todo",
|
||||
},
|
||||
{
|
||||
label: '🍔 Food Glance',
|
||||
href: '/food',
|
||||
label: "🍔 Food Glance",
|
||||
href: "/food",
|
||||
},
|
||||
{
|
||||
label: '🤖 Study Rundown',
|
||||
href: '/study',
|
||||
label: "🤖 Study Rundown",
|
||||
href: "/study",
|
||||
},
|
||||
{
|
||||
label: 'Weather',
|
||||
href: '/weather',
|
||||
label: "☁️ Weather",
|
||||
href: "/weather",
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
// next.config.js
|
||||
const UnoCSS = require('@unocss/webpack').default
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
webpack: (config) => {
|
||||
config.plugins.push(
|
||||
UnoCSS(),
|
||||
)
|
||||
return config
|
||||
},
|
||||
}
|
||||
const nextConfig = {};
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = nextConfig;
|
||||
|
|
|
@ -23,13 +23,15 @@
|
|||
"@nextui-org/theme": "2.2.11",
|
||||
"@react-aria/ssr": "3.9.4",
|
||||
"@react-aria/visually-hidden": "3.8.12",
|
||||
"@unocss/webpack": "^0.63.6",
|
||||
"clsx": "2.1.1",
|
||||
"framer-motion": "~11.1.1",
|
||||
"intl-messageformat": "^10.5.0",
|
||||
"next": "14.2.4",
|
||||
"next": "^15.0.2",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
"react-dom": "18.3.1",
|
||||
"react-icons": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.5.7",
|
||||
|
@ -37,6 +39,7 @@
|
|||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"@unocss/postcss": "^0.63.6",
|
||||
"autoprefixer": "10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "14.2.1",
|
||||
|
@ -51,6 +54,7 @@
|
|||
"postcss": "8.4.38",
|
||||
"tailwind-variants": "0.1.20",
|
||||
"tailwindcss": "3.4.3",
|
||||
"typescript": "5.0.4"
|
||||
"typescript": "5.0.4",
|
||||
"unocss": "^0.63.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
"@unocss/postcss": {
|
||||
// Optional
|
||||
content: ["**/*.{html,js,ts,jsx,tsx}"],
|
||||
},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
@import '@unocss/reset/tailwind.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@unocss all;
|
|
@ -1,11 +1,11 @@
|
|||
import {nextui} from '@nextui-org/theme'
|
||||
import { nextui } from "@nextui-org/theme";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
|
@ -18,4 +18,4 @@ module.exports = {
|
|||
darkMode: "class",
|
||||
darkMode: "class",
|
||||
plugins: [nextui()],
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { SVGProps } from 'react'
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export type IconSvgProps = SVGProps<SVGSVGElement> & {
|
||||
size?: number
|
||||
}
|
||||
size?: number;
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @filename uno.config.ts
|
||||
import { defineConfig, presetUno } from "unocss";
|
||||
|
||||
export default defineConfig({
|
||||
|
|
Loading…
Reference in New Issue