Skip to content

shrutibhise2002-debug/playschool-porta

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 

Repository files navigation

<title>Little Learners Portal</title> <script src="proxy.php?url=https%3A%2F%2Fgithub.com%2F%3Ca+href%3D"https://cdn.tailwindcss.com"></script>" rel="nofollow">https://cdn.tailwindcss.com"></script> <script type="module"> import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js"; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"; import { getFirestore, collection, addDoc, query, orderBy, onSnapshot, serverTimestamp, setLogLevel, doc, setDoc, getDoc, getDocs, updateDoc, where } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
    // Firebase configuration and global variables
    const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
    const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {
        apiKey: "your-api-key",
        authDomain: "your-auth-domain",
        projectId: "your-project-id"
    };
    const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;

    // Logging for debugging
    setLogLevel('debug');

    // IMPORTANT: Replace this with your actual User ID after signing in for the first time.
    // Copy your User ID from the top right corner of the app.
    const PRINCIPAL_USER_ID = 'YOUR_PRINCIPAL_ID_HERE';

    // Gemini API configuration
    const apiKey = ""; 
    const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`;

    let app, db, auth;
    let userId = 'loading...';
    let userName = 'Anonymous';
    let isAuthReady = false;
    let isPrincipal = false;

    const initFirebase = async () => {
        app = initializeApp(firebaseConfig);
        db = getFirestore(app);
        auth = getAuth(app);

        onAuthStateChanged(auth, async (user) => {
            if (user) {
                userId = user.uid;
                isPrincipal = (userId === PRINCIPAL_USER_ID);
                await fetchUserName(userId);
            } else {
                userId = 'not-signed-in';
                isAuthReady = true;
            }
        });

        try {
            if (initialAuthToken) {
                await signInWithCustomToken(auth, initialAuthToken);
            } else {
                await signInAnonymously(auth);
            }
        } catch (error) {
            console.error("Firebase Auth Error:", error);
        }
    };

    // UI elements
    const messagesContainer = document.getElementById('messagesContainer');
    const messageInput = document.getElementById('messageInput');
    const sendButton = document.getElementById('sendButton');
    const loadingSpinner = document.getElementById('loadingSpinner');
    const nameInputContainer = document.getElementById('nameInputContainer');
    const nameInput = document.getElementById('nameInput');
    const saveNameButton = document.getElementById('saveNameButton');
    const userIdDisplay = document.getElementById('userIdDisplay');
    const userNameDisplay = document.getElementById('userNameDisplay');
    const chatTab = document.getElementById('chatTab');
    const feesTab = document.getElementById('feesTab');
    const lessonPlanTab = document.getElementById('lessonPlanTab');
    const chatSection = document.getElementById('chatSection');
    const feesSection = document.getElementById('feesSection');
    const lessonPlanSection = document.getElementById('lessonPlanSection');
    const feesPrincipalView = document.getElementById('feesPrincipalView');
    const feesParentView = document.getElementById('feesParentView');
    const studentNameInput = document.getElementById('studentNameInput');
    const feesAmountInput = document.getElementById('feesAmountInput');
    const feesDueDateInput = document.getElementById('feesDueDateInput');
    const addFeesButton = document.getElementById('addFeesButton');
    const feesListContainer = document.getElementById('feesListContainer');
    const parentFeesCard = document.getElementById('parentFeesCard');
    const feesStatusBadge = document.getElementById('feesStatusBadge');
    const feesNotificationBadge = document.getElementById('feesNotificationBadge');
    const parentFeeText = document.getElementById('parentFeeText');
    const markAsPaidButton = document.getElementById('markAsPaidButton');
    const assistantTypingIndicator = document.getElementById('assistantTypingIndicator');
    const lessonPlanInput = document.getElementById('lessonPlanInput');
    const generatePlanButton = document.getElementById('generatePlanButton');
    const lessonPlanOutput = document.getElementById('lessonPlanOutput');
    const copyPlanButton = document.getElementById('copyPlanButton');
    const lessonPlanLoading = document.getElementById('lessonPlanLoading');
    const lessonPlanContainer = document.getElementById('lessonPlanContainer');

    let currentParentFeeId = null;

    // Tabs functionality
    const showSection = (sectionId) => {
        chatSection.classList.add('hidden');
        feesSection.classList.add('hidden');
        lessonPlanSection.classList.add('hidden');
        chatTab.classList.remove('bg-blue-600', 'text-white');
        feesTab.classList.remove('bg-blue-600', 'text-white');
        lessonPlanTab.classList.remove('bg-blue-600', 'text-white');
        if (sectionId === 'chat') {
            chatSection.classList.remove('hidden');
            chatTab.classList.add('bg-blue-600', 'text-white');
        } else if (sectionId === 'fees') {
            feesSection.classList.remove('hidden');
            feesTab.classList.add('bg-blue-600', 'text-white');
        } else if (sectionId === 'lesson-plan') {
            lessonPlanSection.classList.remove('hidden');
            lessonPlanTab.classList.add('bg-blue-600', 'text-white');
        }
    };

    chatTab.addEventListener('click', () => showSection('chat'));
    feesTab.addEventListener('click', () => showSection('fees'));
    lessonPlanTab.addEventListener('click', () => showSection('lesson-plan'));

    // Function to fetch and set the user's name
    const fetchUserName = async (id) => {
        const userProfilePath = `artifacts/${appId}/users/${id}/profile`;
        const profileRef = doc(db, userProfilePath, 'details');
        try {
            const docSnap = await getDoc(profileRef);
            if (docSnap.exists()) {
                userName = docSnap.data().name;
                nameInputContainer.classList.add('hidden');
            } else {
                nameInputContainer.classList.remove('hidden');
            }
            userIdDisplay.textContent = isPrincipal ? `Principal` : `Parent`;
            userNameDisplay.textContent = `(${userName})`;
            isAuthReady = true;
            setupRealtimeListener();
            setupFeesListener();
        } catch (error) {
            console.error("Error fetching user name:", error);
            isAuthReady = true;
            setupRealtimeListener();
            setupFeesListener();
        }
    };

    const saveUserName = async () => {
        const newName = nameInput.value.trim();
        if (newName === '') return;
        const userProfilePath = `artifacts/${appId}/users/${userId}/profile`;
        const profileRef = doc(db, userProfilePath, 'details');
        try {
            await setDoc(profileRef, { name: newName });
            userName = newName;
            userNameDisplay.textContent = `(${userName})`;
            nameInputContainer.classList.add('hidden');
        } catch (error) {
            console.error("Error saving user name:", error);
        }
    };

    // Chat functionality
    const createMessageElement = (messageData) => {
        const isSentByUser = messageData.userId === userId;
        const isSentByBot = messageData.userId === 'bot';
        const messageElement = document.createElement('div');
        messageElement.className = `flex mb-4 ${isSentByUser ? 'justify-end' : 'justify-start'}`;
        const messageBubble = document.createElement('div');
        messageBubble.className = `max-w-[70%] p-3 rounded-xl shadow-md ${isSentByUser ? 'bg-blue-500 text-white rounded-br-none' : isSentByBot ? 'bg-indigo-200 text-gray-900 rounded-bl-none' : 'bg-gray-200 text-gray-800 rounded-bl-none'}`;
        messageBubble.style.minWidth = '5rem';
        const sender = document.createElement('div');
        sender.className = 'text-xs font-semibold mb-1 opacity-70';
        sender.textContent = isSentByUser ? `You (${userName})` : isSentByBot ? 'Little Learners Assistant' : `${messageData.userName}:`;
        const text = document.createElement('div');
        text.className = 'break-words';
        text.textContent = messageData.text;
        messageBubble.appendChild(sender);
        messageBubble.appendChild(text);
        messageElement.appendChild(messageBubble);
        return messageElement;
    };

    const setupRealtimeListener = () => {
        if (!isAuthReady || !db) return;
        const publicDataPath = `artifacts/${appId}/public/data/messages`;
        const q = query(collection(db, publicDataPath), orderBy('timestamp'));
        onSnapshot(q, (querySnapshot) => {
            messagesContainer.innerHTML = '';
            querySnapshot.forEach((doc) => {
                const messageData = doc.data();
                const messageElement = createMessageElement(messageData);
                messagesContainer.appendChild(messageElement);
            });
            messagesContainer.scrollTop = messagesContainer.scrollHeight;
            loadingSpinner.classList.add('hidden');
        }, (error) => {
            console.error("Error fetching messages:", error);
            loadingSpinner.classList.add('hidden');
        });
    };

    const getBotResponse = async (userQuery) => {
        assistantTypingIndicator.classList.remove('hidden');
        const systemPrompt = "You are a friendly and helpful playschool assistant named Little Learners Assistant. Your purpose is to provide brief, general information about the school's events, timings, and policies. If you don't know the answer, politely suggest they contact the principal.";
        const payload = {
            contents: [{ parts: [{ text: userQuery }] }],
            systemInstruction: { parts: [{ text: systemPrompt }] },
        };
        try {
            const response = await fetch(apiUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
            });
            const result = await response.json();
            const botReply = result?.candidates?.[0]?.content?.parts?.[0]?.text || "I'm sorry, I couldn't process that request. Please try again or contact the principal.";
            await addDoc(collection(db, `artifacts/${appId}/public/data/messages`), {
                text: botReply,
                userId: 'bot',
                userName: 'Little Learners Assistant',
                timestamp: serverTimestamp()
            });
        } catch (error) {
            console.error("Error getting bot response:", error);
        } finally {
            assistantTypingIndicator.classList.add('hidden');
        }
    };

    const sendMessage = async () => {
        const messageText = messageInput.value.trim();
        if (messageText === '' || !userId) return;
        const publicDataPath = `artifacts/${appId}/public/data/messages`;
        const messagePayload = {
            text: messageText,
            userId: userId,
            userName: userName,
            timestamp: serverTimestamp()
        };
        try {
            await addDoc(collection(db, publicDataPath), messagePayload);
            if (!isPrincipal && messageText.toLowerCase().startsWith('@assistant')) {
                getBotResponse(messageText.substring('@assistant'.length).trim());
            }
            messageInput.value = '';
        } catch (error) {
            console.error("Error sending message:", error);
        }
    };

    // Fees functionality
    const setupFeesListener = () => {
        if (!isAuthReady || !db) return;
        if (isPrincipal) {
            feesPrincipalView.classList.remove('hidden');
            feesParentView.classList.add('hidden');
            listenForPrincipalFees();
        } else {
            feesPrincipalView.classList.add('hidden');
            feesParentView.classList.remove('hidden');
            listenForParentFees();
        }
    };

    const listenForPrincipalFees = () => {
        const feesPath = `artifacts/${appId}/public/data/fees`;
        const q = query(collection(db, feesPath), orderBy('timestamp', 'desc'));
        onSnapshot(q, (querySnapshot) => {
            feesListContainer.innerHTML = '';
            querySnapshot.forEach((doc) => {
                const feeData = doc.data();
                const feeId = doc.id;
                const feeElement = document.createElement('div');
                feeElement.className = `p-4 mb-2 rounded-xl shadow-md flex items-center justify-between ${feeData.paid ? 'bg-green-100' : 'bg-red-100'}`;
                feeElement.innerHTML = `
                    <div>
                        <div class="font-bold text-lg">${feeData.studentName}</div>
                        <div class="text-sm">Amount: $${feeData.amount}</div>
                        <div class="text-xs text-gray-500">Due: ${feeData.dueDate}</div>
                    </div>
                    <span class="px-3 py-1 text-sm font-semibold rounded-full ${feeData.paid ? 'bg-green-500 text-white' : 'bg-red-500 text-white'}">
                        ${feeData.paid ? 'Paid' : 'Unpaid'}
                    </span>
                `;
                feesListContainer.appendChild(feeElement);
            });
        }, (error) => {
            console.error("Error listening for principal fees:", error);
        });
    };

    const addFees = async () => {
        const studentName = studentNameInput.value.trim();
        const amount = feesAmountInput.value.trim();
        const dueDate = feesDueDateInput.value.trim();
        if (!studentName || !amount || !dueDate) {
            console.error("Please fill all fields.");
            return;
        }
        const feesPath = `artifacts/${appId}/public/data/fees`;
        const feePayload = {
            studentName: studentName,
            amount: parseFloat(amount),
            dueDate: dueDate,
            paid: false,
            timestamp: serverTimestamp()
        };
        try {
            await addDoc(collection(db, feesPath), feePayload);
            studentNameInput.value = '';
            feesAmountInput.value = '';
            feesDueDateInput.value = '';
        } catch (error) {
            console.error("Error adding fees:", error);
        }
    };

    const listenForParentFees = () => {
        const feesPath = `artifacts/${appId}/public/data/fees`;
        const q = query(collection(db, feesPath), where('studentName', '==', userName));

        onSnapshot(q, (querySnapshot) => {
            let foundFee = false;
            querySnapshot.forEach((doc) => {
                const feeData = doc.data();
                currentParentFeeId = doc.id;
                parentFeesCard.classList.remove('hidden');
                feesNotificationBadge.classList.add('hidden');
                if (feeData.paid) {
                    feesStatusBadge.className = 'px-3 py-1 text-sm font-semibold rounded-full bg-green-500 text-white';
                    feesStatusBadge.textContent = 'Paid';
                    markAsPaidButton.classList.add('hidden');
                } else {
                    feesStatusBadge.className = 'px-3 py-1 text-sm font-semibold rounded-full bg-red-500 text-white animate-pulse';
                    feesStatusBadge.textContent = 'Unpaid';
                    feesNotificationBadge.classList.remove('hidden');
                    markAsPaidButton.classList.remove('hidden');
                }
                parentFeeText.textContent = `Amount: $${feeData.amount} (Due: ${feeData.dueDate})`;
                foundFee = true;
            });
            if (!foundFee) {
                parentFeesCard.classList.add('hidden');
                feesNotificationBadge.classList.add('hidden');
            }
        }, (error) => {
            console.error("Error listening for parent fees:", error);
        });
    };

    const markFeeAsPaid = async () => {
        if (!currentParentFeeId) return;
        const feesPath = `artifacts/${appId}/public/data/fees`;
        const feeRef = doc(db, feesPath, currentParentFeeId);
        try {
            await updateDoc(feeRef, { paid: true });
            markAsPaidButton.classList.add('hidden');
        } catch (error) {
            console.error("Error marking fee as paid:", error);
        }
    };

    // Lesson Plan functionality
    const generateLessonPlan = async () => {
        const theme = lessonPlanInput.value.trim();
        if (!theme) return;
        lessonPlanOutput.textContent = '';
        lessonPlanContainer.classList.add('hidden');
        lessonPlanLoading.classList.remove('hidden');
        copyPlanButton.classList.add('hidden');
        
        const systemPrompt = "You are an expert in early childhood education. Your task is to create a detailed, engaging lesson plan for a preschool class based on a given theme. The plan should be well-structured and include three sections: 'Circle Time & Story', 'Creative Activity', and 'Gross Motor Play'. Use a friendly, encouraging tone. Start the response with a title.";
        const userQuery = `Create a lesson plan with the theme: ${theme}.`;

        const payload = {
            contents: [{ parts: [{ text: userQuery }] }],
            systemInstruction: { parts: [{ text: systemPrompt }] },
        };

        try {
            const response = await fetch(apiUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
            });
            const result = await response.json();
            const generatedText = result?.candidates?.[0]?.content?.parts?.[0]?.text || "An error occurred while generating the lesson plan. Please try again.";
            
            lessonPlanOutput.textContent = generatedText;
            lessonPlanContainer.classList.remove('hidden');
            copyPlanButton.classList.remove('hidden');
        } catch (error) {
            console.error("Error generating lesson plan:", error);
            lessonPlanOutput.textContent = "An error occurred. Please try again.";
        } finally {
            lessonPlanLoading.classList.add('hidden');
        }
    };

    const copyLessonPlan = () => {
        const textToCopy = lessonPlanOutput.textContent;
        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(textToCopy)
                .then(() => {
                    copyPlanButton.textContent = 'Copied!';
                    setTimeout(() => copyPlanButton.textContent = 'Copy to Clipboard', 2000);
                })
                .catch(err => {
                    console.error('Could not copy text: ', err);
                });
        } else {
            const tempTextArea = document.createElement('textarea');
            tempTextArea.value = textToCopy;
            document.body.appendChild(tempTextArea);
            tempTextArea.select();
            document.execCommand('copy');
            document.body.removeChild(tempTextArea);
            copyPlanButton.textContent = 'Copied!';
            setTimeout(() => copyPlanButton.textContent = 'Copy to Clipboard', 2000);
        }
    };

    // Event listeners
    sendButton.addEventListener('click', sendMessage);
    messageInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') sendMessage();
    });
    saveNameButton.addEventListener('click', saveUserName);
    nameInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') saveUserName();
    });
    addFeesButton.addEventListener('click', addFees);
    markAsPaidButton.addEventListener('click', markFeeAsPaid);
    generatePlanButton.addEventListener('click', generateLessonPlan);
    copyPlanButton.addEventListener('click', copyLessonPlan);

    // Initialize the app on window load
    window.onload = () => {
        initFirebase();
        showSection('chat');
    };
</script>

Little Learners Portal

Loading...
    <!-- Tabs -->
    <div class="flex bg-blue-500 text-white text-center rounded-t-lg shadow-lg">
        <button id="chatTab" class="flex-1 p-4 font-bold transition-all duration-200 hover:bg-blue-600 focus:outline-none relative">
            Chat
        </button>
        <button id="feesTab" class="flex-1 p-4 font-bold transition-all duration-200 hover:bg-blue-600 focus:outline-none relative">
            Fees & Payments
            <span id="feesNotificationBadge" class="absolute top-2 right-2 px-2 py-1 bg-red-500 rounded-full text-xs font-bold leading-none hidden">!</span>
        </button>
        <button id="lessonPlanTab" class="flex-1 p-4 font-bold transition-all duration-200 hover:bg-blue-600 focus:outline-none relative">
            Lesson Plans
        </button>
    </div>

    <!-- Name Input -->
    <div id="nameInputContainer" class="p-4 bg-gray-200 hidden">
        <h2 class="text-md font-bold mb-2">Please enter your name to start chatting:</h2>
        <div class="flex">
            <input type="text" id="nameInput" placeholder="Your Name"
                   class="flex-1 p-3 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500">
            <button id="saveNameButton"
                    class="ml-3 p-3 bg-green-500 text-white rounded-full shadow-lg transition-transform duration-200 transform hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-green-500">
                <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
                </svg>
            </button>
        </div>
    </div>

    <!-- Section Containers -->
    <div class="flex-1 flex flex-col p-4 overflow-y-auto">
        <div id="loadingSpinner" class="flex justify-center items-center h-full">
            <div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-500"></div>
        </div>

        <!-- Chat Section -->
        <div id="chatSection" class="flex-1 flex flex-col hidden">
            <div id="messagesContainer" class="flex-1 overflow-y-auto"></div>
            <!-- Typing Indicator -->
            <div id="assistantTypingIndicator" class="flex items-center mb-2 p-2 hidden">
                <div class="w-2 h-2 bg-blue-500 rounded-full mr-1 animate-bounce" style="animation-delay: 0s;"></div>
                <div class="w-2 h-2 bg-blue-500 rounded-full mr-1 animate-bounce" style="animation-delay: 0.2s;"></div>
                <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style="animation-delay: 0.4s;"></div>
            </div>
            <div class="p-4 bg-gray-200 flex items-center rounded-b-lg">
                <input type="text" id="messageInput" placeholder="Write a message..."
                       class="flex-1 p-3 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500">
                <button id="sendButton"
                        class="ml-3 p-3 bg-blue-500 text-white rounded-full shadow-lg transition-transform duration-200 transform hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-blue-500">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7" />
                    </svg>
                </button>
            </div>
        </div>

        <!-- Fees Section -->
        <div id="feesSection" class="flex-1 flex flex-col hidden">
            <!-- Principal's View -->
            <div id="feesPrincipalView" class="hidden flex-1 flex flex-col">
                <div class="bg-blue-100 p-4 rounded-xl mb-4 shadow-md">
                    <h2 class="text-xl font-bold mb-2">Add New Student Fees</h2>
                    <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
                        <input type="text" id="studentNameInput" placeholder="Student Name" class="p-2 rounded-lg border">
                        <input type="number" id="feesAmountInput" placeholder="Amount ($)" class="p-2 rounded-lg border">
                        <input type="date" id="feesDueDateInput" class="p-2 rounded-lg border">
                    </div>
                    <button id="addFeesButton" class="mt-4 w-full bg-green-500 text-white p-3 rounded-lg shadow-lg font-bold hover:bg-green-600 transition-colors">
                        Add Fees
                    </button>
                </div>
                <div class="flex-1 overflow-y-auto">
                    <h2 class="text-xl font-bold mb-2">All Student Fees</h2>
                    <div id="feesListContainer"></div>
                </div>
            </div>

            <!-- Parent's View -->
            <div id="feesParentView" class="hidden flex-1 flex flex-col items-center justify-center">
                <div id="parentFeesCard" class="bg-white p-6 rounded-2xl shadow-2xl text-center hidden w-full">
                    <h2 class="text-2xl font-extrabold mb-4 text-gray-800">Your Child's Fees</h2>
                    <div class="flex items-center justify-center mb-4">
                        <span id="feesStatusBadge" class="px-4 py-2 text-md font-bold rounded-full"></span>
                    </div>
                    <p id="parentFeeText" class="text-lg text-gray-600 mb-6"></p>
                    <button id="markAsPaidButton" class="w-full bg-indigo-500 text-white p-4 rounded-lg font-bold text-lg shadow-lg hover:bg-indigo-600 transition-colors hidden">
                        Mark as Paid
                    </button>
                </div>
                <p id="noFeesMessage" class="text-gray-500 text-lg hidden">No outstanding fees at this time.</p>
            </div>
        </div>

        <!-- Lesson Plan Section -->
        <div id="lessonPlanSection" class="flex-1 flex-col hidden">
            <div class="bg-purple-100 p-4 rounded-xl mb-4 shadow-md">
                <h2 class="text-xl font-bold mb-2">Generate a Lesson Plan</h2>
                <div class="flex items-center space-x-2">
                    <input type="text" id="lessonPlanInput" placeholder="e.g., 'jungle animals' or 'seasons'" class="flex-1 p-2 rounded-lg border">
                    <button id="generatePlanButton" class="p-2 bg-purple-500 text-white rounded-lg shadow-lg font-bold hover:bg-purple-600 transition-colors">
                        Generate Plan ✨
                    </button>
                </div>
            </div>
            <div id="lessonPlanLoading" class="flex justify-center items-center h-48 hidden">
                <div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-purple-500"></div>
            </div>
            <div id="lessonPlanContainer" class="bg-white p-4 rounded-xl shadow-md hidden flex-1 flex-col">
                <pre id="lessonPlanOutput" class="whitespace-pre-wrap font-sans text-sm text-gray-700 leading-relaxed"></pre>
                <button id="copyPlanButton" class="mt-4 w-full bg-gray-200 text-gray-800 p-3 rounded-lg font-bold hover:bg-gray-300 transition-colors">
                    Copy to Clipboard
                </button>
            </div>
        </div>
    </div>
</div>

About

A web-based portal for playschool communication, including chat, fees management, and lesson plan generation." "A simple, single-file web application for the Little Learners Playschool parent-principal portal." "Little Learners Playschool Portal: An all-in-one app for parents and administrators."

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages