Compare commits

...

2 Commits

Author SHA1 Message Date
Oliver Schütz ccdfc21f43 Saving 2024-10-31 15:58:37 +01:00
Oliver Schütz 0a4af50d08 Saving 2024-10-31 14:23:21 +01:00
32 changed files with 1048 additions and 607 deletions

View File

@ -22,3 +22,9 @@
### Layout ### Layout
### Deployment ### Deployment
## Hilfsmittel
- ChatGPT
- https://chatgpt.com/share/671d735f-8f40-8006-a195-1834c70412df

Binary file not shown.

View File

@ -1,61 +1,134 @@
# course_content_extractor.py
import os import os
import zipfile import zipfile
import shutil import shutil
import tempfile import tempfile
import subprocess import subprocess
import sys import sys
import re
import unicodedata
import logging
class CourseContentExtractor: 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.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): def extract_contents(self, courses):
# Ensure output_dir exists """
if not os.path.exists(self.output_dir): Extract and organize course contents based on the provided folder structure.
os.makedirs(self.output_dir)
# 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')] zip_files = [f for f in os.listdir(self.download_dir) if f.endswith('.zip')]
for filename in zip_files: for filename in zip_files:
zip_path = os.path.join(self.download_dir, filename) zip_path = os.path.join(self.download_dir, filename)
base_name = os.path.splitext(filename)[0] base_name = os.path.splitext(filename)[0]
# Use the base name as the course folder name # Find the course info matching the ZIP file
course_name = base_name # 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) # Build the folder structure
# Ensure course_output_dir exists semester = course_info['Semester']
if not os.path.exists(course_output_dir): course_name = course_info['CourseName']
os.makedirs(course_output_dir)
# 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: with tempfile.TemporaryDirectory() as temp_extract_dir:
# Extract ZIP file to temporary directory try:
with zipfile.ZipFile(zip_path, 'r') as zip_ref: with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(temp_extract_dir) 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 root, dirs, files in os.walk(temp_extract_dir):
for file in files: for file in files:
file_path = os.path.join(root, file) file_path = os.path.join(root, file)
if file.lower().endswith('.pdf'): if file.lower().endswith('.pdf'):
# Copy PDF files to course_output_dir dest_folder = os.path.join(course_output_dir, 'Lectures')
shutil.copy2(file_path, course_output_dir) 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')): elif file.lower().endswith(('.ppt', '.pptx')):
# Convert PowerPoint files to PDF try:
self.convert_ppt_to_pdf(file_path, course_output_dir) self.convert_ppt_to_pdf(file_path, os.path.join(course_output_dir, 'Lectures'))
# Delete the ZIP file after processing 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) 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): 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: try:
# Determine the command based on the operating system # Determine the command based on the operating system
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
# Windows systems office_executable = 'soffice' # Ensure LibreOffice is installed and in PATH
office_executable = 'soffice'
else: else:
# Linux and others
office_executable = 'libreoffice' office_executable = 'libreoffice'
# Prepare the command to convert PPT/PPTX to PDF using LibreOffice # Prepare the command to convert PPT/PPTX to PDF using LibreOffice
@ -68,12 +141,46 @@ class CourseContentExtractor:
] ]
# Execute the command # Execute the command
subprocess.run(command, check=True) 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.") print(f"Converted {os.path.basename(ppt_path)} to PDF.")
except subprocess.CalledProcessError as e: 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}") print(f"Failed to convert {os.path.basename(ppt_path)} to PDF. Error: {e}")
# Optionally, copy the original PPT/PPTX file # Optionally, copy the original PPT/PPTX file
shutil.copy2(ppt_path, output_dir) 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: 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.") print(f"{office_executable} is not installed or not found in the system path.")
# Optionally, copy the original PPT/PPTX file # Optionally, copy the original PPT/PPTX file
shutil.copy2(ppt_path, output_dir) 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

View File

@ -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

View File

@ -1,41 +1,76 @@
# main.py # update_study_material.py
import logging import os
import shutil
import tempfile
from moodle_downloader import MoodleDownloader from moodle_downloader import MoodleDownloader
from course_content_extractor import CourseContentExtractor 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 def main():
USERNAME = os.getenv('MOODLE_USERNAME') # Configure logging
PASSWORD = os.getenv('MOODLE_PASSWORD') 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: # Load environment variables
print("Please set the MOODLE_USERNAME and MOODLE_PASSWORD environment variables.") load_dotenv()
exit(1) 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 # Check if root_dir exists and is a directory
downloader = MoodleDownloader(USERNAME, PASSWORD, headless=True) 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: # Treat root_dir as the study_program folder
# Login to Moodle study_program = os.path.basename(os.path.normpath(root_dir))
downloader.login() logging.info(f"Using root_dir as the study_program: {study_program}")
# Retrieve courses # Use system temporary directory for downloads
downloader.get_courses() with tempfile.TemporaryDirectory() as download_dir:
logging.info(f"Using temporary download directory: {download_dir}")
# Download all courses # Load credentials from environment variables
downloader.download_all_courses() username = os.getenv('MOODLE_USERNAME')
password = os.getenv('MOODLE_PASSWORD')
# Extract course contents using the updated class if not username or not password:
extractor = CourseContentExtractor(downloader.download_dir) print("Please set your Moodle credentials in environment variables.")
extractor.extract_contents() logging.error("Moodle credentials not set in environment variables.")
finally: return
# Close the browser
downloader.close() # Initialize downloader
downloader = MoodleDownloader(username, password, download_dir=download_dir, headless=True)
try:
downloader.login()
downloader.get_courses()
downloader.download_all_courses()
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()

View File

