v1
This commit is contained in:
5
.env.sample
Normal file
5
.env.sample
Normal file
@@ -0,0 +1,5 @@
|
||||
# MongoDB connection URI
|
||||
MONGODB_URI=mongodb://localhost/beanstalk
|
||||
|
||||
# API domain for S3 file downloads
|
||||
API_DOMAIN=https://app.example.tech
|
||||
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
/public/observations
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
# .env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
README.md
Normal file
36
README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
18
eslint.config.mjs
Normal file
18
eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
68
import-to-mongodb.js
Normal file
68
import-to-mongodb.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const { MongoClient } = require('mongodb');
|
||||
const fs = require('fs');
|
||||
|
||||
// MongoDB connection string - update with your actual connection details
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const DATABASE_NAME = 'beanstalk';
|
||||
|
||||
async function importJSONToMongoDB() {
|
||||
const client = new MongoClient(MONGODB_URI);
|
||||
|
||||
try {
|
||||
// Connect to MongoDB
|
||||
await client.connect();
|
||||
console.log('Connected to MongoDB');
|
||||
|
||||
const db = client.db(DATABASE_NAME);
|
||||
|
||||
// Import indicators
|
||||
console.log('Importing indicators...');
|
||||
const indicatorsData = JSON.parse(fs.readFileSync('./indicators.json', 'utf8'));
|
||||
|
||||
if (indicatorsData.data && Array.isArray(indicatorsData.data)) {
|
||||
// Clear existing collection
|
||||
await db.collection('indicators').deleteMany({});
|
||||
console.log('Cleared existing indicators collection');
|
||||
|
||||
// Insert new data
|
||||
const result = await db.collection('indicators').insertMany(indicatorsData.data);
|
||||
console.log(`Inserted ${result.insertedCount} indicators`);
|
||||
} else {
|
||||
console.log('No valid data found in indicators.json');
|
||||
}
|
||||
|
||||
// Import learning areas
|
||||
console.log('Importing learning areas...');
|
||||
const learningAreasData = JSON.parse(fs.readFileSync('./learning_areas.json', 'utf8'));
|
||||
|
||||
if (learningAreasData.data && Array.isArray(learningAreasData.data)) {
|
||||
// Clear existing collection
|
||||
await db.collection('learning_areas').deleteMany({});
|
||||
console.log('Cleared existing learning_areas collection');
|
||||
|
||||
// Insert new data
|
||||
const result = await db.collection('learning_areas').insertMany(learningAreasData.data);
|
||||
console.log(`Inserted ${result.insertedCount} learning areas`);
|
||||
} else {
|
||||
console.log('No valid data found in learning_areas.json');
|
||||
}
|
||||
|
||||
console.log('Import completed successfully!');
|
||||
|
||||
// Verify the import
|
||||
const indicatorsCount = await db.collection('indicators').countDocuments();
|
||||
const learningAreasCount = await db.collection('learning_areas').countDocuments();
|
||||
|
||||
console.log(`Verification: ${indicatorsCount} indicators in database`);
|
||||
console.log(`Verification: ${learningAreasCount} learning areas in database`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during import:', error);
|
||||
} finally {
|
||||
await client.close();
|
||||
console.log('MongoDB connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
// Run the import
|
||||
importJSONToMongoDB();
|
||||
1
indicators.json
Normal file
1
indicators.json
Normal file
File diff suppressed because one or more lines are too long
1
learning_areas.json
Normal file
1
learning_areas.json
Normal file
File diff suppressed because one or more lines are too long
24
next.config.ts
Normal file
24
next.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
reactCompiler: true,
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'k1-default1.siliconpin.net',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: '192.168.99.180',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
8177
package-lock.json
generated
Normal file
8177
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "observation-import",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"mongodb": "^7.1.0",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
7
postcss.config.mjs
Normal file
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
41
public/data_structure.json
Normal file
41
public/data_structure.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"learningAreas": [
|
||||
"Communication & Language",
|
||||
"Expressive Arts & Design",
|
||||
"Literacy",
|
||||
"Mathematics",
|
||||
"Personal, Social & Emotional Development",
|
||||
"Physical Development",
|
||||
"Physical Development & Aesthetics",
|
||||
"Thinking Creatively & Critically",
|
||||
"Understanding The World"
|
||||
],
|
||||
"subLearningAreas": [
|
||||
"Being Imaginative & Expressive",
|
||||
"Building Theory",
|
||||
"Creating with materials",
|
||||
"Fine Motor & Handwriting",
|
||||
"Health & Self-Care",
|
||||
"Listening & Attention",
|
||||
"Making relationships",
|
||||
"Moving & Handling",
|
||||
"Numbers",
|
||||
"People & Communities",
|
||||
"Reading",
|
||||
"Sense of self",
|
||||
"Shapes, Patterns, Spacial Awareness & Measurements",
|
||||
"Speaking",
|
||||
"Technology",
|
||||
"The World",
|
||||
"Understanding",
|
||||
"Understanding emotions",
|
||||
"Writing"
|
||||
],
|
||||
"frameworks": [
|
||||
"COEL",
|
||||
"EYFS",
|
||||
"KSPK"
|
||||
],
|
||||
"lastUpdated": "2026-02-23T10:21:54.935Z",
|
||||
"totalObservations": 1232
|
||||
}
|
||||
26575
public/db/indicators.json
Normal file
26575
public/db/indicators.json
Normal file
File diff suppressed because it is too large
Load Diff
913
public/db/learning_areas.json
Normal file
913
public/db/learning_areas.json
Normal file
@@ -0,0 +1,913 @@
|
||||
[
|
||||
{
|
||||
"_id": "Personal, Social & Emotional Development",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "664c3a3720040f27ff22d6b0",
|
||||
"subLearningArea": "Making relationships"
|
||||
},
|
||||
{
|
||||
"_id": "664c3a5120040f27ff22d6b4",
|
||||
"subLearningArea": "Sense of self"
|
||||
},
|
||||
{
|
||||
"_id": "664c3a6720040f27ff22d6b8",
|
||||
"subLearningArea": "Understanding emotions"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Communication & Language",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "664ca2b68b8e6aea398e3071",
|
||||
"subLearningArea": "Listening & Attention"
|
||||
},
|
||||
{
|
||||
"_id": "664ca2c48b8e6aea398e3072",
|
||||
"subLearningArea": "Understanding"
|
||||
},
|
||||
{
|
||||
"_id": "664ca2cd8b8e6aea398e3073",
|
||||
"subLearningArea": "Speaking"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Physical Development",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "664ca2e38b8e6aea398e3074",
|
||||
"subLearningArea": "Moving & Handling"
|
||||
},
|
||||
{
|
||||
"_id": "664ca2f18b8e6aea398e3075",
|
||||
"subLearningArea": "Health & Self-Care"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Literacy",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "664ca3048b8e6aea398e3076",
|
||||
"subLearningArea": "Reading"
|
||||
},
|
||||
{
|
||||
"_id": "664ca30f8b8e6aea398e3077",
|
||||
"subLearningArea": "Writing"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Mathematics",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "664ca31f8b8e6aea398e3078",
|
||||
"subLearningArea": "Numbers"
|
||||
},
|
||||
{
|
||||
"_id": "664ca3298b8e6aea398e3079",
|
||||
"subLearningArea": "Shapes, Patterns, Spacial Awareness & Measurements"
|
||||
},
|
||||
{
|
||||
"_id": "698d94aa4399c42eabf156e6",
|
||||
"subLearningArea": "Language of mathematics"
|
||||
},
|
||||
{
|
||||
"_id": "698d94d94399c42eabf156f7",
|
||||
"subLearningArea": "Mathematical concepts: indirect preparation"
|
||||
},
|
||||
{
|
||||
"_id": "698d95244399c42eabf15708",
|
||||
"subLearningArea": "Quantities and symbols 1 to 10"
|
||||
},
|
||||
{
|
||||
"_id": "698d95684399c42eabf1571b",
|
||||
"subLearningArea": "Decimal system"
|
||||
},
|
||||
{
|
||||
"_id": "698d95974399c42eabf1572a",
|
||||
"subLearningArea": "Language of numbers larger than 10"
|
||||
},
|
||||
{
|
||||
"_id": "698d95c44399c42eabf15739",
|
||||
"subLearningArea": "Counting: continuation"
|
||||
},
|
||||
{
|
||||
"_id": "698d95f74399c42eabf15746",
|
||||
"subLearningArea": "Operations"
|
||||
},
|
||||
{
|
||||
"_id": "698d96254399c42eabf15757",
|
||||
"subLearningArea": "Expanding the decimal system: beyond 1000"
|
||||
},
|
||||
{
|
||||
"_id": "698d965d4399c42eabf15768",
|
||||
"subLearningArea": "Memorisation"
|
||||
},
|
||||
{
|
||||
"_id": "698d96914399c42eabf15777",
|
||||
"subLearningArea": "Geometry"
|
||||
},
|
||||
{
|
||||
"_id": "698d96c74399c42eabf1578a",
|
||||
"subLearningArea": "Algebra"
|
||||
},
|
||||
{
|
||||
"_id": "698d96fe4399c42eabf1579b",
|
||||
"subLearningArea": "Time and sequence"
|
||||
},
|
||||
{
|
||||
"_id": "698d9d8a4399c42eabf162a0",
|
||||
"subLearningArea": "Fractions"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Understanding The World",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "664ca3458b8e6aea398e307a",
|
||||
"subLearningArea": "People & Communities"
|
||||
},
|
||||
{
|
||||
"_id": "664ca3518b8e6aea398e307b",
|
||||
"subLearningArea": "The World"
|
||||
},
|
||||
{
|
||||
"_id": "664ca35d8b8e6aea398e307c",
|
||||
"subLearningArea": "Technology"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Expressive Arts & Design",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "664ca3758b8e6aea398e307d",
|
||||
"subLearningArea": "Creating with materials"
|
||||
},
|
||||
{
|
||||
"_id": "664ca3858b8e6aea398e307e",
|
||||
"subLearningArea": "Being Imaginative & Expressive"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Playing & Exploring",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "664ca3de8b8e6aea398e307f",
|
||||
"subLearningArea": "Finding out & exploring"
|
||||
},
|
||||
{
|
||||
"_id": "664ca3f08b8e6aea398e3080",
|
||||
"subLearningArea": "Playing with what they know"
|
||||
},
|
||||
{
|
||||
"_id": "664ca4038b8e6aea398e3081",
|
||||
"subLearningArea": "Being willing to ‘have a go’"
|
||||
},
|
||||
{
|
||||
"_id": "6655946632635c95f10b01b0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Active Learning",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "664ca4258b8e6aea398e3082",
|
||||
"subLearningArea": "Being involved & concentrating"
|
||||
},
|
||||
{
|
||||
"_id": "664ca4358b8e6aea398e3083",
|
||||
"subLearningArea": "Keep on trying"
|
||||
},
|
||||
{
|
||||
"_id": "664ca4478b8e6aea398e3084",
|
||||
"subLearningArea": "Enjoying achieving what they set out to do"
|
||||
},
|
||||
{
|
||||
"_id": "6655949132635c95f10b01b8"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Thinking Creatively & Critically",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "664ca4618b8e6aea398e3085",
|
||||
"subLearningArea": "Creative Thinking"
|
||||
},
|
||||
{
|
||||
"_id": "664ca46d8b8e6aea398e3086",
|
||||
"subLearningArea": "Building Theory"
|
||||
},
|
||||
{
|
||||
"_id": "664ca4828b8e6aea398e3087",
|
||||
"subLearningArea": "Critical Thinking"
|
||||
},
|
||||
{
|
||||
"_id": "665594ba32635c95f10b01c0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "665087b5c601c727f7706122",
|
||||
"subLearningArea": ""
|
||||
},
|
||||
{
|
||||
"_id": "698444ad4360b85ae983c298",
|
||||
"subLearningArea": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Physical & Motor Development",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e79762c2f6370ebe59bd5b",
|
||||
"subLearningArea": "Gross Motor"
|
||||
},
|
||||
{
|
||||
"_id": "68e79786c2f6370ebe59bd64",
|
||||
"subLearningArea": "Fine Motor"
|
||||
},
|
||||
{
|
||||
"_id": "68e797b2c2f6370ebe59bd6d",
|
||||
"subLearningArea": "Health Safety & Nutrition"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Cognitive Development",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e797e0c2f6370ebe59bda3",
|
||||
"subLearningArea": "Exploration & Discovery"
|
||||
},
|
||||
{
|
||||
"_id": "68e79803c2f6370ebe59bdd8",
|
||||
"subLearningArea": "Problem Solving"
|
||||
},
|
||||
{
|
||||
"_id": "68e79822c2f6370ebe59bde5",
|
||||
"subLearningArea": "Early Numeracy"
|
||||
},
|
||||
{
|
||||
"_id": "68e7984bc2f6370ebe59bdf0",
|
||||
"subLearningArea": "Memory & Concept Formation"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Language Communication & Early Literacy",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e79893c2f6370ebe59bdfd",
|
||||
"subLearningArea": "Listening & Understanding"
|
||||
},
|
||||
{
|
||||
"_id": "68e798c4c2f6370ebe59be06",
|
||||
"subLearningArea": "Speaking"
|
||||
},
|
||||
{
|
||||
"_id": "68e798e9c2f6370ebe59be0f",
|
||||
"subLearningArea": "Early Reading"
|
||||
},
|
||||
{
|
||||
"_id": "68e79907c2f6370ebe59be16",
|
||||
"subLearningArea": "Early Writing"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Socio-Emotional Development",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e79938c2f6370ebe59be21",
|
||||
"subLearningArea": "Attachment & Trust"
|
||||
},
|
||||
{
|
||||
"_id": "68e7995bc2f6370ebe59be28",
|
||||
"subLearningArea": "Self-Awareness"
|
||||
},
|
||||
{
|
||||
"_id": "68e79976c2f6370ebe59be31",
|
||||
"subLearningArea": "Emotional Regulation"
|
||||
},
|
||||
{
|
||||
"_id": "68e79996c2f6370ebe59be38",
|
||||
"subLearningArea": "Social Interaction"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Spiritual Moral & Values",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e799b5c2f6370ebe59be41",
|
||||
"subLearningArea": "Awareness of the Creator"
|
||||
},
|
||||
{
|
||||
"_id": "68e799dac2f6370ebe59be4a",
|
||||
"subLearningArea": "Gratitude & Courtesy"
|
||||
},
|
||||
{
|
||||
"_id": "68e799ffc2f6370ebe59be53",
|
||||
"subLearningArea": "Values & Conduct"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Creativity & Aesthetics",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e79a1dc2f6370ebe59be5a",
|
||||
"subLearningArea": "Artistic Expression"
|
||||
},
|
||||
{
|
||||
"_id": "68e79a41c2f6370ebe59be63",
|
||||
"subLearningArea": "Imagination & Play"
|
||||
},
|
||||
{
|
||||
"_id": "68e79a60c2f6370ebe59be6d",
|
||||
"subLearningArea": "Appreciation of Beauty"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Communication",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e8d703f8335116beaf2c6c",
|
||||
"subLearningArea": "Listening & Attention"
|
||||
},
|
||||
{
|
||||
"_id": "68e8d72af8335116beaf2c71",
|
||||
"subLearningArea": "Speaking & Vocabulary"
|
||||
},
|
||||
{
|
||||
"_id": "68e8d752f8335116beaf2c76",
|
||||
"subLearningArea": "Reading (Early Literacy)"
|
||||
},
|
||||
{
|
||||
"_id": "68e8d775f8335116beaf2c7b",
|
||||
"subLearningArea": "Writing (Emergent Writing)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Spirituality & Values",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e8d806f8335116beaf2c80",
|
||||
"subLearningArea": "Gratitude & Courtesy"
|
||||
},
|
||||
{
|
||||
"_id": "68e8d829f8335116beaf2c85",
|
||||
"subLearningArea": "Respect, Tolerance, Compassion"
|
||||
},
|
||||
{
|
||||
"_id": "68e8d850f8335116beaf2c8a",
|
||||
"subLearningArea": "Moral Understanding & Choices"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Humanities",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e8d8edf8335116beaf2c8f",
|
||||
"subLearningArea": "Self, Family & Community"
|
||||
},
|
||||
{
|
||||
"_id": "68e8d910f8335116beaf2c94",
|
||||
"subLearningArea": "Malaysia & National Identity"
|
||||
},
|
||||
{
|
||||
"_id": "68e8d944f8335116beaf2e37",
|
||||
"subLearningArea": "Environment & Sustainability"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Personal Competence",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e8da5ef8335116beaf2e69",
|
||||
"subLearningArea": "Self-Awareness & Confidence"
|
||||
},
|
||||
{
|
||||
"_id": "68e8da7bf8335116beaf2e6e",
|
||||
"subLearningArea": "Social Skills & Collaboration"
|
||||
},
|
||||
{
|
||||
"_id": "68e8da94f8335116beaf2e79",
|
||||
"subLearningArea": "Emotional Regulation"
|
||||
},
|
||||
{
|
||||
"_id": "68e8dac2f8335116beaf2e80",
|
||||
"subLearningArea": "Responsibility & Safety"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Physical Development & Aesthetics",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e8dafdf8335116beaf2e87",
|
||||
"subLearningArea": "Gross Motor (Locomotor)"
|
||||
},
|
||||
{
|
||||
"_id": "68e8db2bf8335116beaf2e8e",
|
||||
"subLearningArea": "Object Control (Manipulative)"
|
||||
},
|
||||
{
|
||||
"_id": "68e8db4bf8335116beaf2e93",
|
||||
"subLearningArea": "Fine Motor & Handwriting"
|
||||
},
|
||||
{
|
||||
"_id": "68e8db6df8335116beaf2e9a",
|
||||
"subLearningArea": "Rhythm, Movement & Expression"
|
||||
},
|
||||
{
|
||||
"_id": "68e8dbb3f8335116beaf2ea1",
|
||||
"subLearningArea": "Health, Hygiene & Nutrition"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Science & Technology",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68e8dbd1f8335116beaf2ea6",
|
||||
"subLearningArea": "Scientific Inquiry Skills"
|
||||
},
|
||||
{
|
||||
"_id": "68e8dbf5f8335116beaf2eab",
|
||||
"subLearningArea": "Materials & Physical World"
|
||||
},
|
||||
{
|
||||
"_id": "68e8dc1ff8335116beaf2eb0",
|
||||
"subLearningArea": "Technology in Daily Life"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Children have a strong sense of identity",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "68ece8b74327021071764158",
|
||||
"subLearningArea": "Children feel safe, secure, and supported"
|
||||
},
|
||||
{
|
||||
"_id": "68ece927432702107176415d",
|
||||
"subLearningArea": "Children develop their emerging autonomy, inter-dependence, resilience, and agency"
|
||||
},
|
||||
{
|
||||
"_id": "69842a93478005ee23edc361",
|
||||
"subLearningArea": "Children develop knowledgeable and confident self-identities, and a positive sense of self-worth"
|
||||
},
|
||||
{
|
||||
"_id": "6984410e4360b85ae983bf65",
|
||||
"subLearningArea": "Children learn to interact in relation to others with care, empathy, and respect"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Children are connected with and contribute to their world",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698441f04360b85ae983bfb3",
|
||||
"subLearningArea": "Children develop a sense of connectedness to groups and communities and an understanding of their reciprocal rights and responsibilities as active and informed citizens"
|
||||
},
|
||||
{
|
||||
"_id": "698442f94360b85ae983c16e",
|
||||
"subLearningArea": "Children respond to diversity with respect"
|
||||
},
|
||||
{
|
||||
"_id": "698445c24360b85ae983c2f1",
|
||||
"subLearningArea": "Children become aware of fairness"
|
||||
},
|
||||
{
|
||||
"_id": "698445fd4360b85ae983c304",
|
||||
"subLearningArea": "Children become socially responsible and show respect for the environment"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Children have a strong sense of wellbeing",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698447a94360b85ae983c48b",
|
||||
"subLearningArea": "Children become strong in their social, emotional, and mental wellbeing"
|
||||
},
|
||||
{
|
||||
"_id": "698449824360b85ae983c960",
|
||||
"subLearningArea": "Children become strong in their physical learning and mental wellbeing"
|
||||
},
|
||||
{
|
||||
"_id": "698449ef4360b85ae983cc5f",
|
||||
"subLearningArea": "Children become strong in their physical learning and wellbeing"
|
||||
},
|
||||
{
|
||||
"_id": "69844a664360b85ae983cc74",
|
||||
"subLearningArea": "Children are aware of and develop strategies to support their own mental and physical health and personal safety"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Children are confident and involved learners",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "69844b3c4360b85ae983cc9f",
|
||||
"subLearningArea": "Children develop dispositions for learning such as curiosity, cooperation, confidence, creativity, commitment, enthusiasm, persistence, imagination, and reflexivity"
|
||||
},
|
||||
{
|
||||
"_id": "698456f64360b85ae983dc08",
|
||||
"subLearningArea": "Children develop a range of skills and processes such as problem-solving, inquiry, experimentation, hypothesising, researching, and investigating"
|
||||
},
|
||||
{
|
||||
"_id": "698457844360b85ae983dc1b",
|
||||
"subLearningArea": "Children transfer and adapt what they have learned from one context to another"
|
||||
},
|
||||
{
|
||||
"_id": "698457ea4360b85ae983dc2c",
|
||||
"subLearningArea": "Children resource their own learning through connecting with people, place, technologies, and natural and processed materials"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Children are effective communicators",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "6984598e4360b85ae983dc5b",
|
||||
"subLearningArea": "Children interact verbally and non-verbally with others for a range of purposes"
|
||||
},
|
||||
{
|
||||
"_id": "698461df4399c42eabeb68d0",
|
||||
"subLearningArea": "Children engage with a range of texts and gain meaning from these texts"
|
||||
},
|
||||
{
|
||||
"_id": "698462754399c42eabeb6a4f",
|
||||
"subLearningArea": "Children express ideas and make meaning using a range of media"
|
||||
},
|
||||
{
|
||||
"_id": "698462c74399c42eabeb6b59",
|
||||
"subLearningArea": "Children begin to understand how symbols and pattern systems work"
|
||||
},
|
||||
{
|
||||
"_id": "6984636a4399c42eabeb6d2f",
|
||||
"subLearningArea": "Children use information and communication technologies to access information, investigate ideas, and represent their thinking"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Development of Movement",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698c1e3f4399c42eabefb541",
|
||||
"subLearningArea": "Development of equilibrium: in supine position"
|
||||
},
|
||||
{
|
||||
"_id": "698c1e964399c42eabefb612",
|
||||
"subLearningArea": "Development of equilibrium: in prone position"
|
||||
},
|
||||
{
|
||||
"_id": "698c1f1c4399c42eabefb900",
|
||||
"subLearningArea": "Control and coordination of body movement"
|
||||
},
|
||||
{
|
||||
"_id": "698c1f6c4399c42eabefba15",
|
||||
"subLearningArea": "Development of the hand grasp"
|
||||
},
|
||||
{
|
||||
"_id": "698c1fa14399c42eabefbbdc",
|
||||
"subLearningArea": "Tactile stimulation"
|
||||
},
|
||||
{
|
||||
"_id": "698c1fe14399c42eabefbc30",
|
||||
"subLearningArea": "Eye-hand coordination"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Language",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698c20624399c42eabefbc61",
|
||||
"subLearningArea": "Oral language acquisition and development"
|
||||
},
|
||||
{
|
||||
"_id": "698c20ce4399c42eabefbd8b",
|
||||
"subLearningArea": "Preparation of the hand for writing"
|
||||
},
|
||||
{
|
||||
"_id": "698d6c894399c42eabf0b2c3",
|
||||
"subLearningArea": "Spoken language Vocabulary enrichment"
|
||||
},
|
||||
{
|
||||
"_id": "698d6cef4399c42eabf0b8ba",
|
||||
"subLearningArea": "The mechanics of writing and reading: sound-letter correspondence, letter formation, decoding, handwriting"
|
||||
},
|
||||
{
|
||||
"_id": "698d6d364399c42eabf0bb5f",
|
||||
"subLearningArea": "Written expression: preparation"
|
||||
},
|
||||
{
|
||||
"_id": "698d6d754399c42eabf0bd2c",
|
||||
"subLearningArea": "The functions of words: parts of speech and their work"
|
||||
},
|
||||
{
|
||||
"_id": "698d6dba4399c42eabf0c14d",
|
||||
"subLearningArea": "Reading: analysis and synthesis"
|
||||
},
|
||||
{
|
||||
"_id": "698d6e0a4399c42eabf0c3e4",
|
||||
"subLearningArea": "Sentence analysis: words, groups and phrases, clauses and sentences"
|
||||
},
|
||||
{
|
||||
"_id": "698d7a154399c42eabf10899",
|
||||
"subLearningArea": "Word study"
|
||||
},
|
||||
{
|
||||
"_id": "698d7a4a4399c42eabf108aa",
|
||||
"subLearningArea": "Spelling"
|
||||
},
|
||||
{
|
||||
"_id": "698d7b734399c42eabf10935",
|
||||
"subLearningArea": "Reading commands"
|
||||
},
|
||||
{
|
||||
"_id": "698d7bbb4399c42eabf109fa",
|
||||
"subLearningArea": "Interpretive reading"
|
||||
},
|
||||
{
|
||||
"_id": "698d7c084399c42eabf10b05",
|
||||
"subLearningArea": "Reading across the subject areas"
|
||||
},
|
||||
{
|
||||
"_id": "698d7c9b4399c42eabf10dca",
|
||||
"subLearningArea": "Punctuation "
|
||||
},
|
||||
{
|
||||
"_id": "698d7cc34399c42eabf11018",
|
||||
"subLearningArea": "Musical notation "
|
||||
},
|
||||
{
|
||||
"_id": "698d94384399c42eabf1567b",
|
||||
"subLearningArea": "Cultural extensions: language across the subject areas: history, geography, science, art and music, appreciation"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Development and Education of the Senses",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698c210f4399c42eabefbdf4",
|
||||
"subLearningArea": "Sensorial exploration"
|
||||
},
|
||||
{
|
||||
"_id": "698c216f4399c42eabefbe11",
|
||||
"subLearningArea": "Visual discrimination"
|
||||
},
|
||||
{
|
||||
"_id": "698c21a94399c42eabefbe59",
|
||||
"subLearningArea": "Tactile discrimination"
|
||||
},
|
||||
{
|
||||
"_id": "698c21dc4399c42eabefbe87",
|
||||
"subLearningArea": "Auditory discrimination"
|
||||
},
|
||||
{
|
||||
"_id": "698c221e4399c42eabefbea2",
|
||||
"subLearningArea": "Olfactory and gustatory"
|
||||
},
|
||||
{
|
||||
"_id": "698c22444399c42eabefbeb1",
|
||||
"subLearningArea": "Stereognostic sense"
|
||||
},
|
||||
{
|
||||
"_id": "698d64964399c42eabf09970",
|
||||
"subLearningArea": "Visual discrimination: dimension"
|
||||
},
|
||||
{
|
||||
"_id": "698d64d24399c42eabf0998c",
|
||||
"subLearningArea": "Visual discrimination: colour"
|
||||
},
|
||||
{
|
||||
"_id": "698d65444399c42eabf099ab",
|
||||
"subLearningArea": "Visual discrimination: shape (form)"
|
||||
},
|
||||
{
|
||||
"_id": "698d65824399c42eabf099e0",
|
||||
"subLearningArea": "Visual discrimination: mixed"
|
||||
},
|
||||
{
|
||||
"_id": "698d65a54399c42eabf099ed",
|
||||
"subLearningArea": "Tactile discrimination: texture"
|
||||
},
|
||||
{
|
||||
"_id": "698d65ef4399c42eabf09a04",
|
||||
"subLearningArea": "Tactile discrimination: mass (baric sense)"
|
||||
},
|
||||
{
|
||||
"_id": "698d662f4399c42eabf09a3a",
|
||||
"subLearningArea": "Tactile discrimination: temperature (thermic sense)"
|
||||
},
|
||||
{
|
||||
"_id": "698d66854399c42eabf09a57",
|
||||
"subLearningArea": "Tactile discrimination: stereognostic"
|
||||
},
|
||||
{
|
||||
"_id": "698d67844399c42eabf09ad7",
|
||||
"subLearningArea": "Auditory discrimination: dynamics/intensity of sound"
|
||||
},
|
||||
{
|
||||
"_id": "698d67eb4399c42eabf09d81",
|
||||
"subLearningArea": "Auditory discrimination: pitch"
|
||||
},
|
||||
{
|
||||
"_id": "698d682c4399c42eabf09f9e",
|
||||
"subLearningArea": "Auditory discrimination: timbre"
|
||||
},
|
||||
{
|
||||
"_id": "698d68564399c42eabf0a008",
|
||||
"subLearningArea": "Auditory discrimination: rhythm"
|
||||
},
|
||||
{
|
||||
"_id": "698d687a4399c42eabf0a06c",
|
||||
"subLearningArea": "Auditory discrimination: style"
|
||||
},
|
||||
{
|
||||
"_id": "698d68d34399c42eabf0a0e0",
|
||||
"subLearningArea": "Olfactory discrimination: smell"
|
||||
},
|
||||
{
|
||||
"_id": "698d69064399c42eabf0a195",
|
||||
"subLearningArea": "Gustatory discrimination: taste"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Fundamental Life Skills in the Infant Community",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698c22784399c42eabefbec6",
|
||||
"subLearningArea": "Transition (from home to Montessori early childhood settings)"
|
||||
},
|
||||
{
|
||||
"_id": "698c23364399c42eabefbef8",
|
||||
"subLearningArea": "Care of the environment (indoor and outdoor)"
|
||||
},
|
||||
{
|
||||
"_id": "698c23784399c42eabefbf0b",
|
||||
"subLearningArea": "Social relations"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Exercises Of Practical Life",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698d627f4399c42eabf09752",
|
||||
"subLearningArea": "Control of movement :Transition or preliminary movements"
|
||||
},
|
||||
{
|
||||
"_id": "698d62fd4399c42eabf0990f",
|
||||
"subLearningArea": "Control of movement : Preliminary activities "
|
||||
},
|
||||
{
|
||||
"_id": "698d638d4399c42eabf09930",
|
||||
"subLearningArea": "Care of person"
|
||||
},
|
||||
{
|
||||
"_id": "698d63c44399c42eabf0993e",
|
||||
"subLearningArea": "Care of environment: indoor and outdoor"
|
||||
},
|
||||
{
|
||||
"_id": "698d641c4399c42eabf09954",
|
||||
"subLearningArea": "Movement: analysis and control"
|
||||
},
|
||||
{
|
||||
"_id": "698d64494399c42eabf09961",
|
||||
"subLearningArea": "Social relations"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Science",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698d9f364399c42eabf1634e",
|
||||
"subLearningArea": "Practical life"
|
||||
},
|
||||
{
|
||||
"_id": "698da14c4399c42eabf16898",
|
||||
"subLearningArea": "Sense exercises"
|
||||
},
|
||||
{
|
||||
"_id": "698da1b24399c42eabf168bb",
|
||||
"subLearningArea": "Physical Science: simple physics and chemistry, time, weather, astronomy"
|
||||
},
|
||||
{
|
||||
"_id": "698da1e94399c42eabf168c8",
|
||||
"subLearningArea": "Botany"
|
||||
},
|
||||
{
|
||||
"_id": "698da2234399c42eabf168d7",
|
||||
"subLearningArea": "Botany: language"
|
||||
},
|
||||
{
|
||||
"_id": "698da2864399c42eabf168f0",
|
||||
"subLearningArea": "Zoology"
|
||||
},
|
||||
{
|
||||
"_id": "698da2b44399c42eabf168fd",
|
||||
"subLearningArea": "Zoology: language"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Geography",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698da3024399c42eabf16910",
|
||||
"subLearningArea": "Geography "
|
||||
},
|
||||
{
|
||||
"_id": "698da3364399c42eabf1691d",
|
||||
"subLearningArea": "Geography: language"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "History",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698da3654399c42eabf1692c",
|
||||
"subLearningArea": "History"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Creative Arts",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698da3f84399c42eabf1694b",
|
||||
"subLearningArea": "Music: auditory discrimination"
|
||||
},
|
||||
{
|
||||
"_id": "698da4224399c42eabf16954",
|
||||
"subLearningArea": "Music: singing"
|
||||
},
|
||||
{
|
||||
"_id": "698da4524399c42eabf16961",
|
||||
"subLearningArea": "Music: appreciation"
|
||||
},
|
||||
{
|
||||
"_id": "698da47a4399c42eabf1696c",
|
||||
"subLearningArea": "Music: timbre"
|
||||
},
|
||||
{
|
||||
"_id": "698da4a44399c42eabf16d42",
|
||||
"subLearningArea": "Music: pitch and notation"
|
||||
},
|
||||
{
|
||||
"_id": "698da4d94399c42eabf16d4b",
|
||||
"subLearningArea": "Music: rhythm"
|
||||
},
|
||||
{
|
||||
"_id": "698da5054399c42eabf16d58",
|
||||
"subLearningArea": "Visual Arts"
|
||||
},
|
||||
{
|
||||
"_id": "698da53c4399c42eabf16d65",
|
||||
"subLearningArea": "Movement and Dance"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_id": "Personal Development, Health and Physical Education",
|
||||
"subLearningAreas": [
|
||||
{
|
||||
"_id": "698da5794399c42eabf16d72",
|
||||
"subLearningArea": "Personal Development"
|
||||
},
|
||||
{
|
||||
"_id": "698da5a14399c42eabf16d7b",
|
||||
"subLearningArea": "Health"
|
||||
},
|
||||
{
|
||||
"_id": "698da5d44399c42eabf16d88",
|
||||
"subLearningArea": "Physical Education"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
48872
public/db/observations.json
Normal file
48872
public/db/observations.json
Normal file
File diff suppressed because it is too large
Load Diff
61
sample.json
Normal file
61
sample.json
Normal file
@@ -0,0 +1,61 @@
|
||||
[
|
||||
{
|
||||
"_id": {
|
||||
"$oid": "68c29a3f8b8a101b6f11d2ef"
|
||||
},
|
||||
"title": "Grasping",
|
||||
"remarks": "Her grasping is secured",
|
||||
"students": [
|
||||
{
|
||||
"$oid": "679749a148464093da300e1d"
|
||||
}
|
||||
],
|
||||
"assets": [
|
||||
"akademy/file_0zriR1MsJhRIaWiJE4mU0GoEUTLbD7sQ-IMG_8680.jpeg"
|
||||
],
|
||||
"classroomId": {
|
||||
"$oid": "67ee488fd1fc1f772c7f2dca"
|
||||
},
|
||||
"schoolId": {
|
||||
"$oid": "66a0daa36969d352535ee676"
|
||||
},
|
||||
"recordedBy": {
|
||||
"$oid": "67efb634d1fc1f772c9a2a9e"
|
||||
},
|
||||
"indicators": [
|
||||
{
|
||||
"indicator": "Holds pen or crayon using a whole hand (palmar) grasp and makes random marks with different strokes",
|
||||
"developmentAreas": [
|
||||
{
|
||||
"learningArea": "Physical Development",
|
||||
"subLearningArea": "Moving & Handling",
|
||||
"framework": "EYFS"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"gradeId": {
|
||||
"$oid": "664b3ce474ce94848a3ababc"
|
||||
},
|
||||
"origin": "OBSERVATION",
|
||||
"createdAt": {
|
||||
"$date": "2025-09-11T09:45:35.744Z"
|
||||
},
|
||||
"updatedAt": {
|
||||
"$date": "2025-09-11T09:45:35.744Z"
|
||||
},
|
||||
"__v": 0,
|
||||
"grade": [
|
||||
{
|
||||
"_id": {
|
||||
"$oid": "664b3ce474ce94848a3ababc"
|
||||
},
|
||||
"frameworkId": {
|
||||
"$oid": "664b3b5b004a6913d156c559"
|
||||
},
|
||||
"grade": "Secure",
|
||||
"__v": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
49
scripts/dump-db.js
Normal file
49
scripts/dump-db.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const { MongoClient } = require('mongodb');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const MONGODB_URI = 'mongodb://localhost/beanstalk';
|
||||
const OUTPUT_DIR = path.join(__dirname, '../public/db');
|
||||
|
||||
async function dumpDatabase() {
|
||||
const client = new MongoClient(MONGODB_URI);
|
||||
|
||||
try {
|
||||
console.log('Connecting to MongoDB...');
|
||||
await client.connect();
|
||||
|
||||
const db = client.db();
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
console.log('Getting collections...');
|
||||
const collections = await db.listCollections().toArray();
|
||||
|
||||
for (const collection of collections) {
|
||||
const collectionName = collection.name;
|
||||
console.log(`Dumping collection: ${collectionName}`);
|
||||
|
||||
const data = await db.collection(collectionName).find({}).toArray();
|
||||
|
||||
// Write to JSON file
|
||||
const filePath = path.join(OUTPUT_DIR, `${collectionName}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
|
||||
console.log(` ✓ ${data.length} documents saved to ${filePath}`);
|
||||
}
|
||||
|
||||
console.log('\nDatabase dump completed successfully!');
|
||||
console.log(`Output directory: ${OUTPUT_DIR}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error dumping database:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
dumpDatabase();
|
||||
93
scripts/import-db.js
Normal file
93
scripts/import-db.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const { MongoClient } = require('mongodb');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const MONGODB_URI = 'mongodb://localhost/beanstalk';
|
||||
const DB_DIR = path.join(__dirname, '../public/db');
|
||||
|
||||
async function importDatabase() {
|
||||
const client = new MongoClient(MONGODB_URI);
|
||||
|
||||
try {
|
||||
console.log('Connecting to MongoDB...');
|
||||
await client.connect();
|
||||
|
||||
const db = client.db();
|
||||
|
||||
// Check if DB directory exists
|
||||
if (!fs.existsSync(DB_DIR)) {
|
||||
console.error(`Error: Database directory not found: ${DB_DIR}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Reading JSON files...');
|
||||
const files = fs.readdirSync(DB_DIR).filter(file => file.endsWith('.json'));
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No JSON files found in the database directory.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const collectionName = path.basename(file, '.json');
|
||||
const filePath = path.join(DB_DIR, file);
|
||||
|
||||
console.log(`\nImporting collection: ${collectionName}`);
|
||||
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
console.log(` ⚠️ Warning: ${file} does not contain an array. Skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Drop existing collection
|
||||
await db.collection(collectionName).drop().catch(() => {});
|
||||
console.log(` ✓ Dropped existing collection`);
|
||||
|
||||
// Insert data
|
||||
if (data.length > 0) {
|
||||
const result = await db.collection(collectionName).insertMany(data);
|
||||
console.log(` ✓ Inserted ${result.insertedCount} documents`);
|
||||
} else {
|
||||
console.log(` ✓ Collection is empty`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ✗ Error importing ${file}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nDatabase import completed successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error importing database:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`
|
||||
Usage: node import-db.js [options]
|
||||
|
||||
Options:
|
||||
--help, -h Show this help message
|
||||
|
||||
Description:
|
||||
Imports JSON files from public/db directory into MongoDB.
|
||||
Each JSON file corresponds to a collection with the same name (without .json extension).
|
||||
|
||||
The script will:
|
||||
1. Drop existing collections
|
||||
2. Create new collections with the imported data
|
||||
3. Preserve all document structure and data
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
importDatabase();
|
||||
187
src/app/api/compress-image/route.ts
Normal file
187
src/app/api/compress-image/route.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createReadStream, createWriteStream, existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
// Store compression progress in memory
|
||||
let compressionProgress = {
|
||||
currentFile: '',
|
||||
status: 'idle', // idle, processing, completed, error
|
||||
progress: 0,
|
||||
totalFiles: 0,
|
||||
message: '',
|
||||
results: [] as Array<{
|
||||
originalPath: string;
|
||||
compressedPath: string;
|
||||
originalSize: number;
|
||||
compressedSize: number;
|
||||
compressionRatio: number;
|
||||
}>
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { imagePath } = body;
|
||||
|
||||
if (!imagePath) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Image path is required'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate image path
|
||||
// Handle both full paths and direct filenames
|
||||
let filename = imagePath;
|
||||
if (imagePath.includes('/')) {
|
||||
filename = imagePath.split('/').pop() || imagePath;
|
||||
}
|
||||
|
||||
const fullPath = join(process.cwd(), 'public', 'observations', filename);
|
||||
|
||||
if (!existsSync(fullPath)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Image file not found',
|
||||
details: `Looking for: ${filename}`,
|
||||
skipped: true // Mark as skipped, not failed
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Start compression process
|
||||
compressionProgress = {
|
||||
currentFile: imagePath,
|
||||
status: 'processing',
|
||||
progress: 0,
|
||||
totalFiles: 1,
|
||||
message: 'Starting image compression...',
|
||||
results: []
|
||||
};
|
||||
|
||||
// Process the image
|
||||
try {
|
||||
// Create compressed directory if it doesn't exist
|
||||
const compressedDir = join(process.cwd(), 'public', 'observations', 'compressed');
|
||||
mkdirSync(compressedDir, { recursive: true });
|
||||
|
||||
// Generate compressed filename
|
||||
const filename = imagePath.split('/').pop() || imagePath;
|
||||
const nameWithExt = filename.split('.');
|
||||
const extension = nameWithExt.pop();
|
||||
const nameWithoutExt = nameWithExt.join('.');
|
||||
const compressedFilename = `${nameWithoutExt}.jpg`; // Keep same name, ensure .jpg extension
|
||||
const compressedPath = join('compressed', compressedFilename);
|
||||
|
||||
// Get original file stats
|
||||
const stats = require('fs').statSync(fullPath);
|
||||
const originalSize = stats.size;
|
||||
|
||||
// Update progress
|
||||
compressionProgress.progress = 25;
|
||||
compressionProgress.message = 'Resizing image to 720p...';
|
||||
|
||||
// Process image with sharp
|
||||
const imageBuffer = require('fs').readFileSync(fullPath);
|
||||
|
||||
// Resize and compress
|
||||
const processedImage = await sharp(imageBuffer)
|
||||
.resize(720, null, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.jpeg({
|
||||
quality: 80,
|
||||
progressive: true
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
// Update progress
|
||||
compressionProgress.progress = 75;
|
||||
compressionProgress.message = 'Compressing image...';
|
||||
|
||||
// Save compressed image
|
||||
writeFileSync(join(compressedDir, compressedFilename), processedImage);
|
||||
|
||||
// Get compressed file stats
|
||||
const compressedStats = require('fs').statSync(join(compressedDir, compressedFilename));
|
||||
const compressedSize = compressedStats.size;
|
||||
|
||||
// Calculate compression ratio
|
||||
const compressionRatio = ((originalSize - compressedSize) / originalSize * 100).toFixed(1);
|
||||
|
||||
// Update progress to completed
|
||||
compressionProgress.progress = 100;
|
||||
compressionProgress.status = 'completed';
|
||||
compressionProgress.message = 'Image compression completed successfully';
|
||||
compressionProgress.results = [{
|
||||
originalPath: imagePath,
|
||||
compressedPath: `compressed/${compressedFilename}`,
|
||||
originalSize,
|
||||
compressedSize,
|
||||
compressionRatio: parseFloat(compressionRatio)
|
||||
}];
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Image compressed successfully',
|
||||
data: {
|
||||
originalPath: imagePath,
|
||||
compressedPath: `compressed/${compressedFilename}`,
|
||||
originalSize,
|
||||
compressedSize,
|
||||
compressionRatio: parseFloat(compressionRatio)
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
compressionProgress.status = 'error';
|
||||
compressionProgress.message = `Compression failed: ${error.message}`;
|
||||
|
||||
console.error('Compression error details:', {
|
||||
imagePath,
|
||||
filename,
|
||||
fullPath,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to compress image',
|
||||
message: error.message,
|
||||
details: {
|
||||
imagePath,
|
||||
filename,
|
||||
fullPath
|
||||
}
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid request format'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Progress endpoint
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: compressionProgress
|
||||
});
|
||||
}
|
||||
42
src/app/api/data-structure/route.ts
Normal file
42
src/app/api/data-structure/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const filePath = join(process.cwd(), 'public', 'data_structure.json');
|
||||
|
||||
try {
|
||||
const fileContent = await readFile(filePath, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: data
|
||||
});
|
||||
} catch (fileError) {
|
||||
// If file doesn't exist, return empty arrays
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
learningAreas: [],
|
||||
subLearningAreas: [],
|
||||
frameworks: [],
|
||||
lastUpdated: null,
|
||||
totalObservations: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error reading data structure:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to read data structure',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
95
src/app/api/delete-observation/[id]/route.ts
Normal file
95
src/app/api/delete-observation/[id]/route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const params = await context.params;
|
||||
const id = params.id;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'No ID provided'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { MongoClient, ObjectId } = require('mongodb');
|
||||
const uri = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const client = new MongoClient(uri);
|
||||
await client.connect();
|
||||
const db = client.db('beanstalk');
|
||||
const collection = db.collection('observations');
|
||||
|
||||
// Try to find the document first to determine the correct _id format
|
||||
let document = await collection.findOne({ _id: id });
|
||||
let filter;
|
||||
|
||||
if (document) {
|
||||
// Found with string ID
|
||||
filter = { _id: id };
|
||||
} else {
|
||||
// Try with ObjectId
|
||||
try {
|
||||
const objectId = new ObjectId(id);
|
||||
document = await collection.findOne({ _id: objectId });
|
||||
if (document) {
|
||||
filter = { _id: objectId };
|
||||
} else {
|
||||
await client.close();
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Observation not found'
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
await client.close();
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid ID format'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the document
|
||||
const result = await collection.deleteOne(filter);
|
||||
|
||||
await client.close();
|
||||
|
||||
if (result.deletedCount === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to delete observation'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Observation deleted successfully'
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Delete observation error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to delete observation',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
54
src/app/api/frameworks/route.ts
Normal file
54
src/app/api/frameworks/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const DATABASE_NAME = 'beanstalk';
|
||||
|
||||
export async function GET() {
|
||||
let client: MongoClient | null = null;
|
||||
|
||||
try {
|
||||
client = new MongoClient(MONGODB_URI);
|
||||
await client.connect();
|
||||
|
||||
const db = client.db(DATABASE_NAME);
|
||||
|
||||
// Get unique frameworks from indicators collection
|
||||
const indicators = await db.collection('indicators').find({}).toArray();
|
||||
|
||||
// Extract unique frameworks
|
||||
const frameworks = new Set<string>();
|
||||
indicators.forEach((indicator: any) => {
|
||||
if (indicator.developmentArea && Array.isArray(indicator.developmentArea)) {
|
||||
indicator.developmentArea.forEach((area: any) => {
|
||||
if (area.framework) {
|
||||
frameworks.add(area.framework);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
frameworks: Array.from(frameworks).sort(),
|
||||
count: frameworks.size
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching frameworks:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch frameworks',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
268
src/app/api/get-images-from-s3/route.ts
Normal file
268
src/app/api/get-images-from-s3/route.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { writeFile, mkdir } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
interface S3Response {
|
||||
url: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
interface ProgressCallback {
|
||||
(progress: {
|
||||
current: number;
|
||||
total: number;
|
||||
currentKey: string;
|
||||
status: 'processing' | 'success' | 'error';
|
||||
message?: string;
|
||||
}): void;
|
||||
}
|
||||
|
||||
// Store progress in memory (in production, use Redis or database)
|
||||
let downloadProgress: any = {
|
||||
current: 0,
|
||||
total: 0,
|
||||
currentKey: '',
|
||||
status: 'idle',
|
||||
results: []
|
||||
};
|
||||
|
||||
async function downloadImage(s3Key: string): Promise<{ url: string; filename: string }> {
|
||||
try {
|
||||
const apiDomain = process.env.API_DOMAIN || 'https://app.example.tech';
|
||||
const response = await fetch(`${apiDomain}/api/one/v1/file/url?s3Key=${encodeURIComponent(s3Key)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get download URL for ${s3Key}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: S3Response = await response.json();
|
||||
|
||||
if (!data.url) {
|
||||
throw new Error(`No download URL returned for ${s3Key}`);
|
||||
}
|
||||
|
||||
// Extract filename from S3 key
|
||||
const filename = s3Key.split('/').pop() || s3Key;
|
||||
|
||||
return { url: data.url, filename };
|
||||
} catch (error) {
|
||||
console.error(`Error getting download URL for ${s3Key}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveImageFromUrl(url: string, filename: string, savePath: string): Promise<void> {
|
||||
try {
|
||||
// Download the image
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download image ${filename}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// Save to file system
|
||||
await writeFile(savePath, buffer);
|
||||
console.log(`Saved image: ${savePath}`);
|
||||
} catch (error) {
|
||||
console.error(`Error saving image ${filename}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function extractImageKeys(documents: any[]): string[] {
|
||||
const imageKeys = new Set<string>();
|
||||
|
||||
documents.forEach(doc => {
|
||||
// Extract from assets array
|
||||
if (doc.assets && Array.isArray(doc.assets)) {
|
||||
doc.assets.forEach((asset: string) => {
|
||||
if (asset.startsWith('akademy/')) {
|
||||
imageKeys.add(asset);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(imageKeys);
|
||||
}
|
||||
|
||||
async function processImageSequentially(
|
||||
imageKeys: string[],
|
||||
observationsDir: string,
|
||||
onProgress?: ProgressCallback
|
||||
): Promise<any[]> {
|
||||
const results = [];
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (let i = 0; i < imageKeys.length; i++) {
|
||||
const imageKey = imageKeys[i];
|
||||
|
||||
try {
|
||||
// Update progress - starting
|
||||
downloadProgress.current = i + 1;
|
||||
downloadProgress.total = imageKeys.length;
|
||||
downloadProgress.currentKey = imageKey;
|
||||
downloadProgress.status = 'processing';
|
||||
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
...downloadProgress,
|
||||
status: 'processing',
|
||||
message: `Starting download: ${imageKey}`
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[${i + 1}/${imageKeys.length}] Processing: ${imageKey}`);
|
||||
|
||||
// Get download URL from S3 API
|
||||
const { url, filename } = await downloadImage(imageKey);
|
||||
|
||||
// Create a safe filename (remove path separators but keep original name)
|
||||
const safeFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
|
||||
const finalFilename = safeFilename;
|
||||
const savePath = join(observationsDir, finalFilename);
|
||||
|
||||
// Download and save image
|
||||
await saveImageFromUrl(url, filename, savePath);
|
||||
|
||||
const result = {
|
||||
original_key: imageKey,
|
||||
filename: finalFilename,
|
||||
local_path: `/observations/${finalFilename}`,
|
||||
status: 'success'
|
||||
};
|
||||
|
||||
results.push(result);
|
||||
successCount++;
|
||||
|
||||
// Update progress - success
|
||||
downloadProgress.status = 'success';
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
...downloadProgress,
|
||||
status: 'success',
|
||||
message: `Successfully saved: ${finalFilename}`
|
||||
});
|
||||
}
|
||||
|
||||
// Small delay between downloads to avoid overwhelming the API
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to process ${imageKey}:`, error.message);
|
||||
|
||||
const result = {
|
||||
original_key: imageKey,
|
||||
error: error.message,
|
||||
status: 'error'
|
||||
};
|
||||
|
||||
results.push(result);
|
||||
errorCount++;
|
||||
|
||||
// Update progress - error
|
||||
downloadProgress.status = 'error';
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
...downloadProgress,
|
||||
status: 'error',
|
||||
message: `Failed: ${error.message}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Ensure the public/observations directory exists
|
||||
const observationsDir = join(process.cwd(), 'public', 'observations');
|
||||
try {
|
||||
await mkdir(observationsDir, { recursive: true });
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'EEXIST') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all documents from MongoDB to extract image keys
|
||||
const { MongoClient } = require('mongodb');
|
||||
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017';
|
||||
const client = new MongoClient(uri);
|
||||
await client.connect();
|
||||
const db = client.db('beanstalk');
|
||||
const collection = db.collection('observations');
|
||||
|
||||
const documents = await collection.find({}).toArray();
|
||||
await client.close();
|
||||
|
||||
// Extract unique image keys from documents
|
||||
const imageKeys = extractImageKeys(documents);
|
||||
|
||||
if (imageKeys.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'No images found in documents',
|
||||
images_processed: 0,
|
||||
images_saved: 0
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Found ${imageKeys.length} unique images to download`);
|
||||
|
||||
// Reset progress
|
||||
downloadProgress = {
|
||||
current: 0,
|
||||
total: imageKeys.length,
|
||||
currentKey: '',
|
||||
status: 'starting',
|
||||
results: []
|
||||
};
|
||||
|
||||
// Process images one by one sequentially
|
||||
const results = await processImageSequentially(imageKeys, observationsDir);
|
||||
|
||||
const successCount = results.filter(r => r.status === 'success').length;
|
||||
const errorCount = results.filter(r => r.status === 'error').length;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Completed processing ${imageKeys.length} images. Successfully saved ${successCount}, failed ${errorCount}`,
|
||||
images_processed: imageKeys.length,
|
||||
images_saved: successCount,
|
||||
images_failed: errorCount,
|
||||
results: results
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error in get-images-from-s3:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to process images from S3',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
// Return current progress
|
||||
return NextResponse.json({
|
||||
message: 'Current download progress',
|
||||
progress: downloadProgress,
|
||||
usage: {
|
||||
method: 'POST',
|
||||
endpoint: '/api/get-images-from-s3',
|
||||
description: 'Downloads all images found in MongoDB observations and saves them to public/observations/',
|
||||
example: 'fetch("/api/get-images-from-s3", { method: "POST" })'
|
||||
}
|
||||
});
|
||||
}
|
||||
74
src/app/api/get-observation/[id]/route.ts
Normal file
74
src/app/api/get-observation/[id]/route.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const params = await context.params;
|
||||
const id = params.id;
|
||||
console.log('=== API CALLED WITH ID:', id);
|
||||
console.log('Full params object:', JSON.stringify(params));
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'No ID provided'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { MongoClient, ObjectId } = require('mongodb');
|
||||
const uri = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const client = new MongoClient(uri);
|
||||
await client.connect();
|
||||
const db = client.db('beanstalk');
|
||||
const collection = db.collection('observations');
|
||||
|
||||
// Try to find by string ID first
|
||||
let document = await collection.findOne({ _id: id });
|
||||
console.log('String ID search result:', document ? 'Found' : 'Not found');
|
||||
|
||||
// If not found, try ObjectId
|
||||
if (!document) {
|
||||
try {
|
||||
const objectId = new ObjectId(id);
|
||||
document = await collection.findOne({ _id: objectId });
|
||||
console.log('ObjectId search result:', document ? 'Found' : 'Not found');
|
||||
} catch (error: any) {
|
||||
console.log('Invalid ObjectId format:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
await client.close();
|
||||
|
||||
if (!document) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Observation not found',
|
||||
searchedId: id
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: document
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching observation:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch observation',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
73
src/app/api/get-observations/route.ts
Normal file
73
src/app/api/get-observations/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const limit = parseInt(searchParams.get('limit') || '12');
|
||||
const framework = searchParams.get('framework');
|
||||
const learningArea = searchParams.get('learningArea');
|
||||
const subLearningArea = searchParams.get('subLearningArea');
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
const uri = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const client = new MongoClient(uri);
|
||||
await client.connect();
|
||||
const db = client.db('beanstalk');
|
||||
const collection = db.collection('observations');
|
||||
|
||||
// Build filter query
|
||||
let filter: any = {};
|
||||
|
||||
if (framework) {
|
||||
filter['indicators.developmentAreas.framework'] = framework.trim();
|
||||
}
|
||||
|
||||
if (learningArea) {
|
||||
filter['indicators.developmentAreas.learningArea'] = learningArea.trim();
|
||||
}
|
||||
|
||||
if (subLearningArea) {
|
||||
filter['indicators.developmentAreas.subLearningArea'] = subLearningArea.trim();
|
||||
}
|
||||
|
||||
// Get total count for pagination
|
||||
const totalCount = await collection.countDocuments(filter);
|
||||
|
||||
// Calculate skip value for pagination
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Fetch observations with pagination and sort by createdAt (newest first)
|
||||
const documents = await collection
|
||||
.find(filter)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
|
||||
await client.close();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: documents,
|
||||
pagination: {
|
||||
currentPage: page,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
totalCount: totalCount,
|
||||
limit: limit,
|
||||
hasNextPage: page < Math.ceil(totalCount / limit),
|
||||
hasPrevPage: page > 1
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching observations:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch observations',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
188
src/app/api/import-json/route.ts
Normal file
188
src/app/api/import-json/route.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { MongoClient, Db, Collection, ObjectId } from 'mongodb';
|
||||
|
||||
// MongoDB connection
|
||||
let client: MongoClient | null = null;
|
||||
let db: Db | null = null;
|
||||
|
||||
async function getDatabase(): Promise<Db> {
|
||||
if (!client) {
|
||||
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017';
|
||||
client = new MongoClient(uri);
|
||||
await client.connect();
|
||||
db = client.db('beanstalk');
|
||||
}
|
||||
return db!;
|
||||
}
|
||||
|
||||
function convertMongoExtendedJSON(obj: any): any {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => convertMongoExtendedJSON(item));
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const converted: any = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (key === '$oid' && typeof value === 'string') {
|
||||
return new ObjectId(value);
|
||||
} else if (key === '$date' && typeof value === 'string') {
|
||||
return new Date(value);
|
||||
} else {
|
||||
converted[key] = convertMongoExtendedJSON(value);
|
||||
}
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const filename = searchParams.get('file');
|
||||
|
||||
if (!filename) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'file parameter is required',
|
||||
message: 'Usage: /api/import-json?file=db_prod.akademyObservations.json'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Read JSON file
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const filePath = path.join(process.cwd(), '..', filename);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||
const rawDocuments = JSON.parse(fileContent);
|
||||
|
||||
// Convert MongoDB extended JSON to proper MongoDB documents
|
||||
const documents = rawDocuments.map((doc: any) => convertMongoExtendedJSON(doc));
|
||||
|
||||
// Get MongoDB collection
|
||||
const db = await getDatabase();
|
||||
const collection = db.collection('observations');
|
||||
|
||||
// Insert documents
|
||||
const result = await collection.insertMany(documents);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Successfully imported ${result.insertedCount} observations`,
|
||||
inserted_count: result.insertedCount,
|
||||
inserted_ids: Object.values(result.insertedIds)
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Import error:', error);
|
||||
|
||||
if (error.code === 'ENOENT') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'File not found',
|
||||
message: `The file was not found`
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error.name === 'SyntaxError') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to parse JSON',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to import data',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'No file provided',
|
||||
message: 'Please select a JSON file to upload'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.includes('json')) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Invalid file type',
|
||||
message: 'Please upload a JSON file'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const fileContent = await file.text();
|
||||
const rawDocuments = JSON.parse(fileContent);
|
||||
|
||||
// Convert MongoDB extended JSON to proper MongoDB documents
|
||||
const documents = rawDocuments.map((doc: any) => convertMongoExtendedJSON(doc));
|
||||
|
||||
// Get MongoDB collection
|
||||
const db = await getDatabase();
|
||||
const collection = db.collection('observations');
|
||||
|
||||
// Insert documents
|
||||
const result = await collection.insertMany(documents);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Successfully imported ${result.insertedCount} observations from ${file.name}`,
|
||||
inserted_count: result.insertedCount,
|
||||
inserted_ids: Object.values(result.insertedIds),
|
||||
filename: file.name,
|
||||
size: `${(file.size / 1024).toFixed(2)} KB`
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Import error:', error);
|
||||
|
||||
if (error.name === 'SyntaxError') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to parse JSON',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to import data',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
85
src/app/api/indicators-by-filter/route.ts
Normal file
85
src/app/api/indicators-by-filter/route.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const DATABASE_NAME = 'beanstalk';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
let client: MongoClient | null = null;
|
||||
|
||||
try {
|
||||
client = new MongoClient(MONGODB_URI);
|
||||
await client.connect();
|
||||
|
||||
const db = client.db(DATABASE_NAME);
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const framework = searchParams.get('framework');
|
||||
const learningArea = searchParams.get('learningArea');
|
||||
const subLearningArea = searchParams.get('subLearningArea');
|
||||
|
||||
// Build filter query
|
||||
let filter: any = {};
|
||||
|
||||
if (framework) {
|
||||
filter['developmentArea.framework'] = framework.trim();
|
||||
}
|
||||
|
||||
if (learningArea) {
|
||||
filter['developmentArea.learningArea'] = learningArea.trim();
|
||||
}
|
||||
|
||||
if (subLearningArea) {
|
||||
filter['developmentArea.subLearningArea'] = subLearningArea.trim();
|
||||
}
|
||||
|
||||
// Get indicators matching the filter
|
||||
const indicators = await db.collection('indicators')
|
||||
.find({
|
||||
developmentArea: {
|
||||
$elemMatch: {
|
||||
...(framework && { framework: framework.trim() }),
|
||||
...(learningArea && { learningArea: learningArea.trim() }),
|
||||
...(subLearningArea && { subLearningArea: subLearningArea.trim() })
|
||||
}
|
||||
}
|
||||
})
|
||||
.toArray();
|
||||
|
||||
// Extract unique indicator descriptions
|
||||
const indicatorDescriptions = new Set<string>();
|
||||
indicators.forEach((indicator: any) => {
|
||||
if (indicator.indicator) {
|
||||
indicatorDescriptions.add(indicator.indicator);
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
indicators: Array.from(indicatorDescriptions).sort(),
|
||||
count: indicatorDescriptions.size,
|
||||
filter: {
|
||||
framework,
|
||||
learningArea,
|
||||
subLearningArea
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching indicators:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch indicators',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/app/api/indicators/route.ts
Normal file
50
src/app/api/indicators/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const DATABASE_NAME = 'beanstalk';
|
||||
|
||||
export async function GET() {
|
||||
let client: MongoClient | null = null;
|
||||
|
||||
try {
|
||||
client = new MongoClient(MONGODB_URI);
|
||||
await client.connect();
|
||||
|
||||
const db = client.db(DATABASE_NAME);
|
||||
|
||||
// Get all indicators from indicators collection
|
||||
const indicators = await db.collection('indicators').find({}).toArray();
|
||||
|
||||
// Extract unique indicator descriptions
|
||||
const indicatorDescriptions = new Set<string>();
|
||||
indicators.forEach((indicator: any) => {
|
||||
if (indicator.indicator) {
|
||||
indicatorDescriptions.add(indicator.indicator);
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
indicators: Array.from(indicatorDescriptions).sort(),
|
||||
count: indicatorDescriptions.size
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching indicators:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch indicators',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/app/api/learning-areas/route.ts
Normal file
63
src/app/api/learning-areas/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const DATABASE_NAME = 'beanstalk';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
let client: MongoClient | null = null;
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const framework = searchParams.get('framework');
|
||||
|
||||
client = new MongoClient(MONGODB_URI);
|
||||
await client.connect();
|
||||
|
||||
const db = client.db(DATABASE_NAME);
|
||||
|
||||
let query: any = {};
|
||||
if (framework) {
|
||||
query['developmentArea.framework'] = framework;
|
||||
}
|
||||
|
||||
// Get unique learning areas from indicators collection
|
||||
const indicators = await db.collection('indicators').find(query).toArray();
|
||||
|
||||
// Extract unique learning areas
|
||||
const learningAreas = new Set<string>();
|
||||
indicators.forEach((indicator: any) => {
|
||||
if (indicator.developmentArea && Array.isArray(indicator.developmentArea)) {
|
||||
indicator.developmentArea.forEach((area: any) => {
|
||||
if (area.learningArea) {
|
||||
learningAreas.add(area.learningArea);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
learningAreas: Array.from(learningAreas).sort(),
|
||||
count: learningAreas.size,
|
||||
framework: framework || 'all'
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching learning areas:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch learning areas',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/app/api/proxy-image/route.ts
Normal file
51
src/app/api/proxy-image/route.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const path = searchParams.get('path');
|
||||
|
||||
if (!path) {
|
||||
return NextResponse.json({ error: 'No path provided' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Security check - ensure the path doesn't contain directory traversal
|
||||
if (path.includes('..') || path.includes('/') || path.includes('\\')) {
|
||||
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
||||
}
|
||||
|
||||
const imagePath = join(process.cwd(), 'public', 'observations', path);
|
||||
|
||||
try {
|
||||
const imageBuffer = await readFile(imagePath);
|
||||
|
||||
// Determine content type based on file extension
|
||||
const ext = path.split('.').pop()?.toLowerCase();
|
||||
let contentType = 'image/jpeg';
|
||||
|
||||
if (ext === 'png') contentType = 'image/png';
|
||||
else if (ext === 'gif') contentType = 'image/gif';
|
||||
else if (ext === 'webp') contentType = 'image/webp';
|
||||
else if (ext === 'jpg' || ext === 'jpeg') contentType = 'image/jpeg';
|
||||
|
||||
return new NextResponse(imageBuffer, {
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=31536000', // Cache for 1 year
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
},
|
||||
});
|
||||
} catch (fileError) {
|
||||
console.error('File not found:', imagePath);
|
||||
return NextResponse.json({ error: 'Image not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Proxy image error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
89
src/app/api/sub-learning-areas/route.ts
Normal file
89
src/app/api/sub-learning-areas/route.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const DATABASE_NAME = 'beanstalk';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
let client: MongoClient | null = null;
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const framework = searchParams.get('framework');
|
||||
const learningArea = searchParams.get('learningArea');
|
||||
|
||||
client = new MongoClient(MONGODB_URI);
|
||||
await client.connect();
|
||||
|
||||
const db = client.db(DATABASE_NAME);
|
||||
|
||||
let query: any = {};
|
||||
if (framework && learningArea) {
|
||||
query['developmentArea'] = {
|
||||
$elemMatch: {
|
||||
framework: framework,
|
||||
learningArea: learningArea
|
||||
}
|
||||
};
|
||||
} else if (framework) {
|
||||
query['developmentArea.framework'] = framework;
|
||||
} else if (learningArea) {
|
||||
query['developmentArea.learningArea'] = learningArea;
|
||||
}
|
||||
|
||||
// Get indicators matching the criteria
|
||||
const indicators = await db.collection('indicators').find(query).toArray();
|
||||
|
||||
// Extract unique sub-learning areas
|
||||
const subLearningAreas = new Set<string>();
|
||||
indicators.forEach((indicator: any) => {
|
||||
if (indicator.developmentArea && Array.isArray(indicator.developmentArea)) {
|
||||
indicator.developmentArea.forEach((area: any) => {
|
||||
if (area.subLearningArea) {
|
||||
// Only add if it matches both framework and learning area if both are provided
|
||||
if (framework && learningArea) {
|
||||
if (area.framework === framework && area.learningArea === learningArea) {
|
||||
subLearningAreas.add(area.subLearningArea);
|
||||
}
|
||||
} else if (framework && !learningArea) {
|
||||
if (area.framework === framework) {
|
||||
subLearningAreas.add(area.subLearningArea);
|
||||
}
|
||||
} else if (!framework && learningArea) {
|
||||
if (area.learningArea === learningArea) {
|
||||
subLearningAreas.add(area.subLearningArea);
|
||||
}
|
||||
} else {
|
||||
subLearningAreas.add(area.subLearningArea);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
subLearningAreas: Array.from(subLearningAreas).sort(),
|
||||
count: subLearningAreas.size,
|
||||
framework: framework || 'all',
|
||||
learningArea: learningArea || 'all'
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching sub-learning areas:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch sub-learning areas',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/app/api/update-data-structure/route.ts
Normal file
73
src/app/api/update-data-structure/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { writeFile, mkdir } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const { MongoClient } = require('mongodb');
|
||||
const uri = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const client = new MongoClient(uri);
|
||||
await client.connect();
|
||||
const db = client.db('beanstalk');
|
||||
const collection = db.collection('observations');
|
||||
|
||||
// Get all observations
|
||||
const observations = await collection.find({}).toArray();
|
||||
|
||||
// Extract unique values
|
||||
const learningAreas = new Set<string>();
|
||||
const subLearningAreas = new Set<string>();
|
||||
const frameworks = new Set<string>();
|
||||
|
||||
observations.forEach((obs: any) => {
|
||||
if (obs.indicators && Array.isArray(obs.indicators)) {
|
||||
obs.indicators.forEach((indicator: any) => {
|
||||
if (indicator.developmentAreas && Array.isArray(indicator.developmentAreas)) {
|
||||
indicator.developmentAreas.forEach((area: any) => {
|
||||
if (area.learningArea) learningAreas.add(area.learningArea);
|
||||
if (area.subLearningArea) subLearningAreas.add(area.subLearningArea);
|
||||
if (area.framework) frameworks.add(area.framework);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const dataStructure = {
|
||||
learningAreas: Array.from(learningAreas).sort(),
|
||||
subLearningAreas: Array.from(subLearningAreas).sort(),
|
||||
frameworks: Array.from(frameworks).sort(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
totalObservations: observations.length
|
||||
};
|
||||
|
||||
// Save to public directory so it can be accessed by the client
|
||||
const publicDir = join(process.cwd(), 'public');
|
||||
const filePath = join(publicDir, 'data_structure.json');
|
||||
|
||||
// Ensure public directory exists
|
||||
await mkdir(publicDir, { recursive: true });
|
||||
|
||||
// Write the file
|
||||
await writeFile(filePath, JSON.stringify(dataStructure, null, 2), 'utf-8');
|
||||
|
||||
await client.close();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Data structure updated successfully',
|
||||
data: dataStructure
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error updating data structure:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to update data structure',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
110
src/app/api/update-observation/[id]/route.ts
Normal file
110
src/app/api/update-observation/[id]/route.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const params = await context.params;
|
||||
const id = params.id;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'No ID provided'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { MongoClient, ObjectId } = require('mongodb');
|
||||
const uri = process.env.MONGODB_URI || 'mongodb://localhost/beanstalk';
|
||||
const client = new MongoClient(uri);
|
||||
await client.connect();
|
||||
const db = client.db('beanstalk');
|
||||
const collection = db.collection('observations');
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Try to find the document first to determine the correct _id format
|
||||
let document = await collection.findOne({ _id: id });
|
||||
let filter;
|
||||
|
||||
if (document) {
|
||||
// Found with string ID
|
||||
filter = { _id: id };
|
||||
} else {
|
||||
// Try with ObjectId
|
||||
try {
|
||||
const objectId = new ObjectId(id);
|
||||
document = await collection.findOne({ _id: objectId });
|
||||
if (document) {
|
||||
filter = { _id: objectId };
|
||||
} else {
|
||||
await client.close();
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Observation not found'
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
await client.close();
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid ID format'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update observation
|
||||
const result = await collection.updateOne(
|
||||
filter,
|
||||
{
|
||||
$set: {
|
||||
...body,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.matchedCount === 0) {
|
||||
await client.close();
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Observation not found'
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch updated document
|
||||
const updatedDocument = await collection.findOne(filter);
|
||||
|
||||
await client.close();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: updatedDocument,
|
||||
message: 'Observation updated successfully'
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error updating observation:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to update observation',
|
||||
message: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
196
src/app/data/page.tsx
Normal file
196
src/app/data/page.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Layout from '@/components/Layout';
|
||||
|
||||
export default function DataPage() {
|
||||
// Compression states
|
||||
const [compressing, setCompressing] = useState(false);
|
||||
const [compressionProgress, setCompressionProgress] = useState<any>(null);
|
||||
|
||||
const compressAllImages = async () => {
|
||||
try {
|
||||
setCompressing(true);
|
||||
setCompressionProgress({ status: 'starting', message: 'Starting image compression...' });
|
||||
|
||||
// Get all unique image paths from observations
|
||||
const response = await fetch('/api/get-observations?limit=1000');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
const imagePaths = new Set<string>();
|
||||
|
||||
data.data.forEach((obs: any) => {
|
||||
if (obs.assets && Array.isArray(obs.assets)) {
|
||||
obs.assets.forEach((asset: string) => {
|
||||
imagePaths.add(asset);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const totalImages = imagePaths.size;
|
||||
let processedImages = 0;
|
||||
let failedImages = 0;
|
||||
let skippedImages = 0;
|
||||
|
||||
console.log(`Found ${totalImages} unique images to compress`);
|
||||
|
||||
// Process each image
|
||||
for (const imagePath of imagePaths) {
|
||||
try {
|
||||
const compressResponse = await fetch('/api/compress-image', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ imagePath }),
|
||||
});
|
||||
|
||||
if (compressResponse.ok) {
|
||||
const result = await compressResponse.json();
|
||||
processedImages++;
|
||||
setCompressionProgress({
|
||||
status: 'processing',
|
||||
message: `Compressed ${processedImages} of ${totalImages} images... (${Math.round((processedImages / totalImages) * 100)}%)`,
|
||||
progress: (processedImages / totalImages) * 100
|
||||
});
|
||||
console.log(`Compressed: ${imagePath} - Ratio: ${result.data.compressionRatio}%`);
|
||||
} else {
|
||||
const errorResult = await compressResponse.json().catch(() => ({}));
|
||||
if (errorResult.skipped) {
|
||||
skippedImages++;
|
||||
console.log(`Skipped (file not found): ${imagePath}`);
|
||||
} else {
|
||||
failedImages++;
|
||||
console.error(`Failed to compress: ${imagePath}`, errorResult);
|
||||
}
|
||||
setCompressionProgress((prev: any) => ({
|
||||
...prev,
|
||||
message: `Compressed ${processedImages} of ${totalImages} images... (${skippedImages} skipped, ${failedImages} failed)`
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
failedImages++;
|
||||
console.error('Error compressing image:', imagePath, error);
|
||||
}
|
||||
}
|
||||
|
||||
let finalMessage = `Successfully compressed ${processedImages} images`;
|
||||
if (skippedImages > 0) {
|
||||
finalMessage += `, ${skippedImages} skipped (files not found)`;
|
||||
}
|
||||
if (failedImages > 0) {
|
||||
finalMessage += `, ${failedImages} failed`;
|
||||
}
|
||||
|
||||
setCompressionProgress({
|
||||
status: 'completed',
|
||||
message: finalMessage,
|
||||
progress: 100,
|
||||
totalImages,
|
||||
processedImages,
|
||||
skippedImages,
|
||||
failedImages
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in compression process:', error);
|
||||
setCompressionProgress({
|
||||
status: 'error',
|
||||
message: 'Compression failed: ' + (error instanceof Error ? error.message : 'Unknown error'),
|
||||
progress: 0
|
||||
});
|
||||
} finally {
|
||||
setCompressing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">Data Management</h1>
|
||||
|
||||
{/* Import Section */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Import Data</h2>
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<p className="text-gray-700 mb-4">Import observation data from JSON files into the database.</p>
|
||||
<a
|
||||
href="/import-json"
|
||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
|
||||
>
|
||||
Import JSON
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Compression Section */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Image Compression</h2>
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<p className="text-gray-700 mb-4">
|
||||
Compress all observation images to reduce file size. Images will be resized to maximum 720p
|
||||
and compressed with optimized settings.
|
||||
</p>
|
||||
<button
|
||||
onClick={compressAllImages}
|
||||
disabled={compressing}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium disabled:bg-gray-400"
|
||||
>
|
||||
{compressing ? 'Compressing...' : 'Start Image Compression'}
|
||||
</button>
|
||||
|
||||
{/* Compression Progress */}
|
||||
{compressionProgress && (
|
||||
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<h3 className="text-lg font-semibold text-blue-900 mb-2">Image Compression Progress</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-700">Status:</span>
|
||||
<span className="text-sm font-medium text-blue-900">{compressionProgress.status}</span>
|
||||
</div>
|
||||
{compressionProgress.message && (
|
||||
<div className="text-sm text-gray-600">{compressionProgress.message}</div>
|
||||
)}
|
||||
{compressionProgress.progress > 0 && (
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${compressionProgress.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
{compressionProgress.totalImages && (
|
||||
<div className="text-sm text-gray-600">
|
||||
Total Images: {compressionProgress.totalImages} |
|
||||
Processed: {compressionProgress.processedImages} |
|
||||
Skipped: {compressionProgress.skippedImages || 0} |
|
||||
Failed: {compressionProgress.failedImages || 0}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download Images Section */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Download Images</h2>
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<p className="text-gray-700 mb-4">Download observation images from S3 storage to local server.</p>
|
||||
<a
|
||||
href="/download-images"
|
||||
className="inline-flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm font-medium"
|
||||
>
|
||||
Download Images
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
137
src/app/download-images/page.tsx
Normal file
137
src/app/download-images/page.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function DownloadImagesPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<any>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleDownload = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/get-images-from-s3', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Download failed');
|
||||
}
|
||||
|
||||
setResult(data);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 text-center">
|
||||
Download Images from S3
|
||||
</h1>
|
||||
<p className="mt-2 text-gray-600 text-center">
|
||||
Extract image URLs from MongoDB observations and download them to public/observations/
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={loading}
|
||||
className={`w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white ${
|
||||
loading
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500'
|
||||
}`}
|
||||
>
|
||||
{loading ? 'Downloading Images...' : 'Download All Images'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-md">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">Download Failed</h3>
|
||||
<div className="mt-2 text-sm text-red-700">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="mt-6 p-4 bg-green-50 border border-green-200 rounded-md">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-green-800">Download Complete</h3>
|
||||
<div className="mt-2 text-sm text-green-700">
|
||||
<p>{result.message}</p>
|
||||
<div className="mt-2 space-y-1">
|
||||
<p><strong>Images Processed:</strong> {result.images_processed}</p>
|
||||
<p><strong>Images Saved:</strong> {result.images_saved}</p>
|
||||
<p><strong>Images Failed:</strong> {result.images_failed}</p>
|
||||
</div>
|
||||
{result.results && result.results.length > 0 && (
|
||||
<details className="mt-3">
|
||||
<summary className="cursor-pointer text-sm font-medium text-green-800 hover:text-green-900">
|
||||
View Detailed Results ({result.results.length})
|
||||
</summary>
|
||||
<div className="mt-2 max-h-40 overflow-y-auto text-xs">
|
||||
{result.results.map((item: any, index: number) => (
|
||||
<div key={index} className="mb-2 p-2 bg-white rounded border">
|
||||
<div className="font-medium">Key: {item.original_key}</div>
|
||||
{item.status === 'success' ? (
|
||||
<div className="text-green-600">
|
||||
✓ Saved as: {item.filename}
|
||||
<br />
|
||||
Path: {item.local_path}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-red-600">
|
||||
✗ Error: {item.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8">
|
||||
<a
|
||||
href="/"
|
||||
className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Back to Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
572
src/app/edit-observation/[id]/page.tsx
Normal file
572
src/app/edit-observation/[id]/page.tsx
Normal file
@@ -0,0 +1,572 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import Layout from '@/components/Layout';
|
||||
|
||||
interface DevelopmentArea {
|
||||
learningArea: string;
|
||||
subLearningArea: string;
|
||||
framework: string;
|
||||
}
|
||||
|
||||
interface Indicator {
|
||||
indicator: string;
|
||||
developmentAreas: DevelopmentArea[];
|
||||
}
|
||||
|
||||
interface Observation {
|
||||
_id: string;
|
||||
title: string;
|
||||
remarks: string;
|
||||
assets: string[];
|
||||
indicators: Indicator[];
|
||||
grade: { grade: string }[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export default function EditObservationPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const id = params.id as string;
|
||||
|
||||
const [observation, setObservation] = useState<Observation | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [title, setTitle] = useState('');
|
||||
const [remarks, setRemarks] = useState('');
|
||||
const [indicators, setIndicators] = useState<Indicator[]>([]);
|
||||
|
||||
// Dropdown options state
|
||||
const [allFrameworks, setAllFrameworks] = useState<string[]>([]);
|
||||
const [allLearningAreas, setAllLearningAreas] = useState<{ [key: number]: string[] }>({});
|
||||
const [allSubLearningAreas, setAllSubLearningAreas] = useState<{ [key: number]: string[] }>({});
|
||||
const [allIndicators, setAllIndicators] = useState<{ [key: number]: string[] }>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchObservation();
|
||||
}, [id]);
|
||||
|
||||
// Load dropdown options
|
||||
useEffect(() => {
|
||||
fetch('/api/frameworks')
|
||||
.then(r => r.json())
|
||||
.then(d => d.success && setAllFrameworks(d.data.frameworks))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
// Load filtered indicators when framework/learning area/sub-learning area changes
|
||||
useEffect(() => {
|
||||
indicators.forEach((indicator, index) => {
|
||||
const framework = indicator.developmentAreas[0]?.framework;
|
||||
const learningArea = indicator.developmentAreas[0]?.learningArea;
|
||||
const subLearningArea = indicator.developmentAreas[0]?.subLearningArea;
|
||||
|
||||
if (framework || learningArea || subLearningArea) {
|
||||
const params = new URLSearchParams();
|
||||
if (framework) params.set('framework', framework);
|
||||
if (learningArea) params.set('learningArea', learningArea);
|
||||
if (subLearningArea) params.set('subLearningArea', subLearningArea);
|
||||
|
||||
fetch(`/api/indicators-by-filter?${params.toString()}`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
setAllIndicators(prev => ({ ...prev, [index]: d.data.indicators }));
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
});
|
||||
}, [indicators.map(ind => `${ind.developmentAreas[0]?.framework}-${ind.developmentAreas[0]?.learningArea}-${ind.developmentAreas[0]?.subLearningArea}`).join(',')]);
|
||||
|
||||
// Load learning areas when framework changes for each indicator
|
||||
useEffect(() => {
|
||||
indicators.forEach((indicator, index) => {
|
||||
const framework = indicator.developmentAreas[0]?.framework;
|
||||
if (framework) {
|
||||
fetch(`/api/learning-areas?framework=${encodeURIComponent(framework)}`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
setAllLearningAreas(prev => ({ ...prev, [index]: d.data.learningAreas }));
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
});
|
||||
}, [indicators.map(ind => ind.developmentAreas[0]?.framework).join(',')]);
|
||||
|
||||
// Load sub-learning areas when framework or learning area changes
|
||||
useEffect(() => {
|
||||
indicators.forEach((indicator, index) => {
|
||||
const framework = indicator.developmentAreas[0]?.framework;
|
||||
const learningArea = indicator.developmentAreas[0]?.learningArea;
|
||||
if (framework && learningArea) {
|
||||
fetch(`/api/sub-learning-areas?framework=${encodeURIComponent(framework)}&learningArea=${encodeURIComponent(learningArea)}`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
setAllSubLearningAreas(prev => ({ ...prev, [index]: d.data.subLearningAreas }));
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
});
|
||||
}, [indicators.map(ind => `${ind.developmentAreas[0]?.framework}-${ind.developmentAreas[0]?.learningArea}`).join(',')]);
|
||||
|
||||
const fetchObservation = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/get-observation/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch observation');
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
setObservation(data);
|
||||
setTitle(data.title || '');
|
||||
setRemarks(data.remarks || '');
|
||||
setIndicators(data.indicators || []);
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to fetch observation');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
const response = await fetch(`/api/update-observation/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
remarks,
|
||||
indicators,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setSuccess('Observation updated successfully!');
|
||||
} else {
|
||||
setError(result.error || 'Failed to update observation');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(`/api/delete-observation/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Redirect to view-development-areas after successful deletion
|
||||
router.push('/view-development-areas');
|
||||
} else {
|
||||
setError(result.error || 'Failed to delete observation');
|
||||
setShowDeleteConfirm(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
setShowDeleteConfirm(false);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addIndicator = () => {
|
||||
setIndicators([
|
||||
...indicators,
|
||||
{
|
||||
indicator: '',
|
||||
developmentAreas: [
|
||||
{
|
||||
learningArea: '',
|
||||
subLearningArea: '',
|
||||
framework: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const updateIndicator = (index: number, field: string, value: string) => {
|
||||
const updatedIndicators = [...indicators];
|
||||
if (field === 'indicator') {
|
||||
updatedIndicators[index].indicator = value;
|
||||
} else {
|
||||
(updatedIndicators[index].developmentAreas[0] as any)[field] = value;
|
||||
|
||||
// Clear dependent fields when framework changes
|
||||
if (field === 'framework') {
|
||||
updatedIndicators[index].developmentAreas[0].learningArea = '';
|
||||
updatedIndicators[index].developmentAreas[0].subLearningArea = '';
|
||||
// Clear indicator selection since it's no longer valid
|
||||
updatedIndicators[index].indicator = '';
|
||||
}
|
||||
|
||||
// Clear sub-learning area and indicator when learning area changes
|
||||
if (field === 'learningArea') {
|
||||
updatedIndicators[index].developmentAreas[0].subLearningArea = '';
|
||||
// Clear indicator selection since it's no longer valid
|
||||
updatedIndicators[index].indicator = '';
|
||||
}
|
||||
|
||||
// Clear indicator when sub-learning area changes
|
||||
if (field === 'subLearningArea') {
|
||||
// Clear indicator selection since it's no longer valid
|
||||
updatedIndicators[index].indicator = '';
|
||||
}
|
||||
}
|
||||
setIndicators(updatedIndicators);
|
||||
};
|
||||
|
||||
const removeIndicator = (index: number) => {
|
||||
setIndicators(indicators.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const getImageUrl = (asset: string) => {
|
||||
const filename = asset.split('/').pop();
|
||||
return `/api/proxy-image?path=${filename}`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading observation...</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && !observation) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600 text-lg mb-4">Error: {error}</p>
|
||||
<Link href="/view-development-areas" className="text-blue-600 hover:text-blue-800">
|
||||
← Back to Development Areas
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-4 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Link href="/view-development-areas" className="text-blue-600 hover:text-blue-800 mr-4">
|
||||
← Back to Development Areas
|
||||
</Link>
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
Edit Observation
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={isDeleting}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium ${
|
||||
isDeleting
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-red-600 text-white hover:bg-red-700'
|
||||
}`}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium ${
|
||||
saving
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
}`}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Success/Error Messages */}
|
||||
{success && (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<div className="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded">
|
||||
{success}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{observation && (
|
||||
<div className="space-y-8">
|
||||
{/* Image Section */}
|
||||
{observation.assets && observation.assets.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Images</h2>
|
||||
<div className="space-y-4">
|
||||
{observation.assets.map((asset, index) => (
|
||||
<div key={index} className="relative bg-gray-100 rounded-lg overflow-auto max-h-96">
|
||||
<img
|
||||
src={getImageUrl(asset)}
|
||||
alt={`Observation image ${index + 1}`}
|
||||
className="w-full h-auto object-contain"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = `https://via.placeholder.com/400x300?text=Image+Not+Found`;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Basic Information */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Basic Information</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Remarks
|
||||
</label>
|
||||
<textarea
|
||||
value={remarks}
|
||||
onChange={(e) => setRemarks(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Indicators Section */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Development Indicators</h2>
|
||||
<button
|
||||
onClick={addIndicator}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
|
||||
>
|
||||
Add Indicator
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{indicators.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">
|
||||
No indicators added yet. Click "Add Indicator" to add one.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{indicators.map((indicator, index) => (
|
||||
<div key={index} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-md font-medium text-gray-900">Indicator {index + 1}</h3>
|
||||
<button
|
||||
onClick={() => removeIndicator(index)}
|
||||
className="text-red-600 hover:text-red-800 text-sm"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Framework
|
||||
</label>
|
||||
<select
|
||||
value={indicator.developmentAreas[0]?.framework || ''}
|
||||
onChange={(e) => updateIndicator(index, 'framework', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
|
||||
>
|
||||
<option value="">Select Framework</option>
|
||||
{allFrameworks.map(framework => (
|
||||
<option key={framework} value={framework}>{framework}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Learning Area
|
||||
</label>
|
||||
<select
|
||||
value={indicator.developmentAreas[0]?.learningArea || ''}
|
||||
onChange={(e) => updateIndicator(index, 'learningArea', e.target.value)}
|
||||
disabled={!indicator.developmentAreas[0]?.framework}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="">Select Learning Area</option>
|
||||
{(allLearningAreas[index] || []).map((area: string, areaIdx: number) => (
|
||||
<option key={`${area}-${areaIdx}`} value={area}>{area}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Sub Learning Area
|
||||
</label>
|
||||
<select
|
||||
value={indicator.developmentAreas[0]?.subLearningArea || ''}
|
||||
onChange={(e) => updateIndicator(index, 'subLearningArea', e.target.value)}
|
||||
disabled={!indicator.developmentAreas[0]?.learningArea}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="">Select Sub Learning Area</option>
|
||||
{(allSubLearningAreas[index] || []).map((area: string, subAreaIdx: number) => (
|
||||
<option key={`${area}-${subAreaIdx}`} value={area}>{area}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Indicator Description
|
||||
</label>
|
||||
<select
|
||||
value={indicator.indicator}
|
||||
onChange={(e) => updateIndicator(index, 'indicator', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
|
||||
>
|
||||
<option value="">Select Indicator</option>
|
||||
{(allIndicators[index] || []).map((indicatorDesc: string, idx: number) => (
|
||||
<option key={`${index}-${idx}`} value={indicatorDesc}>{indicatorDesc}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Metadata</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">ID:</span>
|
||||
<span className="ml-2 text-gray-600">{observation._id}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Created:</span>
|
||||
<span className="ml-2 text-gray-600">
|
||||
{new Date(observation.createdAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Last Updated:</span>
|
||||
<span className="ml-2 text-gray-600">
|
||||
{new Date(observation.updatedAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
{observation.grade && observation.grade.length > 0 && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Grade:</span>
|
||||
<span className="ml-2 text-gray-600">{observation.grade[0].grade}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{showDeleteConfirm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-md mx-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Confirm Delete</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Are you sure you want to delete this observation? This action cannot be undone.
|
||||
</p>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium ${
|
||||
isDeleting
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-red-600 text-white hover:bg-red-700'
|
||||
}`}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
disabled={isDeleting}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 text-sm font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
397
src/app/edit-observation/[id]/page.tsx.backup
Normal file
397
src/app/edit-observation/[id]/page.tsx.backup
Normal file
@@ -0,0 +1,397 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import Layout from '@/components/Layout';
|
||||
|
||||
interface DevelopmentArea {
|
||||
learningArea: string;
|
||||
subLearningArea: string;
|
||||
framework: string;
|
||||
}
|
||||
|
||||
interface Indicator {
|
||||
indicator: string;
|
||||
developmentAreas: DevelopmentArea[];
|
||||
}
|
||||
|
||||
interface Observation {
|
||||
_id: string;
|
||||
title: string;
|
||||
remarks: string;
|
||||
assets: string[];
|
||||
indicators: Indicator[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
grade?: Array<{
|
||||
grade: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default function EditObservation() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const id = params.id as string;
|
||||
|
||||
const [observation, setObservation] = useState<Observation | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [title, setTitle] = useState('');
|
||||
const [remarks, setRemarks] = useState('');
|
||||
const [indicators, setIndicators] = useState<Indicator[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchObservation();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const fetchObservation = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/get-observation/${id}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setObservation(data.data);
|
||||
setTitle(data.data.title || '');
|
||||
setRemarks(data.data.remarks || '');
|
||||
setIndicators(data.data.indicators || []);
|
||||
} else {
|
||||
setError('Failed to fetch observation');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Error fetching observation');
|
||||
console.error('Error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
setSuccess(null);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(`/api/update-observation/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
remarks,
|
||||
indicators,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setSuccess('Observation updated successfully!');
|
||||
// Update local state
|
||||
setObservation(data.data);
|
||||
} else {
|
||||
setError(data.message || 'Failed to update observation');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Error updating observation');
|
||||
console.error('Error:', err);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addIndicator = () => {
|
||||
setIndicators([...indicators, {
|
||||
indicator: '',
|
||||
developmentAreas: [{
|
||||
learningArea: '',
|
||||
subLearningArea: '',
|
||||
framework: 'EYFS'
|
||||
}]
|
||||
}]);
|
||||
};
|
||||
|
||||
const updateIndicator = (index: number, field: string, value: string) => {
|
||||
const updatedIndicators = [...indicators];
|
||||
if (field === 'indicator') {
|
||||
updatedIndicators[index].indicator = value;
|
||||
} else {
|
||||
// Type assertion to handle dynamic field access
|
||||
(updatedIndicators[index].developmentAreas[0] as any)[field] = value;
|
||||
}
|
||||
setIndicators(updatedIndicators);
|
||||
};
|
||||
|
||||
const removeIndicator = (index: number) => {
|
||||
setIndicators(indicators.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const getImageUrl = (asset: string) => {
|
||||
const filename = asset.split('/').pop();
|
||||
return `/api/proxy-image?path=${filename}`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading observation...</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && !observation) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600 text-lg mb-4">Error: {error}</p>
|
||||
<Link href="/view-development-areas" className="text-blue-600 hover:text-blue-800">
|
||||
← Back to Development Areas
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-4 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Link href="/view-development-areas" className="text-blue-600 hover:text-blue-800 mr-4">
|
||||
← Back to Development Areas
|
||||
</Link>
|
||||
<h1 className="text-xl font-semibold text-gray-900">
|
||||
Edit Observation
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium ${
|
||||
saving
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
}`}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Success/Error Messages */}
|
||||
{success && (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<div className="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded">
|
||||
{success}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
|
||||
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{observation && (
|
||||
<div className="space-y-8">
|
||||
{/* Image Section */}
|
||||
{observation.assets && observation.assets.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Images</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{observation.assets.map((asset, index) => (
|
||||
<div key={index} className="relative h-48 bg-gray-200 rounded-lg overflow-hidden">
|
||||
<Image
|
||||
src={getImageUrl(asset)}
|
||||
alt={`Observation image ${index + 1}`}
|
||||
fill
|
||||
className="object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = `https://via.placeholder.com/400x300?text=Image+Not+Found`;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Basic Information */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Basic Information</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="remarks" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Remarks
|
||||
</label>
|
||||
<textarea
|
||||
id="remarks"
|
||||
value={remarks}
|
||||
onChange={(e) => setRemarks(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Development Indicators */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Development Indicators</h2>
|
||||
<button
|
||||
onClick={addIndicator}
|
||||
className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700"
|
||||
>
|
||||
+ Add Indicator
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{indicators.map((indicator, index) => (
|
||||
<div key={index} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<h3 className="text-sm font-medium text-gray-700">Indicator {index + 1}</h3>
|
||||
<button
|
||||
onClick={() => removeIndicator(index)}
|
||||
className="text-red-600 hover:text-red-800 text-sm"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Indicator Description
|
||||
</label>
|
||||
<textarea
|
||||
value={indicator.indicator}
|
||||
onChange={(e) => updateIndicator(index, 'indicator', e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{indicator.developmentAreas && indicator.developmentAreas.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Learning Area
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={indicator.developmentAreas[0].learningArea}
|
||||
onChange={(e) => updateIndicator(index, 'learningArea', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Sub Learning Area
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={indicator.developmentAreas[0].subLearningArea}
|
||||
onChange={(e) => updateIndicator(index, 'subLearningArea', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Framework
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={indicator.developmentAreas[0].framework}
|
||||
onChange={(e) => updateIndicator(index, 'framework', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{indicators.length === 0 && (
|
||||
<p className="text-gray-500 text-center py-4">
|
||||
No indicators added yet. Click "Add Indicator" to add one.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Metadata</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">ID:</span>
|
||||
<span className="ml-2 text-gray-600">{observation._id}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Created:</span>
|
||||
<span className="ml-2 text-gray-600">
|
||||
{new Date(observation.createdAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Last Updated:</span>
|
||||
<span className="ml-2 text-gray-600">
|
||||
{new Date(observation.updatedAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
{observation.grade && observation.grade.length > 0 && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Grade:</span>
|
||||
<span className="ml-2 text-gray-600">{observation.grade[0].grade}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
26
src/app/globals.css
Normal file
26
src/app/globals.css
Normal file
@@ -0,0 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
180
src/app/import-json/page.tsx
Normal file
180
src/app/import-json/page.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function ImportJsonPage() {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState<any>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = e.target.files?.[0];
|
||||
if (selectedFile && selectedFile.type === 'application/json') {
|
||||
setFile(selectedFile);
|
||||
setError(null);
|
||||
} else if (selectedFile) {
|
||||
setError('Please select a valid JSON file');
|
||||
setFile(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!file) {
|
||||
setError('Please select a JSON file');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch('/api/import-json', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Import failed');
|
||||
}
|
||||
|
||||
setResult(data);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 text-center">
|
||||
Upload JSON to MongoDB
|
||||
</h1>
|
||||
<p className="mt-2 text-gray-600 text-center">
|
||||
Upload observation data from JSON file into MongoDB database
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleImport} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="file" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
JSON File
|
||||
</label>
|
||||
<div className="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md hover:border-gray-400 transition-colors">
|
||||
<div className="space-y-1 text-center">
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 0 48 48"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex text-sm text-gray-600">
|
||||
<label
|
||||
htmlFor="file"
|
||||
className="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-blue-500"
|
||||
>
|
||||
<span>Upload a file</span>
|
||||
<input
|
||||
id="file"
|
||||
name="file"
|
||||
type="file"
|
||||
className="sr-only"
|
||||
accept=".json,application/json"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</label>
|
||||
<p className="pl-1">or drag and drop</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">JSON files only</p>
|
||||
</div>
|
||||
</div>
|
||||
{file && (
|
||||
<div className="mt-2 text-sm text-gray-600">
|
||||
Selected: <span className="font-medium">{file.name}</span> ({(file.size / 1024).toFixed(2)} KB)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !file}
|
||||
className={`w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white ${
|
||||
loading || !file
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
}`}
|
||||
>
|
||||
{loading ? 'Importing...' : 'Import JSON'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{error && (
|
||||
<div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-md">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">Import Failed</h3>
|
||||
<div className="mt-2 text-sm text-red-700">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="mt-6 p-4 bg-green-50 border border-green-200 rounded-md">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-green-800">Import Successful</h3>
|
||||
<div className="mt-2 text-sm text-green-700">
|
||||
<p>{result.message}</p>
|
||||
<p className="mt-1">Documents inserted: {result.inserted_count}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8">
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Back to Home
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
src/app/layout.tsx
Normal file
34
src/app/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
128
src/app/page.tsx
Normal file
128
src/app/page.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import Layout from "@/components/Layout";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="text-center">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Hero Section */}
|
||||
<div className="mb-16">
|
||||
<h1 className="text-5xl font-bold text-slate-900 mb-6 leading-tight">
|
||||
SiliconPin Data Classifier
|
||||
</h1>
|
||||
<p className="text-xl text-slate-600 mb-8 leading-relaxed max-w-3xl mx-auto">
|
||||
Annotate and modify data before sending to AI for retraining.
|
||||
Ensure your AI receives clean, correctly structured data for optimal performance.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<Link
|
||||
className="inline-flex items-center justify-center px-8 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors shadow-lg"
|
||||
href="/import-json"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
Import JSON Data
|
||||
</Link>
|
||||
<Link
|
||||
className="inline-flex items-center justify-center px-8 py-3 bg-white text-slate-700 font-semibold rounded-lg border border-slate-300 hover:bg-slate-50 transition-colors shadow-lg"
|
||||
href="/view-development-areas"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
View & Edit Data
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="grid md:grid-cols-3 gap-8 mb-16">
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-200">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4 mx-auto">
|
||||
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-2">Data Import</h3>
|
||||
<p className="text-slate-600">
|
||||
Seamlessly import JSON observation data into MongoDB for processing and classification.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-200">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4 mx-auto">
|
||||
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-2">Data Annotation</h3>
|
||||
<p className="text-slate-600">
|
||||
Modify and annotate observation data to ensure clean, structured input for AI training.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-200">
|
||||
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mb-4 mx-auto">
|
||||
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-2">AI Training Ready</h3>
|
||||
<p className="text-slate-600">
|
||||
Export clean, structured data optimized for AI model retraining and improvement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Section */}
|
||||
<div className="bg-white p-8 rounded-xl shadow-sm border border-slate-200">
|
||||
<h2 className="text-2xl font-bold text-slate-900 mb-6">How It Works</h2>
|
||||
<div className="grid md:grid-cols-4 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mb-3 mx-auto">
|
||||
1
|
||||
</div>
|
||||
<h4 className="font-semibold text-slate-900 mb-2">Import Data</h4>
|
||||
<p className="text-sm text-slate-600">
|
||||
Upload JSON files containing observation data
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mb-3 mx-auto">
|
||||
2
|
||||
</div>
|
||||
<h4 className="font-semibold text-slate-900 mb-2">Review & Edit</h4>
|
||||
<p className="text-sm text-slate-600">
|
||||
View and modify data in the card-based interface
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mb-3 mx-auto">
|
||||
3
|
||||
</div>
|
||||
<h4 className="font-semibold text-slate-900 mb-2">Annotate</h4>
|
||||
<p className="text-sm text-slate-600">
|
||||
Add development areas and indicators
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold mb-3 mx-auto">
|
||||
4
|
||||
</div>
|
||||
<h4 className="font-semibold text-slate-900 mb-2">Export for AI</h4>
|
||||
<p className="text-sm text-slate-600">
|
||||
Clean data ready for AI model training
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
366
src/app/view-development-areas/page.tsx
Normal file
366
src/app/view-development-areas/page.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Layout from '@/components/Layout';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface DevelopmentArea {
|
||||
learningArea: string;
|
||||
subLearningArea: string;
|
||||
framework: string;
|
||||
}
|
||||
|
||||
interface Indicator {
|
||||
indicator: string;
|
||||
developmentAreas: DevelopmentArea[];
|
||||
}
|
||||
|
||||
interface Observation {
|
||||
_id: string;
|
||||
title: string;
|
||||
remarks: string;
|
||||
assets: string[];
|
||||
indicators: Indicator[];
|
||||
createdAt: string;
|
||||
grade?: Array<{ grade: string }>;
|
||||
}
|
||||
|
||||
interface PaginationInfo {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalCount: number;
|
||||
limit: number;
|
||||
hasNextPage: boolean;
|
||||
hasPrevPage: boolean;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
success: boolean;
|
||||
data: Observation[];
|
||||
pagination: PaginationInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function getPageNumbers(currentPage: number, totalPages: number): (number | string)[] {
|
||||
const delta = 2;
|
||||
const range: (number | string)[] = [1];
|
||||
|
||||
const start = Math.max(2, currentPage - delta);
|
||||
const end = Math.min(totalPages - 1, currentPage + delta);
|
||||
|
||||
if (start > 2) range.push('...');
|
||||
for (let i = start; i <= end; i++) range.push(i);
|
||||
if (end < totalPages - 1) range.push('...');
|
||||
if (totalPages > 1) range.push(totalPages);
|
||||
|
||||
return range.filter((item, idx, arr) => item !== arr[idx - 1]);
|
||||
}
|
||||
|
||||
function formatDate(dateString: string) {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function getImageUrl(asset: string) {
|
||||
const filename = asset?.split('/').pop();
|
||||
return `/api/proxy-image?path=${filename}`;
|
||||
}
|
||||
|
||||
const FALLBACK_IMG =
|
||||
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2UwZTBlMCIvPjx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE0IiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSI+SW1hZ2U8L3RleHQ+PC9zdmc+';
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function ViewDevelopmentAreas() {
|
||||
const [observations, setObservations] = useState<Observation[]>([]);
|
||||
const [pagination, setPagination] = useState<PaginationInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(12);
|
||||
|
||||
const [frameworkFilter, setFrameworkFilter] = useState('');
|
||||
const [learningAreaFilter, setLearningAreaFilter] = useState('');
|
||||
const [subLearningAreaFilter, setSubLearningAreaFilter] = useState('');
|
||||
|
||||
const [allFrameworks, setAllFrameworks] = useState<string[]>([]);
|
||||
const [allLearningAreas, setAllLearningAreas] = useState<string[]>([]);
|
||||
const [allSubLearningAreas, setAllSubLearningAreas] = useState<string[]>([]);
|
||||
|
||||
// ── ONE fetch function — passes filters as server-side query params ──────────
|
||||
const fetchObservations = useCallback(async (page = currentPage) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const params = new URLSearchParams({ page: String(page), limit: String(pageSize) });
|
||||
if (frameworkFilter) params.set('framework', frameworkFilter);
|
||||
if (learningAreaFilter) params.set('learningArea', learningAreaFilter);
|
||||
if (subLearningAreaFilter) params.set('subLearningArea', subLearningAreaFilter);
|
||||
|
||||
const res = await fetch(`/api/get-observations?${params}`);
|
||||
const data: ApiResponse = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
setObservations(data.data);
|
||||
setPagination(data.pagination);
|
||||
} else {
|
||||
setError(data.error || 'Failed to fetch observations');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentPage, pageSize, frameworkFilter, learningAreaFilter, subLearningAreaFilter]);
|
||||
|
||||
useEffect(() => { fetchObservations(currentPage); }, [currentPage, pageSize, frameworkFilter, learningAreaFilter, subLearningAreaFilter]);
|
||||
|
||||
// ── Load dropdown options ────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
fetch('/api/frameworks')
|
||||
.then(r => r.json())
|
||||
.then(d => d.success && setAllFrameworks(d.data.frameworks))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!frameworkFilter) { setAllLearningAreas([]); setLearningAreaFilter(''); return; }
|
||||
fetch(`/api/learning-areas?framework=${encodeURIComponent(frameworkFilter)}`)
|
||||
.then(r => r.json())
|
||||
.then(d => d.success && setAllLearningAreas(d.data.learningAreas))
|
||||
.catch(console.error);
|
||||
}, [frameworkFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!frameworkFilter || !learningAreaFilter) { setAllSubLearningAreas([]); setSubLearningAreaFilter(''); return; }
|
||||
fetch(`/api/sub-learning-areas?framework=${encodeURIComponent(frameworkFilter)}&learningArea=${encodeURIComponent(learningAreaFilter)}`)
|
||||
.then(r => r.json())
|
||||
.then(d => d.success && setAllSubLearningAreas(d.data.subLearningAreas))
|
||||
.catch(console.error);
|
||||
}, [frameworkFilter, learningAreaFilter]);
|
||||
|
||||
// ── Handlers ────────────────────────────────────────────────────────────────
|
||||
const handlePageChange = (page: number) => {
|
||||
if (!pagination || page < 1 || page > pagination.totalPages) return;
|
||||
setCurrentPage(page);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setFrameworkFilter('');
|
||||
setLearningAreaFilter('');
|
||||
setSubLearningAreaFilter('');
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// ── Loading / Error ──────────────────────────────────────────────────────────
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto" />
|
||||
<p className="mt-4 text-gray-600">Loading observations...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600 text-lg mb-4">Error: {error}</p>
|
||||
<button onClick={() => fetchObservations()} className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Render ───────────────────────────────────────────────────────────────────
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-4 mb-6 flex items-center justify-between">
|
||||
<h1 className="text-xl font-semibold text-gray-900">Development Areas View</h1>
|
||||
<div className="flex items-center space-x-2">
|
||||
<label htmlFor="page-size" className="text-sm text-gray-600">Show:</label>
|
||||
<select
|
||||
id="page-size"
|
||||
value={pageSize}
|
||||
onChange={e => { setPageSize(Number(e.target.value)); setCurrentPage(1); }}
|
||||
className="text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{[6, 12, 24, 48].map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
<span className="text-sm text-gray-600">per page</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-4 mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Filters</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label htmlFor="framework" className="block text-sm font-medium text-gray-700 mb-1">Framework</label>
|
||||
<select
|
||||
id="framework"
|
||||
value={frameworkFilter}
|
||||
onChange={e => { setFrameworkFilter(e.target.value); setLearningAreaFilter(''); setSubLearningAreaFilter(''); setCurrentPage(1); }}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
|
||||
>
|
||||
<option value="">All Frameworks</option>
|
||||
{allFrameworks.map(f => <option key={f} value={f}>{f}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="learning-area" className="block text-sm font-medium text-gray-700 mb-1">Learning Area</label>
|
||||
<select
|
||||
id="learning-area"
|
||||
value={learningAreaFilter}
|
||||
onChange={e => { setLearningAreaFilter(e.target.value); setSubLearningAreaFilter(''); setCurrentPage(1); }}
|
||||
disabled={!frameworkFilter}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="">All Learning Areas</option>
|
||||
{allLearningAreas.map(a => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="sub-learning-area" className="block text-sm font-medium text-gray-700 mb-1">Sub Learning Area</label>
|
||||
<select
|
||||
id="sub-learning-area"
|
||||
value={subLearningAreaFilter}
|
||||
onChange={e => { setSubLearningAreaFilter(e.target.value); setCurrentPage(1); }}
|
||||
disabled={!learningAreaFilter}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="">All Sub Learning Areas</option>
|
||||
{allSubLearningAreas.map(a => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<button onClick={clearFilters} className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 text-sm font-medium">
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{observations.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 text-lg">No observations found</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{pagination && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-4 mb-6 text-sm text-gray-500">
|
||||
Showing {((pagination.currentPage - 1) * pagination.limit) + 1}–
|
||||
{Math.min(pagination.currentPage * pagination.limit, pagination.totalCount)} of {pagination.totalCount} observations
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{observations.map((obs, index) => (
|
||||
<Link key={`${obs._id}-${index}`} href={`/edit-observation/${obs._id}`} className="block">
|
||||
<div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg hover:scale-105 transform transition-all duration-200 cursor-pointer">
|
||||
<div className="relative h-48 bg-gray-200">
|
||||
<img
|
||||
src={getImageUrl(obs.assets?.[0])}
|
||||
alt={obs.title}
|
||||
className="w-full h-full object-cover"
|
||||
onError={e => { e.currentTarget.src = FALLBACK_IMG; }}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
|
||||
<span className="text-white font-medium">Click to Edit</span>
|
||||
</div>
|
||||
{obs.grade?.[0] && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<span className="bg-green-600 text-white text-xs px-2 py-1 rounded-full">{obs.grade[0].grade}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold text-lg text-gray-900 mb-1">{obs.title || 'Untitled Observation'}</h3>
|
||||
<p className="text-sm text-gray-500 mb-2">{formatDate(obs.createdAt)}</p>
|
||||
{obs.remarks && <p className="text-gray-600 text-sm mb-3 line-clamp-2">{obs.remarks}</p>}
|
||||
{obs.indicators?.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-sm text-gray-700">Development Areas:</h4>
|
||||
{obs.indicators.slice(0, 2).map((ind, idx) => (
|
||||
<div key={idx} className="text-xs">
|
||||
<p className="text-gray-600 mb-1 line-clamp-2">{ind.indicator}</p>
|
||||
{ind.developmentAreas?.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{ind.developmentAreas.map((area, aIdx) => (
|
||||
<span key={aIdx} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">{area.learningArea}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{obs.indicators.length > 2 && (
|
||||
<p className="text-xs text-gray-500">+{obs.indicators.length - 2} more indicators</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{pagination && pagination.totalPages > 1 && (
|
||||
<div className="mt-8 flex justify-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handlePageChange(pagination.currentPage - 1)}
|
||||
disabled={!pagination.hasPrevPage}
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium ${pagination.hasPrevPage ? 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50' : 'bg-gray-100 text-gray-400 cursor-not-allowed'}`}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<div className="flex space-x-1">
|
||||
{getPageNumbers(pagination.currentPage, pagination.totalPages).map((pageNum, idx) =>
|
||||
pageNum === '...' ? (
|
||||
<span key={`ellipsis-${idx}`} className="px-3 py-2 text-gray-500">...</span>
|
||||
) : (
|
||||
<button
|
||||
key={pageNum}
|
||||
onClick={() => handlePageChange(pageNum as number)}
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium ${pageNum === pagination.currentPage ? 'bg-blue-600 text-white' : 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50'}`}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handlePageChange(pagination.currentPage + 1)}
|
||||
disabled={!pagination.hasNextPage}
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium ${pagination.hasNextPage ? 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50' : 'bg-gray-100 text-gray-400 cursor-not-allowed'}`}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
83
src/components/Footer.tsx
Normal file
83
src/components/Footer.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-slate-900 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="grid md:grid-cols-4 gap-8">
|
||||
<div className="col-span-2">
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">SP</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold">SiliconPin</span>
|
||||
</div>
|
||||
<p className="text-slate-400 mb-4 max-w-md">
|
||||
Advanced data classification platform for AI training. Transform raw observation data into clean, structured datasets for optimal machine learning performance.
|
||||
</p>
|
||||
<div className="flex space-x-4">
|
||||
<a href="https://siliconpin.com" target="_blank" rel="noopener noreferrer" className="text-slate-400 hover:text-white transition-colors">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369 1.343-3.369 1.343-.452 1.043-1.119 1.343-1.343.18.31.28.672.28 1.065 0 .019 0 .12-.008.179-.015-.41.295-.762.75-1.091 1.022A4.92 4.92 0 0112 19.5c-3.308 0-6-2.692-6-6s2.692-6 6-6 6 2.692 6 6c.99 0 1.916-.255 2.75-.7a4.92 4.92 0 001.5-1.022c.32-.272.58-.727.76-1.022.007.059.015.119.015.179 0 .266-.182.393-.483.683-.483.835 0 1.835.005 1.703.013 1.703-.18.31-.503.617-.683 1.02C19.135 17.825 22 14.42 22 12c0-5.523-4.477-10-10-10z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-slate-400 hover:text-white transition-colors">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 006.675 2.493c.086 0 .173-.01.255-.015a8.348 8.348 0 006.432-7.744c0-.18-.005-.362-.013-.54A8.372 8.372 0 0022 10.292a8.236 8.236 0 01-2.416.662z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-white mb-4">Product</h3>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link href="/import-json" className="text-slate-400 hover:text-white transition-colors">
|
||||
Data Import
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/view-development-areas" className="text-slate-400 hover:text-white transition-colors">
|
||||
Data Viewer
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/download-images" className="text-slate-400 hover:text-white transition-colors">
|
||||
Image Manager
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-white mb-4">Resources</h3>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<a href="https://siliconpin.com/docs" target="_blank" rel="noopener noreferrer" className="text-slate-400 hover:text-white transition-colors">
|
||||
Documentation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://siliconpin.com/api" target="_blank" rel="noopener noreferrer" className="text-slate-400 hover:text-white transition-colors">
|
||||
API Reference
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://siliconpin.com/support" target="_blank" rel="noopener noreferrer" className="text-slate-400 hover:text-white transition-colors">
|
||||
Support
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-800 mt-8 pt-8 text-center">
|
||||
<p className="text-slate-400">
|
||||
© 2026 SiliconPin Data Classifier. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
34
src/components/Header.tsx
Normal file
34
src/components/Header.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className="bg-white shadow-sm border-b border-slate-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">SP</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-slate-900">SiliconPin</span>
|
||||
</Link>
|
||||
<span className="text-slate-500">|</span>
|
||||
<span className="text-slate-600 font-medium">Data Classifier</span>
|
||||
</div>
|
||||
<nav className="hidden md:flex space-x-8">
|
||||
<Link href="/" className="text-slate-600 hover:text-slate-900 font-medium">
|
||||
Home
|
||||
</Link>
|
||||
<Link href="/view-development-areas" className="text-slate-600 hover:text-slate-900 font-medium">
|
||||
View Data
|
||||
</Link>
|
||||
<Link href="/data" className="text-slate-600 hover:text-slate-900 font-medium">
|
||||
Data
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
18
src/components/Layout.tsx
Normal file
18
src/components/Layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
tsconfig.json
Normal file
34
tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user