main
Oliver Schütz 2024-10-31 14:23:21 +01:00
parent 2c69edd489
commit 0a4af50d08
32 changed files with 891 additions and 602 deletions

View File

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

Binary file not shown.

View File

@ -1,61 +1,90 @@
# course_content_extractor.py
import os
import zipfile
import shutil
import tempfile
import subprocess
import sys
import re
import unicodedata
class CourseContentExtractor:
def __init__(self, download_dir, output_dir=None):
def __init__(self, download_dir, root_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):
# Ensure output_dir exists
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir)
def extract_contents(self, courses):
# Ensure root_dir exists
if not os.path.exists(self.root_dir):
os.makedirs(self.root_dir)
# Find all ZIP files in download_dir
# 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
course_info = next((course for course in courses if course['CourseName'] == base_name), None)
if not course_info:
print(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
study_program = course_info['StudyProgram']
semester = course_info['Semester']
course_name = course_info['CourseName']
# Create a temporary directory for extraction
course_output_dir = os.path.join(
self.root_dir,
study_program,
semester,
course_name
)
# Create subfolders
subfolders = ['Lectures', 'Notes', 'Summary', 'Tasks']
for subfolder in subfolders:
subfolder_path = os.path.join(course_output_dir, subfolder)
os.makedirs(subfolder_path, exist_ok=True)
# Create 'Code_files' subfolder under 'Tasks/<task_name>'
task_name = 'Task1' # Adjust as needed or make dynamic
code_files_path = os.path.join(course_output_dir, 'Tasks', task_name, 'Code_files')
os.makedirs(code_files_path, exist_ok=True)
# Extract and organize files
with tempfile.TemporaryDirectory() as temp_extract_dir:
# Extract ZIP file to temporary directory
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(temp_extract_dir)
# Walk through the extracted files
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')
shutil.copy2(file_path, dest_folder)
elif file.lower().endswith(('.ppt', '.pptx')):
# Convert PowerPoint files to PDF
self.convert_ppt_to_pdf(file_path, course_output_dir)
self.convert_ppt_to_pdf(file_path, os.path.join(course_output_dir, 'Lectures'))
elif file.lower().endswith(('.py', '.java', '.cpp', '.c', '.js', '.html', '.css')):
# Example: Place code files into 'Tasks/<task_name>/Code_files'
shutil.copy2(file_path, code_files_path)
else:
# Handle other file types or skip
pass
# Delete the ZIP file after processing
os.remove(zip_path)
print(f"All PDF and PowerPoint files have been extracted to {self.output_dir}")
print(f"All files have been extracted to {self.root_dir}")
def convert_ppt_to_pdf(self, ppt_path, output_dir):
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
@ -77,3 +106,12 @@ class CourseContentExtractor:
print(f"{office_executable} is not installed or not found in the system path.")
# Optionally, copy the original PPT/PPTX file
shutil.copy2(ppt_path, output_dir)
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)
return sanitized[:100]

View File

@ -0,0 +1,12 @@
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:
# User-defined task_name
<task_name>: # The specific task or assignment name, defined by the user (e.g., Task1)
Code_files: [] # Folder for code files related to the specific task (relative to the user-specified root path, e.g., <root_path>/Computational_and_Data_Science/HS24/cds-201_Programmierung und Prompt Engineering/Tasks/Task1/Code_files)

View File

@ -1,41 +1,47 @@
# main.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
# 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():
# 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.")
return
if not USERNAME or not PASSWORD:
print("Please set the MOODLE_USERNAME and MOODLE_PASSWORD environment variables.")
exit(1)
# Use system temporary directory for downloads
with tempfile.TemporaryDirectory() as download_dir:
print(f"Using temporary download directory: {download_dir}")
# Create an instance of MoodleDownloader
downloader = MoodleDownloader(USERNAME, PASSWORD, headless=True)
# 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.")
return
# Initialize downloader
downloader = MoodleDownloader(username, password, download_dir=download_dir, headless=True)
try:
# Login to Moodle
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
downloader.close()
# Initialize extractor
extractor = CourseContentExtractor(download_dir=download_dir, root_dir=root_dir)
extractor.extract_contents(downloader.courses)
# Temporary directory is automatically cleaned up here
print("Temporary download directory has been cleaned up.")
if __name__ == "__main__":
main()

View File

@ -1,6 +1,7 @@
# moodle_downloader.py
import os
import re
import time
import logging
import requests
import unicodedata
@ -13,20 +14,13 @@ 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):
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 = []
@ -142,25 +136,24 @@ class MoodleDownloader:
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 code and term
short_name = self.extract_course_code_and_term(course_name)
# Extract course info
course_info = self.extract_course_info(full_text)
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({
'StudyProgram': course_info['study_program'],
'Semester': course_info['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,29 +165,29 @@ 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)
def extract_course_info(self, course_title):
# Example course title: 'Programmierung und Prompt Engineering (cds-201) HS24'
pattern = r'^(.*?)\s*\(([^)]+)\)\s*(\w+\d*)$'
match = re.search(pattern, course_title)
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)}"
study_program = 'Computational_and_Data_Science' # Replace with your actual study program if different
course_full_name = match.group(1).strip()
course_code = match.group(2).strip()
semester = match.group(3).strip()
course_identifier = f"{course_code}_{self.sanitize_filename(course_full_name)}"
return {
'study_program': study_program,
'semester': semester,
'course_name': course_identifier
}
else:
# If pattern doesn't match, return sanitized course name
return self.sanitize_filename(course_name)
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]
# Handle cases where the pattern doesn't match
sanitized_title = self.sanitize_filename(course_title)
return {
'study_program': 'Unknown_Program',
'semester': 'Unknown_Semester',
'course_name': sanitized_title
}
def download_all_courses(self):
if not self.courses:
@ -262,18 +255,8 @@ 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
# Determine filename
filename = f"{course_name}.zip"
filename = self.sanitize_filename(filename)
filepath = os.path.join(self.download_dir, filename)
# Overwrite existing files
@ -290,10 +273,17 @@ class MoodleDownloader:
logging.error(f"Error downloading course '{course_name}': {e}", exc_info=True)
continue
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]
def close(self):
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()

View File

@ -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": {

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

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

View File

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

View File

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

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

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

View File

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

View File

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

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({
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",
});

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,3 @@
@import '@unocss/reset/tailwind.css';
@tailwind base;
@tailwind components;
@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} */
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()],
}
};

View File

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

View File

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