@ -1,6 +1,7 @@
# moodle_downloader.py
import os import os
import re import re
import time
import logging import logging
import requests import requests
import unicodedata import unicodedata
@ -13,20 +14,21 @@ from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException from selenium.common.exceptions import TimeoutException
from selenium.webdriver.chrome.service import Service as ChromeService from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.chrome import ChromeDriverManager
import tempfile
class MoodleDownloader: 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.username = username
self.password = password self.password = password
if download_dir: self.download_dir = download_dir # Set externally to use system temp
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.headless = headless self.headless = headless
self.driver = None self.driver = None
self.courses = [] self.courses = []
@ -34,7 +36,9 @@ class MoodleDownloader:
self.MY_COURSES_URL = 'https://moodle.fhgr.ch/my/courses.php' self.MY_COURSES_URL = 'https://moodle.fhgr.ch/my/courses.php'
def setup_driver(self): def setup_driver(self):
# Set up Chrome options """
Set up the Selenium WebDriver with Chrome options.
"""
chrome_options = Options() chrome_options = Options()
if self.headless: if self.headless:
chrome_options.add_argument('--headless') # Headless mode chrome_options.add_argument('--headless') # Headless mode
@ -58,6 +62,9 @@ class MoodleDownloader:
self.driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options) self.driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options)
def login(self): def login(self):
"""
Log in to Moodle.
"""
self.setup_driver() self.setup_driver()
driver = self.driver driver = self.driver
try: try:
@ -118,6 +125,9 @@ class MoodleDownloader:
raise e raise e
def get_courses(self): def get_courses(self):
"""
Retrieve the list of courses from Moodle.
"""
driver = self.driver driver = self.driver
try: try:
# Navigate to "My Courses" page # Navigate to "My Courses" page
@ -140,27 +150,34 @@ class MoodleDownloader:
existing_urls = set() existing_urls = set()
for coursename_element in course_elements: for coursename_element in course_elements:
try: try:
# Get the text content # Extract course name from the nested span
full_text = coursename_element.text.strip() course_name_element = coursename_element.find_element(By.CSS_SELECTOR, 'span.multiline span[aria-hidden="true"]')
lines = [line.strip() for line in full_text.split('\n') if line.strip()] course_title = course_name_element.text.strip()
# Remove duplicates logging.debug(f"Course title extracted: '{course_title}'")
unique_lines = list(dict.fromkeys(lines))
# Assume the last line is the actual course name
course_name = unique_lines[-1]
# Extract course code and term # Extract semester from the sibling div
short_name = self.extract_course_code_and_term(course_name) 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') course_url = coursename_element.get_attribute('href')
# Check for duplicates # Check for duplicates
if course_url in existing_urls: 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 continue
existing_urls.add(course_url) existing_urls.add(course_url)
self.courses.append({'CourseName': short_name, 'URL': course_url}) self.courses.append({
logging.info(f"Course found: {short_name} - {course_url}") '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: except Exception as e:
logging.warning(f"Error extracting course: {e}") logging.warning(f"Error extracting course: {e}")
continue continue
@ -172,31 +189,46 @@ class MoodleDownloader:
logging.error("An error occurred while retrieving courses.", exc_info=True) logging.error("An error occurred while retrieving courses.", exc_info=True)
raise e raise e
def extract_course_code_and_term(self, course_name): def sanitize_semester(self, semester):
# Regular expression to match course code and term """
# Example course name: 'Mathematik I (cds-401) HS24' Sanitize the semester name by replacing spaces with underscores and removing trailing underscores.
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_filename(self, name): :param semester: Original semester string
# Normalize unicode characters :return: Sanitized semester string
name = unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore').decode('ASCII') """
# Remove invalid characters for filenames, including newlines sanitized = re.sub(r'\s+', '_', semester).strip('_')
sanitized = re.sub(r'[<>:"/\\|?*\n\r]+', '', name) logging.debug(f"Sanitized semester: '{sanitized}'")
# Replace spaces and other problematic characters with underscores return sanitized
sanitized = re.sub(r'[\s]+', '_', sanitized)
# Truncate to a reasonable length (e.g., 100 characters) def extract_course_info(self, course_title):
return sanitized[:100] """
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): def download_all_courses(self):
"""
Download all courses as ZIP files.
"""
if not self.courses: if not self.courses:
logging.warning("No courses to download.") logging.warning("No courses to download.")
return return
@ -206,6 +238,7 @@ class MoodleDownloader:
# Ensure the download directory exists # Ensure the download directory exists
if not os.path.exists(self.download_dir): if not os.path.exists(self.download_dir):
os.makedirs(self.download_dir) os.makedirs(self.download_dir)
logging.info(f"Created download directory: {self.download_dir}")
for course in self.courses: for course in self.courses:
course_name = course['CourseName'] course_name = course['CourseName']
@ -262,23 +295,14 @@ class MoodleDownloader:
response = session.post(download_url, data=post_data, headers=headers, stream=True) response = session.post(download_url, data=post_data, headers=headers, stream=True)
response.raise_for_status() response.raise_for_status()
# Attempt to extract filename from Content-Disposition header # Determine filename
content_disposition = response.headers.get('Content-Disposition', '') filename = f"{self.sanitize_filename(course_name)}.zip"
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)
filepath = os.path.join(self.download_dir, filename) filepath = os.path.join(self.download_dir, filename)
# Overwrite existing files # Overwrite existing files
if os.path.exists(filepath): if os.path.exists(filepath):
os.remove(filepath) os.remove(filepath)
logging.info(f"Overwriting existing file: {filepath}")
with open(filepath, 'wb') as f: with open(filepath, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192): 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) logging.error(f"Error downloading course '{course_name}': {e}", exc_info=True)
continue 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): def close(self):
"""
Close the Selenium WebDriver.
"""
if self.driver: if self.driver:
logging.info("Closing the browser.") logging.info("Closing the browser.")
self.driver.quit() self.driver.quit()
if self.cleanup_download_dir:
logging.info("Cleaning up temporary download directory.")
self.temp_dir.cleanup()

View File

@ -11,7 +11,14 @@
"plugin:react-hooks/recommended", "plugin:react-hooks/recommended",
"plugin:jsx-a11y/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", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"ecmaFeatures": { "ecmaFeatures": {
@ -80,8 +87,8 @@
], ],
"padding-line-between-statements": [ "padding-line-between-statements": [
"warn", "warn",
{"blankLine": "always", "prev": "*", "next": "return"}, { "blankLine": "always", "prev": "*", "next": "return" },
{"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"}, { "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
{ {
"blankLine": "any", "blankLine": "any",
"prev": ["const", "let", "var"], "prev": ["const", "let", "var"],

View File

@ -0,0 +1,3 @@
{
"endOfLine": "lf"
}

View File

@ -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)"
}
]
}

View File

@ -1,3 +1,3 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib"
} }

View File

@ -1,31 +1,31 @@
'use client' "use client";
import { useEffect } from 'react' import { useEffect } from "react";
export default function Error({ export default function Error({
error, error,
reset, reset,
}: { }: {
error: Error error: Error;
reset: () => void reset: () => void;
}) { }) {
useEffect(() => { useEffect(() => {
// Log the error to an error reporting service // Log the error to an error reporting service
/* eslint-disable no-console */ /* eslint-disable no-console */
console.error(error) console.error(error);
}, [error]) }, [error]);
return ( return (
<div> <div>
<h2>Something went wrong!</h2> <h2>Something went wrong!</h2>
<button <button
onClick={ onClick={
// Attempt to recover by trying to re-render the segment // Attempt to recover by trying to re-render the segment
() => reset() () => reset()
} }
> >
Try again Try again
</button> </button>
</div> </div>
) );
} }

View File

@ -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...

View File

@ -37,16 +37,12 @@ export default function RootLayout({
<body <body
className={clsx( className={clsx(
"min-h-screen bg-background font-sans antialiased", "min-h-screen bg-background font-sans antialiased",
fontSans.variable, fontSans.variable
)} )}
> >
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}> <Providers>
<div className="relative flex flex-col h-screen"> {children}
<main className="container mx-auto max-w-7xl pt-16 px-6 flex-grow"> <BottomNavbar />
{children}
</main>
<BottomNavbar />
</div>
</Providers> </Providers>
</body> </body>
</html> </html>

View File

@ -1,3 +1,11 @@
"use client";
import { Text } from "@nextui-org/react";
export default function App() { 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>
);
} }

View File

@ -1,22 +1,27 @@
'use client' "use client";
import * as React from 'react' import * as React from "react";
import { NextUIProvider } from '@nextui-org/system' import { NextUIProvider } from "@nextui-org/react";
import { useRouter } from 'next/navigation' import { useRouter } from "next/navigation";
import { ThemeProvider as NextThemesProvider } from 'next-themes' import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from 'next-themes/dist/types' import { ThemeProviderProps } from "next-themes/dist/types";
export interface ProvidersProps { export interface ProvidersProps {
children: React.ReactNode children: React.ReactNode;
themeProps?: ThemeProviderProps themeProps?: ThemeProviderProps;
} }
export function Providers({ children, themeProps }: ProvidersProps) { export function Providers({ children, themeProps }: ProvidersProps) {
const router = useRouter() return (
<NextThemesProvider
return ( attribute="class"
<NextUIProvider navigate={router.push}> defaultTheme="system"
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider> enableSystem
</NextUIProvider> {...themeProps}
) >
<NextUIProvider>
{children}
</NextUIProvider>
</NextThemesProvider>
);
} }

View File

@ -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...

View File

@ -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>
);
}

View File

@ -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...

View File

@ -1,14 +1,28 @@
import Link from "next/link"; 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() { export default function BottomNavbar() {
return ( return (
<footer className="w-full flex items-center justify-center py-3"> <Navbar isBordered className="fixed bottom-0 left-0 w-full">
{siteConfig.navItems.map((item) => ( <Navbar.Content justify="center">
<Link key={item.href} href={item.href}> {siteConfig.navItems.map((item) => {
{item.label} const [icon, ...labelParts] = item.label.split(" ");
</Link> const label = labelParts.join(" ");
))}
</footer> 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>
</Navbar.Item>
);
})}
</Navbar.Content>
</Navbar>
); );
} }

View File

@ -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>
)
}

View File

@ -1,215 +1,215 @@
import * as React from 'react' import * as React from "react";
import { IconSvgProps } from '@/types' import { IconSvgProps } from "@/types";
export const Logo: React.FC<IconSvgProps> = ({ export const Logo: React.FC<IconSvgProps> = ({
size = 36, size = 36,
width, width,
height, height,
...props ...props
}) => ( }) => (
<svg <svg
fill="none" fill="none"
height={size || height} height={size || height}
viewBox="0 0 32 32" viewBox="0 0 32 32"
width={size || width} width={size || width}
{...props} {...props}
> >
<path <path
clipRule="evenodd" clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z" d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor" fill="currentColor"
fillRule="evenodd" fillRule="evenodd"
/> />
</svg> </svg>
) );
export const DiscordIcon: React.FC<IconSvgProps> = ({ export const DiscordIcon: React.FC<IconSvgProps> = ({
size = 24, size = 24,
width, width,
height, height,
...props ...props
}) => { }) => {
return ( return (
<svg <svg
height={size || height} height={size || height}
viewBox="0 0 24 24" viewBox="0 0 24 24"
width={size || width} width={size || width}
{...props} {...props}
> >
<path <path
d="M14.82 4.26a10.14 10.14 0 0 0-.53 1.1 14.66 14.66 0 0 0-4.58 0 10.14 10.14 0 0 0-.53-1.1 16 16 0 0 0-4.13 1.3 17.33 17.33 0 0 0-3 11.59 16.6 16.6 0 0 0 5.07 2.59A12.89 12.89 0 0 0 8.23 18a9.65 9.65 0 0 1-1.71-.83 3.39 3.39 0 0 0 .42-.33 11.66 11.66 0 0 0 10.12 0q.21.18.42.33a10.84 10.84 0 0 1-1.71.84 12.41 12.41 0 0 0 1.08 1.78 16.44 16.44 0 0 0 5.06-2.59 17.22 17.22 0 0 0-3-11.59 16.09 16.09 0 0 0-4.09-1.35zM8.68 14.81a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.93 1.93 0 0 1 1.8 2 1.93 1.93 0 0 1-1.8 2zm6.64 0a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.92 1.92 0 0 1 1.8 2 1.92 1.92 0 0 1-1.8 2z" d="M14.82 4.26a10.14 10.14 0 0 0-.53 1.1 14.66 14.66 0 0 0-4.58 0 10.14 10.14 0 0 0-.53-1.1 16 16 0 0 0-4.13 1.3 17.33 17.33 0 0 0-3 11.59 16.6 16.6 0 0 0 5.07 2.59A12.89 12.89 0 0 0 8.23 18a9.65 9.65 0 0 1-1.71-.83 3.39 3.39 0 0 0 .42-.33 11.66 11.66 0 0 0 10.12 0q.21.18.42.33a10.84 10.84 0 0 1-1.71.84 12.41 12.41 0 0 0 1.08 1.78 16.44 16.44 0 0 0 5.06-2.59 17.22 17.22 0 0 0-3-11.59 16.09 16.09 0 0 0-4.09-1.35zM8.68 14.81a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.93 1.93 0 0 1 1.8 2 1.93 1.93 0 0 1-1.8 2zm6.64 0a1.94 1.94 0 0 1-1.8-2 1.93 1.93 0 0 1 1.8-2 1.92 1.92 0 0 1 1.8 2 1.92 1.92 0 0 1-1.8 2z"
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
) );
} };
export const TwitterIcon: React.FC<IconSvgProps> = ({ export const TwitterIcon: React.FC<IconSvgProps> = ({
size = 24, size = 24,
width, width,
height, height,
...props ...props
}) => { }) => {
return ( return (
<svg <svg
height={size || height} height={size || height}
viewBox="0 0 24 24" viewBox="0 0 24 24"
width={size || width} width={size || width}
{...props} {...props}
> >
<path <path
d="M19.633 7.997c.013.175.013.349.013.523 0 5.325-4.053 11.461-11.46 11.461-2.282 0-4.402-.661-6.186-1.809.324.037.636.05.973.05a8.07 8.07 0 0 0 5.001-1.721 4.036 4.036 0 0 1-3.767-2.793c.249.037.499.062.761.062.361 0 .724-.05 1.061-.137a4.027 4.027 0 0 1-3.23-3.953v-.05c.537.299 1.16.486 1.82.511a4.022 4.022 0 0 1-1.796-3.354c0-.748.199-1.434.548-2.032a11.457 11.457 0 0 0 8.306 4.215c-.062-.3-.1-.611-.1-.923a4.026 4.026 0 0 1 4.028-4.028c1.16 0 2.207.486 2.943 1.272a7.957 7.957 0 0 0 2.556-.973 4.02 4.02 0 0 1-1.771 2.22 8.073 8.073 0 0 0 2.319-.624 8.645 8.645 0 0 1-2.019 2.083z" d="M19.633 7.997c.013.175.013.349.013.523 0 5.325-4.053 11.461-11.46 11.461-2.282 0-4.402-.661-6.186-1.809.324.037.636.05.973.05a8.07 8.07 0 0 0 5.001-1.721 4.036 4.036 0 0 1-3.767-2.793c.249.037.499.062.761.062.361 0 .724-.05 1.061-.137a4.027 4.027 0 0 1-3.23-3.953v-.05c.537.299 1.16.486 1.82.511a4.022 4.022 0 0 1-1.796-3.354c0-.748.199-1.434.548-2.032a11.457 11.457 0 0 0 8.306 4.215c-.062-.3-.1-.611-.1-.923a4.026 4.026 0 0 1 4.028-4.028c1.16 0 2.207.486 2.943 1.272a7.957 7.957 0 0 0 2.556-.973 4.02 4.02 0 0 1-1.771 2.22 8.073 8.073 0 0 0 2.319-.624 8.645 8.645 0 0 1-2.019 2.083z"
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
) );
} };
export const GithubIcon: React.FC<IconSvgProps> = ({ export const GithubIcon: React.FC<IconSvgProps> = ({
size = 24, size = 24,
width, width,
height, height,
...props ...props
}) => { }) => {
return ( return (
<svg <svg
height={size || height} height={size || height}
viewBox="0 0 24 24" viewBox="0 0 24 24"
width={size || width} width={size || width}
{...props} {...props}
> >
<path <path
clipRule="evenodd" clipRule="evenodd"
d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z" d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z"
fill="currentColor" fill="currentColor"
fillRule="evenodd" fillRule="evenodd"
/> />
</svg> </svg>
) );
} };
export const MoonFilledIcon = ({ export const MoonFilledIcon = ({
size = 24, size = 24,
width, width,
height, height,
...props ...props
}: IconSvgProps) => ( }: IconSvgProps) => (
<svg <svg
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"
height={size || height} height={size || height}
role="presentation" role="presentation"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width={size || width} width={size || width}
{...props} {...props}
> >
<path <path
d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z" d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z"
fill="currentColor" fill="currentColor"
/> />
</svg> </svg>
) );
export const SunFilledIcon = ({ export const SunFilledIcon = ({
size = 24, size = 24,
width, width,
height, height,
...props ...props
}: IconSvgProps) => ( }: IconSvgProps) => (
<svg <svg
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"
height={size || height} height={size || height}
role="presentation" role="presentation"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width={size || width} width={size || width}
{...props} {...props}
> >
<g fill="currentColor"> <g fill="currentColor">
<path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" /> <path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" />
<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" /> <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> </g>
</svg> </svg>
) );
export const HeartFilledIcon = ({ export const HeartFilledIcon = ({
size = 24, size = 24,
width, width,
height, height,
...props ...props
}: IconSvgProps) => ( }: IconSvgProps) => (
<svg <svg
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"
height={size || height} height={size || height}
role="presentation" role="presentation"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width={size || width} width={size || width}
{...props} {...props}
> >
<path <path
d="M12.62 20.81c-.34.12-.9.12-1.24 0C8.48 19.82 2 15.69 2 8.69 2 5.6 4.49 3.1 7.56 3.1c1.82 0 3.43.88 4.44 2.24a5.53 5.53 0 0 1 4.44-2.24C19.51 3.1 22 5.6 22 8.69c0 7-6.48 11.13-9.38 12.12Z" d="M12.62 20.81c-.34.12-.9.12-1.24 0C8.48 19.82 2 15.69 2 8.69 2 5.6 4.49 3.1 7.56 3.1c1.82 0 3.43.88 4.44 2.24a5.53 5.53 0 0 1 4.44-2.24C19.51 3.1 22 5.6 22 8.69c0 7-6.48 11.13-9.38 12.12Z"
fill="currentColor" fill="currentColor"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth={1.5} strokeWidth={1.5}
/> />
</svg> </svg>
) );
export const SearchIcon = (props: IconSvgProps) => ( export const SearchIcon = (props: IconSvgProps) => (
<svg <svg
aria-hidden="true" aria-hidden="true"
fill="none" fill="none"
focusable="false" focusable="false"
height="1em" height="1em"
role="presentation" role="presentation"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="1em" width="1em"
{...props} {...props}
> >
<path <path
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
stroke="currentColor" stroke="currentColor"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="2" strokeWidth="2"
/> />
<path <path
d="M22 22L20 20" d="M22 22L20 20"
stroke="currentColor" stroke="currentColor"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="2" strokeWidth="2"
/> />
</svg> </svg>
) );
export const NextUILogo: React.FC<IconSvgProps> = (props) => { export const NextUILogo: React.FC<IconSvgProps> = (props) => {
const { width, height = 40 } = props const { width, height = 40 } = props;
return ( return (
<svg <svg
fill="none" fill="none"
height={height} height={height}
viewBox="0 0 161 32" viewBox="0 0 161 32"
width={width} width={width}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
> >
<path <path
className="fill-black dark:fill-white" className="fill-black dark:fill-white"
d="M55.6827 5V26.6275H53.7794L41.1235 8.51665H40.9563V26.6275H39V5H40.89L53.5911 23.1323H53.7555V5H55.6827ZM67.4831 26.9663C66.1109 27.0019 64.7581 26.6329 63.5903 25.9044C62.4852 25.185 61.6054 24.1633 61.0537 22.9582C60.4354 21.5961 60.1298 20.1106 60.1598 18.6126C60.132 17.1113 60.4375 15.6228 61.0537 14.2563C61.5954 13.0511 62.4525 12.0179 63.5326 11.268C64.6166 10.5379 65.8958 10.16 67.1986 10.1852C68.0611 10.1837 68.9162 10.3468 69.7187 10.666C70.5398 10.9946 71.2829 11.4948 71.8992 12.1337C72.5764 12.8435 73.0985 13.6889 73.4318 14.6152C73.8311 15.7483 74.0226 16.9455 73.9968 18.1479V19.0773H63.2262V17.4194H72.0935C72.1083 16.4456 71.8952 15.4821 71.4714 14.6072C71.083 13.803 70.4874 13.1191 69.7472 12.6272C68.9887 12.1348 68.1022 11.8812 67.2006 11.8987C66.2411 11.8807 65.3005 12.1689 64.5128 12.7223C63.7332 13.2783 63.1083 14.0275 62.6984 14.8978C62.2582 15.8199 62.0314 16.831 62.0352 17.8546V18.8476C62.009 20.0078 62.2354 21.1595 62.6984 22.2217C63.1005 23.1349 63.7564 23.9108 64.5864 24.4554C65.4554 24.9973 66.4621 25.2717 67.4831 25.2448C68.1676 25.2588 68.848 25.1368 69.4859 24.8859C70.0301 24.6666 70.5242 24.3376 70.9382 23.919C71.3183 23.5345 71.6217 23.0799 71.8322 22.5799L73.5995 23.1604C73.3388 23.8697 72.9304 24.5143 72.4019 25.0506C71.8132 25.6529 71.1086 26.1269 70.3314 26.4434C69.4258 26.8068 68.4575 26.9846 67.4831 26.9663V26.9663ZM78.8233 10.4075L82.9655 17.325L87.1076 10.4075H89.2683L84.1008 18.5175L89.2683 26.6275H87.103L82.9608 19.9317L78.8193 26.6275H76.6647L81.7711 18.5169L76.6647 10.4062L78.8233 10.4075ZM99.5142 10.4075V12.0447H91.8413V10.4075H99.5142ZM94.2427 6.52397H96.1148V22.3931C96.086 22.9446 96.2051 23.4938 96.4597 23.9827C96.6652 24.344 96.9805 24.629 97.3589 24.7955C97.7328 24.9548 98.1349 25.0357 98.5407 25.0332C98.7508 25.0359 98.9607 25.02 99.168 24.9857C99.3422 24.954 99.4956 24.9205 99.6283 24.8853L100.026 26.5853C99.8062 26.6672 99.5805 26.7327 99.3511 26.7815C99.0274 26.847 98.6977 26.8771 98.3676 26.8712C97.6854 26.871 97.0119 26.7156 96.3973 26.4166C95.7683 26.1156 95.2317 25.6485 94.8442 25.0647C94.4214 24.4018 94.2097 23.6242 94.2374 22.8363L94.2427 6.52397ZM118.398 5H120.354V19.3204C120.376 20.7052 120.022 22.0697 119.328 23.2649C118.644 24.4235 117.658 25.3698 116.477 26.0001C115.168 26.6879 113.708 27.0311 112.232 26.9978C110.759 27.029 109.302 26.6835 107.996 25.9934C106.815 25.362 105.827 24.4161 105.141 23.2582C104.447 22.0651 104.092 20.7022 104.115 19.319V5H106.08V19.1831C106.061 20.2559 106.324 21.3147 106.843 22.2511C107.349 23.1459 108.094 23.8795 108.992 24.3683C109.993 24.9011 111.111 25.1664 112.242 25.139C113.373 25.1656 114.493 24.9003 115.495 24.3683C116.395 23.8815 117.14 23.1475 117.644 22.2511C118.16 21.3136 118.421 20.2553 118.402 19.1831L118.398 5ZM128 5V26.6275H126.041V5H128Z" d="M55.6827 5V26.6275H53.7794L41.1235 8.51665H40.9563V26.6275H39V5H40.89L53.5911 23.1323H53.7555V5H55.6827ZM67.4831 26.9663C66.1109 27.0019 64.7581 26.6329 63.5903 25.9044C62.4852 25.185 61.6054 24.1633 61.0537 22.9582C60.4354 21.5961 60.1298 20.1106 60.1598 18.6126C60.132 17.1113 60.4375 15.6228 61.0537 14.2563C61.5954 13.0511 62.4525 12.0179 63.5326 11.268C64.6166 10.5379 65.8958 10.16 67.1986 10.1852C68.0611 10.1837 68.9162 10.3468 69.7187 10.666C70.5398 10.9946 71.2829 11.4948 71.8992 12.1337C72.5764 12.8435 73.0985 13.6889 73.4318 14.6152C73.8311 15.7483 74.0226 16.9455 73.9968 18.1479V19.0773H63.2262V17.4194H72.0935C72.1083 16.4456 71.8952 15.4821 71.4714 14.6072C71.083 13.803 70.4874 13.1191 69.7472 12.6272C68.9887 12.1348 68.1022 11.8812 67.2006 11.8987C66.2411 11.8807 65.3005 12.1689 64.5128 12.7223C63.7332 13.2783 63.1083 14.0275 62.6984 14.8978C62.2582 15.8199 62.0314 16.831 62.0352 17.8546V18.8476C62.009 20.0078 62.2354 21.1595 62.6984 22.2217C63.1005 23.1349 63.7564 23.9108 64.5864 24.4554C65.4554 24.9973 66.4621 25.2717 67.4831 25.2448C68.1676 25.2588 68.848 25.1368 69.4859 24.8859C70.0301 24.6666 70.5242 24.3376 70.9382 23.919C71.3183 23.5345 71.6217 23.0799 71.8322 22.5799L73.5995 23.1604C73.3388 23.8697 72.9304 24.5143 72.4019 25.0506C71.8132 25.6529 71.1086 26.1269 70.3314 26.4434C69.4258 26.8068 68.4575 26.9846 67.4831 26.9663V26.9663ZM78.8233 10.4075L82.9655 17.325L87.1076 10.4075H89.2683L84.1008 18.5175L89.2683 26.6275H87.103L82.9608 19.9317L78.8193 26.6275H76.6647L81.7711 18.5169L76.6647 10.4062L78.8233 10.4075ZM99.5142 10.4075V12.0447H91.8413V10.4075H99.5142ZM94.2427 6.52397H96.1148V22.3931C96.086 22.9446 96.2051 23.4938 96.4597 23.9827C96.6652 24.344 96.9805 24.629 97.3589 24.7955C97.7328 24.9548 98.1349 25.0357 98.5407 25.0332C98.7508 25.0359 98.9607 25.02 99.168 24.9857C99.3422 24.954 99.4956 24.9205 99.6283 24.8853L100.026 26.5853C99.8062 26.6672 99.5805 26.7327 99.3511 26.7815C99.0274 26.847 98.6977 26.8771 98.3676 26.8712C97.6854 26.871 97.0119 26.7156 96.3973 26.4166C95.7683 26.1156 95.2317 25.6485 94.8442 25.0647C94.4214 24.4018 94.2097 23.6242 94.2374 22.8363L94.2427 6.52397ZM118.398 5H120.354V19.3204C120.376 20.7052 120.022 22.0697 119.328 23.2649C118.644 24.4235 117.658 25.3698 116.477 26.0001C115.168 26.6879 113.708 27.0311 112.232 26.9978C110.759 27.029 109.302 26.6835 107.996 25.9934C106.815 25.362 105.827 24.4161 105.141 23.2582C104.447 22.0651 104.092 20.7022 104.115 19.319V5H106.08V19.1831C106.061 20.2559 106.324 21.3147 106.843 22.2511C107.349 23.1459 108.094 23.8795 108.992 24.3683C109.993 24.9011 111.111 25.1664 112.242 25.139C113.373 25.1656 114.493 24.9003 115.495 24.3683C116.395 23.8815 117.14 23.1475 117.644 22.2511C118.16 21.3136 118.421 20.2553 118.402 19.1831L118.398 5ZM128 5V26.6275H126.041V5H128Z"
/> />
<path <path
className="fill-black dark:fill-white" className="fill-black dark:fill-white"
d="M23.5294 0H8.47059C3.79241 0 0 3.79241 0 8.47059V23.5294C0 28.2076 3.79241 32 8.47059 32H23.5294C28.2076 32 32 28.2076 32 23.5294V8.47059C32 3.79241 28.2076 0 23.5294 0Z" d="M23.5294 0H8.47059C3.79241 0 0 3.79241 0 8.47059V23.5294C0 28.2076 3.79241 32 8.47059 32H23.5294C28.2076 32 32 28.2076 32 23.5294V8.47059C32 3.79241 28.2076 0 23.5294 0Z"
/> />
<path <path
className="fill-white dark:fill-black" className="fill-white dark:fill-black"
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" 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> </svg>
) );
} };

View File

@ -1,53 +1,53 @@
import { tv } from 'tailwind-variants' import { tv } from "tailwind-variants";
export const title = tv({ export const title = tv({
base: 'tracking-tight inline font-semibold', base: "tracking-tight inline font-semibold",
variants: { variants: {
color: { color: {
violet: 'from-[#FF1CF7] to-[#b249f8]', violet: "from-[#FF1CF7] to-[#b249f8]",
yellow: 'from-[#FF705B] to-[#FFB457]', yellow: "from-[#FF705B] to-[#FFB457]",
blue: 'from-[#5EA2EF] to-[#0072F5]', blue: "from-[#5EA2EF] to-[#0072F5]",
cyan: 'from-[#00b7fa] to-[#01cfea]', cyan: "from-[#00b7fa] to-[#01cfea]",
green: 'from-[#6FEE8D] to-[#17c964]', green: "from-[#6FEE8D] to-[#17c964]",
pink: 'from-[#FF72E1] to-[#F54C7A]', pink: "from-[#FF72E1] to-[#F54C7A]",
foreground: 'dark:from-[#FFFFFF] dark:to-[#4B4B4B]', 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',
},
fullWidth: {
true: 'w-full block',
},
}, },
defaultVariants: { size: {
size: 'md', sm: "text-3xl lg:text-4xl",
md: "text-[2.3rem] lg:text-5xl leading-9",
lg: "text-4xl lg:text-6xl",
}, },
compoundVariants: [ fullWidth: {
{ true: "w-full block",
color: [ },
'violet', },
'yellow', defaultVariants: {
'blue', size: "md",
'cyan', },
'green', compoundVariants: [
'pink', {
'foreground', color: [
], "violet",
class: 'bg-clip-text text-transparent bg-gradient-to-b', "yellow",
}, "blue",
], "cyan",
}) "green",
"pink",
"foreground",
],
class: "bg-clip-text text-transparent bg-gradient-to-b",
},
],
});
export const subtitle = tv({ 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: { variants: {
fullWidth: { fullWidth: {
true: '!w-full', true: "!w-full",
},
}, },
defaultVariants: { },
fullWidth: true, defaultVariants: {
}, fullWidth: true,
}) },
});

View File

@ -1,81 +1,81 @@
'use client' "use client";
import { FC } from 'react' import { FC } from "react";
import { VisuallyHidden } from '@react-aria/visually-hidden' import { VisuallyHidden } from "@react-aria/visually-hidden";
import { SwitchProps, useSwitch } from '@nextui-org/switch' import { SwitchProps, useSwitch } from "@nextui-org/switch";
import { useTheme } from 'next-themes' import { useTheme } from "next-themes";
import { useIsSSR } from '@react-aria/ssr' import { useIsSSR } from "@react-aria/ssr";
import clsx from 'clsx' import clsx from "clsx";
import { SunFilledIcon, MoonFilledIcon } from '@/components/icons' import { SunFilledIcon, MoonFilledIcon } from "@/components/icons";
export interface ThemeSwitchProps { export interface ThemeSwitchProps {
className?: string className?: string;
classNames?: SwitchProps['classNames'] classNames?: SwitchProps["classNames"];
} }
export const ThemeSwitch: FC<ThemeSwitchProps> = ({ export const ThemeSwitch: FC<ThemeSwitchProps> = ({
className, className,
classNames, classNames,
}) => { }) => {
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme();
const isSSR = useIsSSR() const isSSR = useIsSSR();
const onChange = () => { const onChange = () => {
theme === 'light' ? setTheme('dark') : setTheme('light') theme === "light" ? setTheme("dark") : setTheme("light");
} };
const { const {
Component, Component,
slots, slots,
isSelected, isSelected,
getBaseProps, getBaseProps,
getInputProps, getInputProps,
getWrapperProps, getWrapperProps,
} = useSwitch({ } = useSwitch({
isSelected: theme === 'light' || isSSR, isSelected: theme === "light" || isSSR,
'aria-label': `Switch to ${theme === 'light' || isSSR ? 'dark' : 'light'} mode`, "aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`,
onChange, onChange,
}) });
return ( return (
<Component <Component
{...getBaseProps({ {...getBaseProps({
className: clsx( className: clsx(
'px-px transition-opacity hover:opacity-80 cursor-pointer', "px-px transition-opacity hover:opacity-80 cursor-pointer",
className, className,
classNames?.base classNames?.base,
), ),
})} })}
> >
<VisuallyHidden> <VisuallyHidden>
<input {...getInputProps()} /> <input {...getInputProps()} />
</VisuallyHidden> </VisuallyHidden>
<div <div
{...getWrapperProps()} {...getWrapperProps()}
className={slots.wrapper({ className={slots.wrapper({
class: clsx( class: clsx(
[ [
'w-auto h-auto', "w-auto h-auto",
'bg-transparent', "bg-transparent",
'rounded-lg', "rounded-lg",
'flex items-center justify-center', "flex items-center justify-center",
'group-data-[selected=true]:bg-transparent', "group-data-[selected=true]:bg-transparent",
'!text-default-500', "!text-default-500",
'pt-px', "pt-px",
'px-0', "px-0",
'mx-0', "mx-0",
], ],
classNames?.wrapper classNames?.wrapper,
), ),
})} })}
> >
{!isSelected || isSSR ? ( {!isSelected || isSSR ? (
<SunFilledIcon size={22} /> <SunFilledIcon size={22} />
) : ( ) : (
<MoonFilledIcon size={22} /> <MoonFilledIcon size={22} />
)} )}
</div> </div>
</Component> </Component>
) );
} };

View File

@ -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({ export const fontSans = Inter({
subsets: ['latin'], subsets: ["latin"],
variable: '--font-sans', variable: "--font-sans",
}) });
export const fontMono = FontMono({ export const fontMono = FontMono({
subsets: ['latin'], subsets: ["latin"],
variable: '--font-mono', variable: "--font-mono",
}) });

View File

@ -1,24 +1,24 @@
export type SiteConfig = typeof siteConfig export type SiteConfig = typeof siteConfig;
export const siteConfig = { export const siteConfig = {
name: 'Blackboard', name: "Blackboard",
description: 'Overview of the day', description: "Overview of the day",
navItems: [ navItems: [
{ {
label: '📝 Todo', label: "📝 Todo",
href: '/todo', href: "/todo",
}, },
{ {
label: '🍔 Food Glance', label: "🍔 Food Glance",
href: '/food', href: "/food",
}, },
{ {
label: '🤖 Study Rundown', label: "🤖 Study Rundown",
href: '/study', href: "/study",
}, },
{ {
label: 'Weather', label: "☁️ Weather",
href: '/weather', href: "/weather",
}, },
], ],
} };

View File

@ -1,15 +1,6 @@
// next.config.js // next.config.js
const UnoCSS = require('@unocss/webpack').default
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {};
reactStrictMode: true,
webpack: (config) => {
config.plugins.push(
UnoCSS(),
)
return config
},
}
module.exports = nextConfig module.exports = nextConfig;

View File

@ -1,56 +1,60 @@
{ {
"name": "next-app-template", "name": "next-app-template",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbo", "dev": "next dev --turbo",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix" "lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix"
}, },
"dependencies": { "dependencies": {
"@nextui-org/button": "2.0.38", "@nextui-org/button": "2.0.38",
"@nextui-org/code": "2.0.33", "@nextui-org/code": "2.0.33",
"@nextui-org/input": "2.2.5", "@nextui-org/input": "2.2.5",
"@nextui-org/kbd": "2.0.34", "@nextui-org/kbd": "2.0.34",
"@nextui-org/link": "2.0.35", "@nextui-org/link": "2.0.35",
"@nextui-org/listbox": "2.1.27", "@nextui-org/listbox": "2.1.27",
"@nextui-org/navbar": "2.0.37", "@nextui-org/navbar": "2.0.37",
"@nextui-org/react": "^2.4.8", "@nextui-org/react": "^2.4.8",
"@nextui-org/snippet": "2.0.43", "@nextui-org/snippet": "2.0.43",
"@nextui-org/switch": "2.0.34", "@nextui-org/switch": "2.0.34",
"@nextui-org/system": "2.2.6", "@nextui-org/system": "2.2.6",
"@nextui-org/theme": "2.2.11", "@nextui-org/theme": "2.2.11",
"@react-aria/ssr": "3.9.4", "@react-aria/ssr": "3.9.4",
"@react-aria/visually-hidden": "3.8.12", "@react-aria/visually-hidden": "3.8.12",
"clsx": "2.1.1", "@unocss/webpack": "^0.63.6",
"framer-motion": "~11.1.1", "clsx": "2.1.1",
"intl-messageformat": "^10.5.0", "framer-motion": "~11.1.1",
"next": "14.2.4", "intl-messageformat": "^10.5.0",
"next-themes": "^0.2.1", "next": "^15.0.2",
"react": "18.3.1", "next-themes": "^0.2.1",
"react-dom": "18.3.1" "react": "18.3.1",
}, "react-dom": "18.3.1",
"devDependencies": { "react-icons": "^5.3.0"
"@types/node": "20.5.7", },
"@types/react": "18.3.3", "devDependencies": {
"@types/react-dom": "18.3.0", "@types/node": "20.5.7",
"@typescript-eslint/eslint-plugin": "7.2.0", "@types/react": "18.3.3",
"@typescript-eslint/parser": "7.2.0", "@types/react-dom": "18.3.0",
"autoprefixer": "10.4.19", "@typescript-eslint/eslint-plugin": "7.2.0",
"eslint": "^8.57.0", "@typescript-eslint/parser": "7.2.0",
"eslint-config-next": "14.2.1", "@unocss/postcss": "^0.63.6",
"eslint-config-prettier": "^8.2.0", "autoprefixer": "10.4.19",
"eslint-plugin-import": "^2.26.0", "eslint": "^8.57.0",
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-config-next": "14.2.1",
"eslint-plugin-node": "^11.1.0", "eslint-config-prettier": "^8.2.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-react": "^7.23.2", "eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-unused-imports": "^3.2.0", "eslint-plugin-prettier": "^5.1.3",
"postcss": "8.4.38", "eslint-plugin-react": "^7.23.2",
"tailwind-variants": "0.1.20", "eslint-plugin-react-hooks": "^4.6.0",
"tailwindcss": "3.4.3", "eslint-plugin-unused-imports": "^3.2.0",
"typescript": "5.0.4" "postcss": "8.4.38",
} "tailwind-variants": "0.1.20",
"tailwindcss": "3.4.3",
"typescript": "5.0.4",
"unocss": "^0.63.6"
}
} }

View File

@ -1,6 +1,10 @@
module.exports = { module.exports = {
plugins: { plugins: {
"@unocss/postcss": {
// Optional
content: ["**/*.{html,js,ts,jsx,tsx}"],
},
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@ -1,7 +1,3 @@
@import '@unocss/reset/tailwind.css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@unocss all;

View File

@ -1,11 +1,11 @@
import {nextui} from '@nextui-org/theme' import { nextui } from "@nextui-org/theme";
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: [ content: [
'./components/**/*.{js,ts,jsx,tsx,mdx}', "./components/**/*.{js,ts,jsx,tsx,mdx}",
'./app/**/*.{js,ts,jsx,tsx,mdx}', "./app/**/*.{js,ts,jsx,tsx,mdx}",
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}' "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
], ],
theme: { theme: {
extend: { extend: {
@ -17,5 +17,5 @@ module.exports = {
}, },
darkMode: "class", darkMode: "class",
darkMode: "class", darkMode: "class",
plugins: [nextui()], plugins: [nextui()],
} };

View File

@ -1,5 +1,5 @@
import { SVGProps } from 'react' import { SVGProps } from "react";
export type IconSvgProps = SVGProps<SVGSVGElement> & { export type IconSvgProps = SVGProps<SVGSVGElement> & {
size?: number size?: number;
} };

View File

@ -1,3 +1,4 @@
// @filename uno.config.ts
import { defineConfig, presetUno } from "unocss"; import { defineConfig, presetUno } from "unocss";
export default defineConfig({ export default defineConfig({