|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -0,0 +1,670 @@
|
||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 w-full h-full flex bg-update-bg bg-cover bg-center">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="absolute left-5 top-5 h-1/4 bg-gray-700 bg-opacity-90 border-r-4 border-gray-500 pixel-border shadow-lg w-16 flex flex-col items-center p-4">
|
||||||
|
<nav class="flex flex-col space-y-6 text-center mt-4">
|
||||||
|
<a href="/dashboard" class="text-white hover:text-yellow-400 transition">
|
||||||
|
<LayoutDashboard class="w-8 h-8" />
|
||||||
|
</a>
|
||||||
|
<a href="/scraper" class="text-white hover:text-blue-400 transition">
|
||||||
|
<Bot class="w-8 h-8" />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="flex-1 ml-20 overflow-y-auto p-4">
|
||||||
|
<Modal v-if="isModalOpen" @close="isModalOpen = false">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-xl font-bold text-white">Edit Data {{ modalTitle }}</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full text-sm text-white table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="(header, index) in tableHeaders" :key="index" class="px-4 py-2 border-b border-gray-500">{{ header }}</th>
|
||||||
|
<th class="px-4 py-2 border-b border-gray-500">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, rowIndex) in editableData" :key="rowIndex">
|
||||||
|
<td v-for="(header, colIndex) in tableHeaders" :key="colIndex" class="px-4 py-2 border-b border-gray-600">
|
||||||
|
<input v-model="editableData[rowIndex][header]" class="bg-gray-700 text-white px-2 py-1 w-full rounded" />
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 border-b border-gray-600 text-center">
|
||||||
|
<button @click="removeRow(rowIndex)" class="text-red-500 hover:underline">Hapus</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button @click="addRow" class="mt-4 bg-green-500 hover:bg-green-400 text-white px-4 py-2 rounded">Tambah Baris</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button @click="saveChanges" class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded">Simpan Perubahan</button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
<div class="w-full bg-gray-700 bg-opacity-90 border-4 border-gray-500 pixel-border flex flex-col items-center p-6">
|
||||||
|
<h2 class="text-white text-xl font-bold mb-6">DASHBOARD</h2>
|
||||||
|
<div class="flex items-center gap-4 mb-10">
|
||||||
|
<button @click="refreshData" class="text-white bg-green-600 hover:bg-green-500 px-4 py-2 rounded-2xl">
|
||||||
|
Refresh Grafik
|
||||||
|
</button>
|
||||||
|
<button @click="clearCache" class="text-white bg-red-600 hover:bg-red-500 px-4 py-2 rounded-2xl">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
|
||||||
|
|
||||||
|
<!-- Pengabdian -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Pengabdian</h3>
|
||||||
|
<div class="relative flex justify-end gap-2">
|
||||||
|
<a
|
||||||
|
:href="`http://localhost:8000/download/pengabdian`"
|
||||||
|
class="text-xs bg-green-600 hover:bg-green-500 text-white px-2 py-1 rounded"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Excel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="refreshSingleData('pengabdian')"
|
||||||
|
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
title="Reload Grafik"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal('pengabdian')" class="text-sm text-yellow-400 hover:underline mb-10">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="pengabdian.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="pengabdianChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HKI -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">HKI</h3>
|
||||||
|
<div class="relative flex justify-end gap-2">
|
||||||
|
<a
|
||||||
|
:href="`http://localhost:8000/download/hki`"
|
||||||
|
class="text-xs bg-green-600 hover:bg-green-500 text-white px-2 py-1 rounded"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Excel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="refreshSingleData('hki')"
|
||||||
|
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
title="Reload Grafik"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal('hki')" class="text-sm text-yellow-400 hover:underline mb-10">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropHKI"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="hki.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="hkiChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Penelitian -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Penelitian</h3>
|
||||||
|
<div class="relative flex justify-end gap-2">
|
||||||
|
<a
|
||||||
|
:href="`http://localhost:8000/download/penelitian`"
|
||||||
|
class="text-xs bg-green-600 hover:bg-green-500 text-white px-2 py-1 rounded"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Excel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="refreshSingleData('penelitian')"
|
||||||
|
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
title="Reload Grafik"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal('penelitian')" class="text-sm text-yellow-400 hover:underline mb-10">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropPenelitian"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="penelitian.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="penelitianChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scopus -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between mt-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Scopus</h3>
|
||||||
|
<div class="relative flex justify-end gap-2">
|
||||||
|
<a
|
||||||
|
:href="`http://localhost:8000/download/scopus`"
|
||||||
|
class="text-xs bg-green-600 hover:bg-green-500 text-white px-2 py-1 rounded"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Excel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="refreshSingleData('scopus')"
|
||||||
|
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
title="Reload Grafik"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal('scopus')" class="text-sm text-yellow-400 hover:underline mb-10">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropScopus"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="scopus.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="scopusChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between mt-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Scholar</h3>
|
||||||
|
<div class="relative flex justify-end gap-2">
|
||||||
|
<a
|
||||||
|
:href="`http://localhost:8000/download/scholar`"
|
||||||
|
class="text-xs bg-green-600 hover:bg-green-500 text-white px-2 py-1 rounded"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Excel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="refreshSingleData('scholar')"
|
||||||
|
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
title="Reload Grafik"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal('scholar')" class="text-sm text-yellow-400 hover:underline mb-10">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropScholar"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="scholar.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="scholarChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-[500px] col-span-1 md:col-span-2 mt-10">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-lg font-semibold">Kontribusi Luaran Publikasi Dosen per Tahun</h3>
|
||||||
|
<select v-model="selectedYear" class="bg-gray-700 text-white p-2 rounded">
|
||||||
|
<option v-for="year in availableYears" :key="year" :value="year">{{ year }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="w-80 h-80 mx-auto">
|
||||||
|
<canvas id="luaranPieChart" class="w-80 h-80" width="320" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-700 bg-opacity-90 border-4 border-gray-500 pixel-border flex flex-col items-center p-6">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Modal from '@/components/Modal.vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { LayoutDashboard, Bot, Settings, LogOut } from "lucide-vue-next"
|
||||||
|
import { computed, defineComponent, h } from 'vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as XLSX from 'xlsx'
|
||||||
|
import { Bar } from 'vue-chartjs'
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
Title, Tooltip, Legend,
|
||||||
|
BarElement, CategoryScale, LinearScale
|
||||||
|
} from 'chart.js'
|
||||||
|
import { onMounted, watch} from 'vue'
|
||||||
|
import Chart from 'chart.js/auto';
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCleanedFile('pengabdian', 'pengabdian_cleaned.xlsx', pengabdian)
|
||||||
|
fetchCleanedFile('penelitian', 'penelitian_cleaned.xlsx', penelitian)
|
||||||
|
fetchCleanedFile('hki', 'hki_cleaned.xlsx', hki)
|
||||||
|
fetchCleanedFile('scholar', 'scholar_cleaned.xlsx', scholar)
|
||||||
|
fetchCleanedFile('scopus', 'scopus_cleaned.xlsx', scopus)
|
||||||
|
getYearsFromData();
|
||||||
|
renderPieChart();
|
||||||
|
})
|
||||||
|
|
||||||
|
function refreshData() {
|
||||||
|
fetchCleanedFile('pengabdian', 'pengabdian_cleaned.xlsx', pengabdian)
|
||||||
|
fetchCleanedFile('penelitian', 'penelitian_cleaned.xlsx', penelitian)
|
||||||
|
fetchCleanedFile('hki', 'hki_cleaned.xlsx', hki)
|
||||||
|
fetchCleanedFile('scholar', 'scholar_cleaned.xlsx', scholar)
|
||||||
|
fetchCleanedFile('scopus', 'scopus_cleaned.xlsx', scopus)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||||
|
|
||||||
|
const pengabdian = ref([])
|
||||||
|
const hki = ref([])
|
||||||
|
const penelitian = ref([])
|
||||||
|
const scopus = ref([])
|
||||||
|
const scholar = ref([])
|
||||||
|
const isModalOpen = ref(false)
|
||||||
|
const modalTitle = ref('')
|
||||||
|
const editableData = ref([])
|
||||||
|
const tableHeaders = ref([])
|
||||||
|
const currentTargetArray = ref(null)
|
||||||
|
|
||||||
|
const selectedYear = ref(new Date().getFullYear());
|
||||||
|
const availableYears = ref([]);
|
||||||
|
let pieChartInstance = null;
|
||||||
|
|
||||||
|
function clearCache() {
|
||||||
|
pengabdian.value = []
|
||||||
|
hki.value = []
|
||||||
|
penelitian.value = []
|
||||||
|
scopus.value = []
|
||||||
|
scholar.value = []
|
||||||
|
|
||||||
|
editableData.value = []
|
||||||
|
tableHeaders.value = []
|
||||||
|
currentTargetArray.value = null
|
||||||
|
|
||||||
|
selectedYear.value = null
|
||||||
|
|
||||||
|
// Clear grafik, misal render ulang grafik kosong
|
||||||
|
renderPieChart()
|
||||||
|
|
||||||
|
// Jika kamu punya fungsi render grafik bar, panggil juga refresh grafik kosongnya
|
||||||
|
// Misal:
|
||||||
|
// pengabdianChartData.value = {}
|
||||||
|
// dan update chart (tergantung cara implementasi chartmu)
|
||||||
|
console.log("Cache data cleared")
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSingleData(type) {
|
||||||
|
switch(type) {
|
||||||
|
case 'pengabdian':
|
||||||
|
fetchCleanedFile('pengabdian', 'pengabdian_cleaned.xlsx', pengabdian);
|
||||||
|
break;
|
||||||
|
case 'hki':
|
||||||
|
fetchCleanedFile('hki', 'hki_cleaned.xlsx', hki);
|
||||||
|
break;
|
||||||
|
case 'penelitian':
|
||||||
|
fetchCleanedFile('penelitian', 'penelitian_cleaned.xlsx', penelitian);
|
||||||
|
break;
|
||||||
|
case 'scopus':
|
||||||
|
fetchCleanedFile('scopus', 'scopus_cleaned.xlsx', scopus);
|
||||||
|
break;
|
||||||
|
case 'scholar':
|
||||||
|
fetchCleanedFile('scholar', 'scholar_cleaned.xlsx', scholar);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getYearsFromData() {
|
||||||
|
const allYears = new Set();
|
||||||
|
[...hki.value, ...pengabdian.value, ...penelitian.value].forEach(item => {
|
||||||
|
const year = item?.Tahun || item?.tahun;
|
||||||
|
if (year) allYears.add(Number(year));
|
||||||
|
});
|
||||||
|
availableYears.value = Array.from(allYears).sort((a, b) => b - a);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countDataByYear(dataset, year) {
|
||||||
|
return dataset.filter(item => {
|
||||||
|
const y = item?.Tahun || item?.tahun;
|
||||||
|
return String(y) === String(year);
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPieChart() {
|
||||||
|
const year = selectedYear.value;
|
||||||
|
|
||||||
|
const hkiCount = countDataByYear(hki.value, year);
|
||||||
|
const pengabdianCount = countDataByYear(pengabdian.value, year);
|
||||||
|
const penelitianCount = countDataByYear(penelitian.value, year);
|
||||||
|
|
||||||
|
const ctx = document.getElementById('luaranPieChart').getContext('2d');
|
||||||
|
if (pieChartInstance) {
|
||||||
|
pieChartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
pieChartInstance = new Chart(ctx, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: ['HKI', 'Pengabdian', 'Penelitian'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Luaran',
|
||||||
|
data: [hkiCount, pengabdianCount, penelitianCount],
|
||||||
|
backgroundColor: ['#38bdf8', '#facc15', '#34d399'],
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: {
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([hki, pengabdian, penelitian], () => {
|
||||||
|
getYearsFromData();
|
||||||
|
renderPieChart();
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
watch(selectedYear, () => {
|
||||||
|
renderPieChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
function openModal(type) {
|
||||||
|
modalTitle.value = type.charAt(0).toUpperCase() + type.slice(1)
|
||||||
|
isModalOpen.value = true
|
||||||
|
|
||||||
|
let sourceData
|
||||||
|
if (type === 'pengabdian') {
|
||||||
|
sourceData = pengabdian.value
|
||||||
|
currentTargetArray.value = pengabdian
|
||||||
|
} else if (type === 'hki') {
|
||||||
|
sourceData = hki.value
|
||||||
|
currentTargetArray.value = hki
|
||||||
|
} else if (type === 'penelitian') {
|
||||||
|
sourceData = penelitian.value
|
||||||
|
currentTargetArray.value = penelitian
|
||||||
|
} else if (type === 'scopus') {
|
||||||
|
sourceData = scopus.value
|
||||||
|
currentTargetArray.value = scopus
|
||||||
|
} else if (type === 'scholar') {
|
||||||
|
sourceData = scholar.value
|
||||||
|
currentTargetArray.value = scholar
|
||||||
|
}
|
||||||
|
|
||||||
|
editableData.value = JSON.parse(JSON.stringify(sourceData))
|
||||||
|
tableHeaders.value = Object.keys(sourceData[0] || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRow() {
|
||||||
|
const newRow = {}
|
||||||
|
tableHeaders.value.forEach(h => newRow[h] = '')
|
||||||
|
editableData.value.push(newRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRow(index) {
|
||||||
|
editableData.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges() {
|
||||||
|
currentTargetArray.value.value = JSON.parse(JSON.stringify(editableData.value));
|
||||||
|
isModalOpen.value = false;
|
||||||
|
|
||||||
|
const folder = modalTitle.value.toLowerCase();
|
||||||
|
const filename = modalTitle.value + '_cleaned.xlsx';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post('http://localhost:8000/save-cleaned-file', {
|
||||||
|
folder,
|
||||||
|
filename,
|
||||||
|
data: editableData.value
|
||||||
|
});
|
||||||
|
alert('File berhasil disimpan dan di-overwrite di server!');
|
||||||
|
// Reload data dari server supaya update di frontend
|
||||||
|
refreshData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal menyimpan file:', error);
|
||||||
|
alert('Gagal menyimpan file. Cek console untuk detail.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function fetchCleanedFile(folder, filename, targetArray) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`http://localhost:8000/cleaned-files/${folder}/${filename}`, {
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = new Uint8Array(response.data)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
|
||||||
|
targetArray.value = json
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Gagal mengambil file cleaned (${folder}/${filename}):`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleDrop(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
pengabdian.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDropScholar(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
scholar.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scholarChartData = computed(() => {
|
||||||
|
const citationsByYear = {}
|
||||||
|
for (const item of scholar.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
const citation = parseInt(item['Total Kutipan']) || 0
|
||||||
|
citationsByYear[year] = (citationsByYear[year] || 0) + citation
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(citationsByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Total Kutipan Scholar per Tahun',
|
||||||
|
backgroundColor: '#f472b6', // pink
|
||||||
|
data: Object.values(citationsByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const pengabdianChartData = computed(() => {
|
||||||
|
const countByYear = {}
|
||||||
|
for (const item of pengabdian.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
countByYear[year] = (countByYear[year] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah Pengabdian per Tahun',
|
||||||
|
backgroundColor: '#facc15',
|
||||||
|
data: Object.values(countByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleDropHKI(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
hki.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hkiChartData = computed(() => {
|
||||||
|
const countByYear = {}
|
||||||
|
for (const item of hki.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
countByYear[year] = (countByYear[year] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah HKI per Tahun',
|
||||||
|
backgroundColor: '#38bdf8',
|
||||||
|
data: Object.values(countByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
function handleDropPenelitian(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
penelitian.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const penelitianChartData = computed(() => {
|
||||||
|
const countByYear = {}
|
||||||
|
for (const item of penelitian.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
countByYear[year] = (countByYear[year] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah Penelitian per Tahun',
|
||||||
|
backgroundColor: '#34d399', // hijau
|
||||||
|
data: Object.values(countByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
function handleDropScopus(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
scopus.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopusChartData = computed(() => {
|
||||||
|
const countByQuartile = {}
|
||||||
|
for (const item of scopus.value) {
|
||||||
|
const quartile = item.Quartile || 'Unknown'
|
||||||
|
countByQuartile[quartile] = (countByQuartile[quartile] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByQuartile),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah Publikasi Scopus per Quartile',
|
||||||
|
backgroundColor: '#a78bfa', // ungu
|
||||||
|
data: Object.values(countByQuartile),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const BarChart = defineComponent({
|
||||||
|
name: 'BarChart',
|
||||||
|
props: ['data'],
|
||||||
|
setup(props) {
|
||||||
|
return () => h(Bar, { data: props.data, options: { responsive: true, plugins: { legend: { display: true }}} })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-pixel {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixel-border {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
position: relative;
|
||||||
|
border-width: 4px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow:
|
||||||
|
inset -2px -2px 0px rgba(0, 0, 0, 0.8),
|
||||||
|
inset 2px 2px 0px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,436 @@
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 w-full h-full flex bg-dashboard-bg bg-cover bg-center">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="absolute left-5 top-5 h-5/6 bg-gray-700 bg-opacity-90 border-r-4 border-gray-500 pixel-border shadow-lg w-16 flex flex-col items-center p-4">
|
||||||
|
<nav class="flex flex-col space-y-6 text-center mt-4">
|
||||||
|
<a href="/dashboard" class="text-white hover:text-yellow-400 transition">
|
||||||
|
<LayoutDashboard class="w-8 h-8" />
|
||||||
|
</a>
|
||||||
|
<a href="/scraper" class="text-white hover:text-blue-400 transition">
|
||||||
|
<Bot class="w-8 h-8" />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="flex-1 ml-20 overflow-y-auto p-4">
|
||||||
|
<Modal v-if="isModalOpen" @close="isModalOpen = false">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-xl font-bold text-white">Edit Data {{ modalTitle }}</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full text-sm text-white table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="(header, index) in tableHeaders" :key="index" class="px-4 py-2 border-b border-gray-500">{{ header }}</th>
|
||||||
|
<th class="px-4 py-2 border-b border-gray-500">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, rowIndex) in editableData" :key="rowIndex">
|
||||||
|
<td v-for="(header, colIndex) in tableHeaders" :key="colIndex" class="px-4 py-2 border-b border-gray-600">
|
||||||
|
<input v-model="editableData[rowIndex][header]" class="bg-gray-700 text-white px-2 py-1 w-full rounded" />
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 border-b border-gray-600 text-center">
|
||||||
|
<button @click="removeRow(rowIndex)" class="text-red-500 hover:underline">Hapus</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button @click="addRow" class="mt-4 bg-green-500 hover:bg-green-400 text-white px-4 py-2 rounded">Tambah Baris</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button @click="saveChanges" class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded">Simpan Perubahan</button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
<div class="w-full bg-gray-700 bg-opacity-90 border-4 border-gray-500 pixel-border flex flex-col items-center p-6">
|
||||||
|
<h2 class="text-white text-xl font-bold mb-6">Dashboard Scraper</h2>
|
||||||
|
<button @click="refreshData" class="text-white bg-green-600 hover:bg-green-500 px-4 py-2 rounded-2xl mb-10">
|
||||||
|
Refresh Grafik
|
||||||
|
</button>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
|
||||||
|
|
||||||
|
<!-- Pengabdian -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Pengabdian</h3>
|
||||||
|
<button @click="openModal('pengabdian')" class="text-sm text-yellow-400 hover:underline">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="pengabdian.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="pengabdianChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HKI -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">HKI</h3>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropHKI"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="hki.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="hkiChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Penelitian -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Penelitian</h3>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropPenelitian"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="penelitian.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="penelitianChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scopus -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between mt-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Scopus</h3>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropScopus"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="scopus.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="scopusChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between mt-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Scholar</h3>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropScholar"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="scholar.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="scholarChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-700 bg-opacity-90 border-4 border-gray-500 pixel-border flex flex-col items-center p-6">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Modal from '@/components/Modal.vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { LayoutDashboard, Bot, Settings, LogOut } from "lucide-vue-next"
|
||||||
|
import { computed, defineComponent, h } from 'vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as XLSX from 'xlsx'
|
||||||
|
import { Bar } from 'vue-chartjs'
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
Title, Tooltip, Legend,
|
||||||
|
BarElement, CategoryScale, LinearScale
|
||||||
|
} from 'chart.js'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCleanedFile('pengabdian', 'Pengabdian_cleaned.xlsx', pengabdian)
|
||||||
|
fetchCleanedFile('penelitian', 'Penelitian_cleaned.xlsx', penelitian)
|
||||||
|
fetchCleanedFile('hki', 'HKI_cleaned.xlsx', hki)
|
||||||
|
fetchCleanedFile('scholar', 'Scholar_cleaned.xlsx', scholar)
|
||||||
|
fetchCleanedFile('scopus', 'Scopus_cleaned.xlsx', scopus)
|
||||||
|
})
|
||||||
|
|
||||||
|
function refreshData() {
|
||||||
|
fetchCleanedFile('pengabdian', 'Pengabdian_cleaned.xlsx', pengabdian)
|
||||||
|
fetchCleanedFile('penelitian', 'Penelitian_cleaned.xlsx', penelitian)
|
||||||
|
fetchCleanedFile('hki', 'HKI_cleaned.xlsx', hki)
|
||||||
|
fetchCleanedFile('scholar', 'Scholar_cleaned.xlsx', scholar)
|
||||||
|
fetchCleanedFile('scopus', 'Scopus_cleaned.xlsx', scopus)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||||
|
|
||||||
|
const pengabdian = ref([])
|
||||||
|
const hki = ref([])
|
||||||
|
const penelitian = ref([])
|
||||||
|
const scopus = ref([])
|
||||||
|
const scholar = ref([])
|
||||||
|
const isModalOpen = ref(false)
|
||||||
|
const modalTitle = ref('')
|
||||||
|
const editableData = ref([])
|
||||||
|
const tableHeaders = ref([])
|
||||||
|
const currentTargetArray = ref(null)
|
||||||
|
|
||||||
|
function openModal(type) {
|
||||||
|
modalTitle.value = type.charAt(0).toUpperCase() + type.slice(1)
|
||||||
|
isModalOpen.value = true
|
||||||
|
|
||||||
|
let sourceData
|
||||||
|
if (type === 'pengabdian') {
|
||||||
|
sourceData = pengabdian.value
|
||||||
|
currentTargetArray.value = pengabdian
|
||||||
|
} else if (type === 'hki') {
|
||||||
|
sourceData = hki.value
|
||||||
|
currentTargetArray.value = hki
|
||||||
|
} else if (type === 'penelitian') {
|
||||||
|
sourceData = penelitian.value
|
||||||
|
currentTargetArray.value = penelitian
|
||||||
|
} else if (type === 'scopus') {
|
||||||
|
sourceData = scopus.value
|
||||||
|
currentTargetArray.value = scopus
|
||||||
|
} else if (type === 'scholar') {
|
||||||
|
sourceData = scholar.value
|
||||||
|
currentTargetArray.value = scholar
|
||||||
|
}
|
||||||
|
|
||||||
|
editableData.value = JSON.parse(JSON.stringify(sourceData))
|
||||||
|
tableHeaders.value = Object.keys(sourceData[0] || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRow() {
|
||||||
|
const newRow = {}
|
||||||
|
tableHeaders.value.forEach(h => newRow[h] = '')
|
||||||
|
editableData.value.push(newRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRow(index) {
|
||||||
|
editableData.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveChanges() {
|
||||||
|
currentTargetArray.value.value = JSON.parse(JSON.stringify(editableData.value))
|
||||||
|
isModalOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCleanedFile(folder, filename, targetArray) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`http://localhost:8000/cleaned-files/${folder}/${filename}`, {
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = new Uint8Array(response.data)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
|
||||||
|
targetArray.value = json
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Gagal mengambil file cleaned (${folder}/${filename}):`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
pengabdian.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDropScholar(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
scholar.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scholarChartData = computed(() => {
|
||||||
|
const citationsByYear = {}
|
||||||
|
for (const item of scholar.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
const citation = parseInt(item['Total Kutipan']) || 0
|
||||||
|
citationsByYear[year] = (citationsByYear[year] || 0) + citation
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(citationsByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Total Kutipan Scholar per Tahun',
|
||||||
|
backgroundColor: '#f472b6', // pink
|
||||||
|
data: Object.values(citationsByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const pengabdianChartData = computed(() => {
|
||||||
|
const countByYear = {}
|
||||||
|
for (const item of pengabdian.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
countByYear[year] = (countByYear[year] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah Pengabdian per Tahun',
|
||||||
|
backgroundColor: '#facc15',
|
||||||
|
data: Object.values(countByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleDropHKI(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
hki.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hkiChartData = computed(() => {
|
||||||
|
const countByYear = {}
|
||||||
|
for (const item of hki.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
countByYear[year] = (countByYear[year] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah HKI per Tahun',
|
||||||
|
backgroundColor: '#38bdf8',
|
||||||
|
data: Object.values(countByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
function handleDropPenelitian(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
penelitian.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const penelitianChartData = computed(() => {
|
||||||
|
const countByYear = {}
|
||||||
|
for (const item of penelitian.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
countByYear[year] = (countByYear[year] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah Penelitian per Tahun',
|
||||||
|
backgroundColor: '#34d399', // hijau
|
||||||
|
data: Object.values(countByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
function handleDropScopus(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
scopus.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopusChartData = computed(() => {
|
||||||
|
const countByQuartile = {}
|
||||||
|
for (const item of scopus.value) {
|
||||||
|
const quartile = item.Quartile || 'Unknown'
|
||||||
|
countByQuartile[quartile] = (countByQuartile[quartile] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByQuartile),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah Publikasi Scopus per Quartile',
|
||||||
|
backgroundColor: '#a78bfa', // ungu
|
||||||
|
data: Object.values(countByQuartile),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const BarChart = defineComponent({
|
||||||
|
name: 'BarChart',
|
||||||
|
props: ['data'],
|
||||||
|
setup(props) {
|
||||||
|
return () => h(Bar, { data: props.data, options: { responsive: true, plugins: { legend: { display: true }}} })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-pixel {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixel-border {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
position: relative;
|
||||||
|
border-width: 4px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow:
|
||||||
|
inset -2px -2px 0px rgba(0, 0, 0, 0.8),
|
||||||
|
inset 2px 2px 0px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,379 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
|
from fastapi import FastAPI, File, UploadFile, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Dict
|
||||||
|
import subprocess
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
import re
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from google_sheets_helper import get_sheet
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
# CORS Middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["http://localhost:5173"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
DATA_FOLDER = "D:\\lecturertask"
|
||||||
|
BASE_FILENAME = "data_scopus"
|
||||||
|
FILE_EXT = ".xlsx"
|
||||||
|
|
||||||
|
BASE_DIR = "D:/lecturertask/download_files_scraper"
|
||||||
|
|
||||||
|
folder_map = {
|
||||||
|
"cleaned_pengabdian.xlsx": "pengabdian",
|
||||||
|
"cleaned_penelitian.xlsx": "penelitian",
|
||||||
|
"cleaned_hki.xlsx": "hki",
|
||||||
|
"cleaned_scholar.xlsx": "scholar",
|
||||||
|
"cleaned_scopus.xlsx": "scopus",
|
||||||
|
}
|
||||||
|
|
||||||
|
file_map = {
|
||||||
|
"pengabdian": os.path.join(BASE_DIR, "pengabdian", "pengabdian_cleaned.xlsx"),
|
||||||
|
"penelitian": os.path.join(BASE_DIR, "penelitian", "penelitian_cleaned.xlsx"),
|
||||||
|
"hki": os.path.join(BASE_DIR, "hki", "hki_cleaned.xlsx"),
|
||||||
|
"scopus": os.path.join(BASE_DIR, "scopus", "scopus_cleaned.xlsx"),
|
||||||
|
"scholar": os.path.join(BASE_DIR, "scholar", "scholar_cleaned.xlsx"),
|
||||||
|
}
|
||||||
|
|
||||||
|
class SaveFileRequest(BaseModel):
|
||||||
|
folder: str
|
||||||
|
filename: str
|
||||||
|
data: List[Dict]
|
||||||
|
|
||||||
|
@router.post("/google-sheet/update/{category}")
|
||||||
|
def update_sheet_from_local(category: str):
|
||||||
|
path_map = {
|
||||||
|
"penelitian": "D:/lecturertask/download_files_scraper/penelitian/penelitian_cleaned.xlsx",
|
||||||
|
"pengabdian": "D:/lecturertask/download_files_scraper/pengabdian/pengabdian_cleaned.xlsx",
|
||||||
|
"hki": "D:/lecturertask/download_files_scraper/hki/hki_cleaned.xlsx",
|
||||||
|
"scopus": "D:/lecturertask/download_files_scraper/scopus/scopus_cleaned.xlsx",
|
||||||
|
"scholar": "D:/lecturertask/download_files_scraper/scholar/scholar_cleaned.xlsx",
|
||||||
|
}
|
||||||
|
|
||||||
|
spreadsheet_id = "SPREADSHEET_ID_KAMU"
|
||||||
|
worksheet_name = category
|
||||||
|
|
||||||
|
if category not in path_map:
|
||||||
|
raise HTTPException(status_code=400, detail="Kategori tidak valid")
|
||||||
|
|
||||||
|
file_path = path_map[category]
|
||||||
|
df = pd.read_excel(file_path)
|
||||||
|
|
||||||
|
worksheet = get_sheet(spreadsheet_id, worksheet_name)
|
||||||
|
worksheet.clear()
|
||||||
|
|
||||||
|
# Set header + data
|
||||||
|
worksheet.update([df.columns.values.tolist()] + df.values.tolist())
|
||||||
|
|
||||||
|
return {"message": f"Data dari {category} berhasil dikirim ke Google Sheets."}
|
||||||
|
|
||||||
|
@app.post("/save-cleaned-file")
|
||||||
|
async def save_cleaned_file(req: SaveFileRequest):
|
||||||
|
# Tentukan path file
|
||||||
|
base_path = r"D:\lecturertask\download_files_scraper"
|
||||||
|
folder_path = os.path.join(base_path, req.folder)
|
||||||
|
os.makedirs(folder_path, exist_ok=True)
|
||||||
|
file_path = os.path.join(folder_path, req.filename)
|
||||||
|
|
||||||
|
# Convert list of dict ke DataFrame
|
||||||
|
df = pd.DataFrame(req.data)
|
||||||
|
|
||||||
|
# Simpan ke Excel (overwrite)
|
||||||
|
df.to_excel(file_path, index=False)
|
||||||
|
|
||||||
|
return {"message": "File berhasil disimpan", "path": file_path}
|
||||||
|
|
||||||
|
@app.delete("/delete_all_excel")
|
||||||
|
def delete_all_excel_files():
|
||||||
|
folder_path = "D:/lecturertask/download_files_scraper"
|
||||||
|
files = glob.glob(os.path.join(folder_path, "*.xlsx"))
|
||||||
|
for file in files:
|
||||||
|
os.remove(file)
|
||||||
|
return {"message": "Semua file Excel berhasil dihapus."}
|
||||||
|
|
||||||
|
@app.get("/download/{file_type}")
|
||||||
|
def download_file(file_type: str):
|
||||||
|
if file_type in file_map:
|
||||||
|
file_path = file_map[file_type]
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
file_like = open(file_path, mode="rb")
|
||||||
|
return StreamingResponse(
|
||||||
|
file_like,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename={os.path.basename(file_path)}"}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="File not found.")
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Invalid file type.")
|
||||||
|
|
||||||
|
@app.get("/cleaned-files/{folder}/{filename}")
|
||||||
|
def get_cleaned_file(folder: str, filename: str):
|
||||||
|
file_path = os.path.join(BASE_DIR, folder, filename)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
return FileResponse(file_path, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
return {"error": "File not found"}
|
||||||
|
|
||||||
|
@app.get("/list-files/{category}")
|
||||||
|
def list_files(category: str):
|
||||||
|
folder_path = os.path.join(BASE_FOLDER, category.lower())
|
||||||
|
if not os.path.exists(folder_path):
|
||||||
|
raise HTTPException(status_code=404, detail="Folder tidak ditemukan")
|
||||||
|
|
||||||
|
files = os.listdir(folder_path)
|
||||||
|
return {"files": files}
|
||||||
|
|
||||||
|
@app.get("/download-file/{category}/{filename}")
|
||||||
|
def download_file(category: str, filename: str):
|
||||||
|
file_path = os.path.join(BASE_FOLDER, category.lower(), filename)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise HTTPException(status_code=404, detail="File tidak ditemukan")
|
||||||
|
|
||||||
|
return FileResponse(path=file_path, filename=filename, media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_file(file_pattern: str):
|
||||||
|
files = glob.glob(os.path.join(DATA_FOLDER, file_pattern))
|
||||||
|
if not files:
|
||||||
|
return None
|
||||||
|
latest_file = max(files, key=os.path.getctime)
|
||||||
|
return latest_file
|
||||||
|
|
||||||
|
# Home route
|
||||||
|
@app.get("/")
|
||||||
|
def home():
|
||||||
|
return {"message": "Welcome to Scraper penelitian API"}
|
||||||
|
|
||||||
|
def scrape_and_download(script_name: str, file_pattern: str):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["python", f"D:\\lecturertask\\scraper\\{script_name}"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return JSONResponse(content={"error": result.stderr}, status_code=500)
|
||||||
|
|
||||||
|
latest_file = get_latest_file(file_pattern)
|
||||||
|
if latest_file:
|
||||||
|
return FileResponse(latest_file, filename=os.path.basename(latest_file))
|
||||||
|
return JSONResponse(content={"error": "File hasil scraping tidak ditemukan."}, status_code=404)
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/submit-cleaned/{chart_type}")
|
||||||
|
async def submit_cleaned_file(chart_type: str, file: UploadFile = File(...)):
|
||||||
|
chart_type = chart_type.lower()
|
||||||
|
folder_map = {
|
||||||
|
"hki": "hki",
|
||||||
|
"penelitian": "penelitian",
|
||||||
|
"pengabdian": "pengabdian",
|
||||||
|
"scopus": "scopus",
|
||||||
|
"scholar": "scholar"
|
||||||
|
}
|
||||||
|
folder = folder_map.get(chart_type)
|
||||||
|
if not folder:
|
||||||
|
raise HTTPException(status_code=400, detail="Kategori tidak valid")
|
||||||
|
|
||||||
|
save_path = f"download_files_scraper/{folder}/{file.filename}"
|
||||||
|
with open(save_path, "wb") as f:
|
||||||
|
f.write(await file.read())
|
||||||
|
|
||||||
|
return {"message": f"File berhasil disimpan ke grafik {chart_type}"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload-excel")
|
||||||
|
async def upload_excel(file: UploadFile = File(...)):
|
||||||
|
try:
|
||||||
|
file_content = await file.read()
|
||||||
|
df = pd.read_excel(BytesIO(file_content))
|
||||||
|
|
||||||
|
with open("D:/lecturertask/cleaner_tokens.txt", "r") as f:
|
||||||
|
cleaner_tokens = [line.strip().lower() for line in f.readlines()]
|
||||||
|
|
||||||
|
def contains_name(row):
|
||||||
|
for token in cleaner_tokens:
|
||||||
|
pattern = r"\b" + re.escape(token) + r"\b"
|
||||||
|
if any(re.search(pattern, str(cell), re.IGNORECASE) for cell in row):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
df_cleaned = df[df.apply(contains_name, axis=1)]
|
||||||
|
|
||||||
|
existing_files = [f for f in os.listdir(DATA_FOLDER) if f.startswith("cleaned_excel_file_scraping") and f.endswith(FILE_EXT)]
|
||||||
|
max_num = 0
|
||||||
|
for filename in existing_files:
|
||||||
|
try:
|
||||||
|
num = int(filename.replace("cleaned_excel_file_scraping", "").replace(FILE_EXT, ""))
|
||||||
|
if num > max_num:
|
||||||
|
max_num = num
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
next_num = max_num + 1
|
||||||
|
new_filename = f"cleaned_excel_file_scraping{next_num}{FILE_EXT}"
|
||||||
|
save_path = os.path.join(DATA_FOLDER, new_filename)
|
||||||
|
|
||||||
|
print(f"Path penyimpanan file: {save_path}")
|
||||||
|
|
||||||
|
df_cleaned.to_excel(save_path, index=False, engine="openpyxl")
|
||||||
|
|
||||||
|
return FileResponse(save_path, media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', filename=new_filename)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_message = traceback.format_exc()
|
||||||
|
return JSONResponse(status_code=500, content={"error": error_message})
|
||||||
|
|
||||||
|
@app.get("/scrape/scholar")
|
||||||
|
def scrape_scholar():
|
||||||
|
return scrape_data("scraper_scholar.py", "Scraping Scholar selesai.")
|
||||||
|
|
||||||
|
@app.get("/scholar/download")
|
||||||
|
def download_latest_scholar():
|
||||||
|
return download_file("data_scholar*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape/scopus")
|
||||||
|
def scrape_scopus():
|
||||||
|
return scrape_data("scraper_scopus.py", "Scraping Scopus selesai.")
|
||||||
|
|
||||||
|
@app.get("/scopus/download")
|
||||||
|
def download_latest_file():
|
||||||
|
return download_file(f"{BASE_FILENAME}*{FILE_EXT}")
|
||||||
|
|
||||||
|
@app.get("/scrape/pengabdian")
|
||||||
|
def scrape_pengabdian():
|
||||||
|
return scrape_data("scraper_pengabdian.py", "Scraping Pengabdian selesai.")
|
||||||
|
|
||||||
|
@app.get("/pengabdian/download")
|
||||||
|
def download_latest_pengabdian():
|
||||||
|
return download_file("data_pengabdian*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape/hki")
|
||||||
|
def scrape_hki():
|
||||||
|
return scrape_data("scraper_HKI.py", "Scraping HKI selesai.")
|
||||||
|
|
||||||
|
@app.get("/hki/download")
|
||||||
|
def download_latest_hki():
|
||||||
|
return download_file("data_hki*.xlsx")
|
||||||
|
@app.get("/scrape/penelitian")
|
||||||
|
def scrape_penelitian():
|
||||||
|
return scrape_data("scraper_penelitian.py", "Scraping Penelitian selesai.")
|
||||||
|
|
||||||
|
@app.get("/penelitian/download")
|
||||||
|
def download_latest_penelitian():
|
||||||
|
return download_file("data_penelitian*.xlsx")
|
||||||
|
|
||||||
|
|
||||||
|
# Generic function to scrape data
|
||||||
|
def scrape_data(script_name: str, success_message: str):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["python", f"D:\\lecturertask\\scraper\\{script_name}"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return {"message": success_message}
|
||||||
|
return JSONResponse(content={"error": result.stderr}, status_code=500)
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
@app.get("/scrape-download/scopus")
|
||||||
|
def scrape_download_scopus():
|
||||||
|
return scrape_and_download("scraper_scopus.py", "data_scopus*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/scholar")
|
||||||
|
def scrape_download_scholar():
|
||||||
|
return scrape_and_download("scraper_scholar.py", "data_scholar*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/pengabdian")
|
||||||
|
def scrape_download_pengabdian():
|
||||||
|
return scrape_and_download("scraper_pengabdian.py", "data_pengabdian*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/hki")
|
||||||
|
def scrape_download_hki():
|
||||||
|
return scrape_and_download("scraper_HKI.py", "data_hki*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/penelitian")
|
||||||
|
def scrape_download_penelitian():
|
||||||
|
return scrape_and_download("scraper_penelitian.py", "data_penelitian*.xlsx")
|
||||||
|
|
||||||
|
|
||||||
|
# Generic function to download file
|
||||||
|
def download_file(file_pattern: str):
|
||||||
|
latest_file = get_latest_file(file_pattern)
|
||||||
|
if latest_file:
|
||||||
|
return FileResponse(latest_file, filename=os.path.basename(latest_file))
|
||||||
|
return JSONResponse(content={"error": "File tidak ditemukan."}, status_code=404)
|
||||||
|
|
||||||
|
# Generic function to preview file
|
||||||
|
def preview_file(file_pattern: str):
|
||||||
|
latest_file = get_latest_file(file_pattern)
|
||||||
|
if not latest_file:
|
||||||
|
return {"data": []}
|
||||||
|
df = pd.read_excel(latest_file)
|
||||||
|
return {"data": df.to_dict(orient="records")}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload-pengabdian")
|
||||||
|
async def upload_pengabdian_excel(file: UploadFile = File(...)):
|
||||||
|
contents = await file.read()
|
||||||
|
path = os.path.join(DATA_FOLDER, f"uploaded_pengabdian_{file.filename}")
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(contents)
|
||||||
|
return {"message": "File berhasil diunggah", "filename": file.filename}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload-dashboard")
|
||||||
|
def upload_dashboard_file(file: UploadFile = File(...)):
|
||||||
|
contents = file.file.read()
|
||||||
|
filename = file.filename.lower()
|
||||||
|
|
||||||
|
# Simpan file sementara
|
||||||
|
path = f"uploaded_files/{file.filename}"
|
||||||
|
os.makedirs("uploaded_files", exist_ok=True)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
# Baca data dan tentukan tipe
|
||||||
|
df = pd.read_excel(path)
|
||||||
|
detected_type = None
|
||||||
|
|
||||||
|
if "judul pengabdian" in df.columns.str.lower():
|
||||||
|
detected_type = "pengabdian"
|
||||||
|
elif "judul penelitian" in df.columns.str.lower():
|
||||||
|
detected_type = "penelitian"
|
||||||
|
elif "inventor" in df.columns.str.lower():
|
||||||
|
detected_type = "hki"
|
||||||
|
elif "source title" in df.columns.str.lower():
|
||||||
|
detected_type = "scopus"
|
||||||
|
elif "title" in df.columns.str.lower() and "citations" in df.columns.str.lower():
|
||||||
|
detected_type = "scholar"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"detected_type": detected_type,
|
||||||
|
"filename": file.filename,
|
||||||
|
"columns": df.columns.tolist(),
|
||||||
|
"rows": df.head(5).to_dict(orient="records")
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
|
|
@ -0,0 +1,321 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
|
from fastapi import FastAPI, File, UploadFile
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import subprocess
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
import re
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# CORS Middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["http://localhost:5173"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
DATA_FOLDER = "D:\\lecturertask"
|
||||||
|
BASE_FILENAME = "data_scopus"
|
||||||
|
FILE_EXT = ".xlsx"
|
||||||
|
|
||||||
|
BASE_DIR = "D:/lecturertask/download_files_scraper"
|
||||||
|
|
||||||
|
folder_map = {
|
||||||
|
"cleaned_pengabdian.xlsx": "pengabdian",
|
||||||
|
"cleaned_penelitian.xlsx": "penelitian",
|
||||||
|
"cleaned_hki.xlsx": "hki",
|
||||||
|
"cleaned_scholar.xlsx": "scholar",
|
||||||
|
"cleaned_scopus.xlsx": "scopus",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/cleaned-files/{folder}/{filename}")
|
||||||
|
def get_cleaned_file(folder: str, filename: str):
|
||||||
|
file_path = os.path.join(BASE_DIR, folder, filename)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
return FileResponse(file_path, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
return {"error": "File not found"}
|
||||||
|
|
||||||
|
@app.get("/list-files/{category}")
|
||||||
|
def list_files(category: str):
|
||||||
|
folder_path = os.path.join(BASE_FOLDER, category.lower())
|
||||||
|
if not os.path.exists(folder_path):
|
||||||
|
raise HTTPException(status_code=404, detail="Folder tidak ditemukan")
|
||||||
|
|
||||||
|
files = os.listdir(folder_path)
|
||||||
|
return {"files": files}
|
||||||
|
|
||||||
|
@app.get("/download-file/{category}/{filename}")
|
||||||
|
def download_file(category: str, filename: str):
|
||||||
|
file_path = os.path.join(BASE_FOLDER, category.lower(), filename)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise HTTPException(status_code=404, detail="File tidak ditemukan")
|
||||||
|
|
||||||
|
return FileResponse(path=file_path, filename=filename, media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_file(file_pattern: str):
|
||||||
|
files = glob.glob(os.path.join(DATA_FOLDER, file_pattern))
|
||||||
|
if not files:
|
||||||
|
return None
|
||||||
|
latest_file = max(files, key=os.path.getctime)
|
||||||
|
return latest_file
|
||||||
|
|
||||||
|
# Home route
|
||||||
|
@app.get("/")
|
||||||
|
def home():
|
||||||
|
return {"message": "Welcome to Scraper penelitian API"}
|
||||||
|
|
||||||
|
def scrape_and_download(script_name: str, file_pattern: str):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["python", f"D:\\lecturertask\\scraper\\{script_name}"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return JSONResponse(content={"error": result.stderr}, status_code=500)
|
||||||
|
|
||||||
|
latest_file = get_latest_file(file_pattern)
|
||||||
|
if latest_file:
|
||||||
|
return FileResponse(latest_file, filename=os.path.basename(latest_file))
|
||||||
|
return JSONResponse(content={"error": "File hasil scraping tidak ditemukan."}, status_code=404)
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/submit-cleaned/{chart_type}")
|
||||||
|
async def submit_cleaned_file(chart_type: str, file: UploadFile = File(...)):
|
||||||
|
folder_map = {
|
||||||
|
"HKI": "hki",
|
||||||
|
"Penelitian": "penelitian",
|
||||||
|
"Pengabdian": "pengabdian",
|
||||||
|
"Scopus": "scopus",
|
||||||
|
"Scholar": "scholar"
|
||||||
|
}
|
||||||
|
folder = folder_map.get(chart_type)
|
||||||
|
if not folder:
|
||||||
|
raise HTTPException(status_code=400, detail="Kategori tidak valid")
|
||||||
|
|
||||||
|
save_path = f"download_files_scraper/{folder}/{file.filename}"
|
||||||
|
with open(save_path, "wb") as f:
|
||||||
|
f.write(await file.read())
|
||||||
|
|
||||||
|
return {"message": f"File berhasil disimpan ke grafik {chart_type}"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload-excel")
|
||||||
|
async def upload_excel(file: UploadFile = File(...)):
|
||||||
|
try:
|
||||||
|
file_content = await file.read()
|
||||||
|
df = pd.read_excel(BytesIO(file_content))
|
||||||
|
|
||||||
|
with open("D:/lecturertask/cleaner_tokens.txt", "r") as f:
|
||||||
|
cleaner_tokens = [line.strip().lower() for line in f.readlines()]
|
||||||
|
|
||||||
|
def contains_name(row):
|
||||||
|
for token in cleaner_tokens:
|
||||||
|
pattern = r"\b" + re.escape(token) + r"\b"
|
||||||
|
if any(re.search(pattern, str(cell), re.IGNORECASE) for cell in row):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
df_cleaned = df[df.apply(contains_name, axis=1)]
|
||||||
|
|
||||||
|
existing_files = [f for f in os.listdir(DATA_FOLDER) if f.startswith("cleaned_excel_file_scraping") and f.endswith(FILE_EXT)]
|
||||||
|
max_num = 0
|
||||||
|
for filename in existing_files:
|
||||||
|
try:
|
||||||
|
num = int(filename.replace("cleaned_excel_file_scraping", "").replace(FILE_EXT, ""))
|
||||||
|
if num > max_num:
|
||||||
|
max_num = num
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
next_num = max_num + 1
|
||||||
|
new_filename = f"cleaned_excel_file_scraping{next_num}{FILE_EXT}"
|
||||||
|
save_path = os.path.join(DATA_FOLDER, new_filename)
|
||||||
|
|
||||||
|
print(f"Path penyimpanan file: {save_path}")
|
||||||
|
|
||||||
|
df_cleaned.to_excel(save_path, index=False, engine="openpyxl")
|
||||||
|
|
||||||
|
return FileResponse(save_path, media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', filename=new_filename)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_message = traceback.format_exc()
|
||||||
|
return JSONResponse(status_code=500, content={"error": error_message})
|
||||||
|
|
||||||
|
@app.get("/scrape/scholar")
|
||||||
|
def scrape_scholar():
|
||||||
|
return scrape_data("scraper_scholar.py", "Scraping Scholar selesai.")
|
||||||
|
|
||||||
|
@app.get("/scholar/download")
|
||||||
|
def download_latest_scholar():
|
||||||
|
return download_file("data_scholar*.xlsx")
|
||||||
|
|
||||||
|
# Scopus Scraping Endpoints
|
||||||
|
@app.get("/scrape/scopus")
|
||||||
|
def scrape_scopus():
|
||||||
|
return scrape_data("scraper_scopus.py", "Scraping Scopus selesai.")
|
||||||
|
|
||||||
|
# @app.get("/scopus/latest-file")
|
||||||
|
# def latest_scopus_file():
|
||||||
|
# latest_file = get_latest_file(f"{BASE_FILENAME}*{FILE_EXT}")
|
||||||
|
# if latest_file:
|
||||||
|
# return {"file": os.path.basename(latest_file)}
|
||||||
|
# return {"file": None}
|
||||||
|
|
||||||
|
@app.get("/scopus/download")
|
||||||
|
def download_latest_file():
|
||||||
|
return download_file(f"{BASE_FILENAME}*{FILE_EXT}")
|
||||||
|
|
||||||
|
# @app.get("/scopus/preview")
|
||||||
|
# def preview_latest_file():
|
||||||
|
# return preview_file(f"{BASE_FILENAME}*{FILE_EXT}")
|
||||||
|
|
||||||
|
# Pengabdian Scraping Endpoints
|
||||||
|
@app.get("/scrape/pengabdian")
|
||||||
|
def scrape_pengabdian():
|
||||||
|
return scrape_data("scraper_pengabdian.py", "Scraping Pengabdian selesai.")
|
||||||
|
|
||||||
|
# @app.get("/pengabdian/latest-file")
|
||||||
|
# def latest_pengabdian_file():
|
||||||
|
# latest_file = get_latest_file("data_pengabdian*.xlsx")
|
||||||
|
# if latest_file:
|
||||||
|
# return {"file": os.path.basename(latest_file)}
|
||||||
|
# return {"file": None}
|
||||||
|
|
||||||
|
@app.get("/pengabdian/download")
|
||||||
|
def download_latest_pengabdian():
|
||||||
|
return download_file("data_pengabdian*.xlsx")
|
||||||
|
|
||||||
|
# @app.get("/pengabdian/preview")
|
||||||
|
# def preview_latest_pengabdian():
|
||||||
|
# return preview_file("data_pengabdian*.xlsx")
|
||||||
|
|
||||||
|
# HKI Scraping Endpoints
|
||||||
|
@app.get("/scrape/hki")
|
||||||
|
def scrape_hki():
|
||||||
|
return scrape_data("scraper_HKI.py", "Scraping HKI selesai.")
|
||||||
|
|
||||||
|
@app.get("/hki/download")
|
||||||
|
def download_latest_hki():
|
||||||
|
return download_file("data_hki*.xlsx")
|
||||||
|
|
||||||
|
|
||||||
|
# Penelitian Scraping Endpoints
|
||||||
|
@app.get("/scrape/penelitian")
|
||||||
|
def scrape_penelitian():
|
||||||
|
return scrape_data("scraper_penelitian.py", "Scraping Penelitian selesai.")
|
||||||
|
|
||||||
|
@app.get("/penelitian/download")
|
||||||
|
def download_latest_penelitian():
|
||||||
|
return download_file("data_penelitian*.xlsx")
|
||||||
|
|
||||||
|
|
||||||
|
# Generic function to scrape data
|
||||||
|
def scrape_data(script_name: str, success_message: str):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["python", f"D:\\lecturertask\\scraper\\{script_name}"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return {"message": success_message}
|
||||||
|
return JSONResponse(content={"error": result.stderr}, status_code=500)
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
@app.get("/scrape-download/scopus")
|
||||||
|
def scrape_download_scopus():
|
||||||
|
return scrape_and_download("scraper_scopus.py", "data_scopus*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/scholar")
|
||||||
|
def scrape_download_scholar():
|
||||||
|
return scrape_and_download("scraper_scholar.py", "data_scholar*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/pengabdian")
|
||||||
|
def scrape_download_pengabdian():
|
||||||
|
return scrape_and_download("scraper_pengabdian.py", "data_pengabdian*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/hki")
|
||||||
|
def scrape_download_hki():
|
||||||
|
return scrape_and_download("scraper_HKI.py", "data_hki*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/penelitian")
|
||||||
|
def scrape_download_penelitian():
|
||||||
|
return scrape_and_download("scraper_penelitian.py", "data_penelitian*.xlsx")
|
||||||
|
|
||||||
|
|
||||||
|
# Generic function to download file
|
||||||
|
def download_file(file_pattern: str):
|
||||||
|
latest_file = get_latest_file(file_pattern)
|
||||||
|
if latest_file:
|
||||||
|
return FileResponse(latest_file, filename=os.path.basename(latest_file))
|
||||||
|
return JSONResponse(content={"error": "File tidak ditemukan."}, status_code=404)
|
||||||
|
|
||||||
|
# Generic function to preview file
|
||||||
|
def preview_file(file_pattern: str):
|
||||||
|
latest_file = get_latest_file(file_pattern)
|
||||||
|
if not latest_file:
|
||||||
|
return {"data": []}
|
||||||
|
df = pd.read_excel(latest_file)
|
||||||
|
return {"data": df.to_dict(orient="records")}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload-pengabdian")
|
||||||
|
async def upload_pengabdian_excel(file: UploadFile = File(...)):
|
||||||
|
contents = await file.read()
|
||||||
|
path = os.path.join(DATA_FOLDER, f"uploaded_pengabdian_{file.filename}")
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(contents)
|
||||||
|
return {"message": "File berhasil diunggah", "filename": file.filename}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload-dashboard")
|
||||||
|
def upload_dashboard_file(file: UploadFile = File(...)):
|
||||||
|
contents = file.file.read()
|
||||||
|
filename = file.filename.lower()
|
||||||
|
|
||||||
|
# Simpan file sementara
|
||||||
|
path = f"uploaded_files/{file.filename}"
|
||||||
|
os.makedirs("uploaded_files", exist_ok=True)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
# Baca data dan tentukan tipe
|
||||||
|
df = pd.read_excel(path)
|
||||||
|
detected_type = None
|
||||||
|
|
||||||
|
if "judul pengabdian" in df.columns.str.lower():
|
||||||
|
detected_type = "pengabdian"
|
||||||
|
elif "judul penelitian" in df.columns.str.lower():
|
||||||
|
detected_type = "penelitian"
|
||||||
|
elif "inventor" in df.columns.str.lower():
|
||||||
|
detected_type = "hki"
|
||||||
|
elif "source title" in df.columns.str.lower():
|
||||||
|
detected_type = "scopus"
|
||||||
|
elif "title" in df.columns.str.lower() and "citations" in df.columns.str.lower():
|
||||||
|
detected_type = "scholar"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"detected_type": detected_type,
|
||||||
|
"filename": file.filename,
|
||||||
|
"columns": df.columns.tolist(),
|
||||||
|
"rows": df.head(5).to_dict(orient="records")
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
|
After Width: | Height: | Size: 2.3 MiB |
After Width: | Height: | Size: 942 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 5.0 MiB |
|
@ -0,0 +1,103 @@
|
||||||
|
afis
|
||||||
|
afriansyah
|
||||||
|
agus
|
||||||
|
agustianto
|
||||||
|
ahmad
|
||||||
|
aji
|
||||||
|
akas
|
||||||
|
albaab
|
||||||
|
angga
|
||||||
|
antika
|
||||||
|
arief
|
||||||
|
arifianto
|
||||||
|
arifin
|
||||||
|
arvita
|
||||||
|
asryullah
|
||||||
|
atmadji
|
||||||
|
ayuninghemi
|
||||||
|
bagus
|
||||||
|
bekti
|
||||||
|
bety
|
||||||
|
bitari
|
||||||
|
choirul
|
||||||
|
choirunnisa
|
||||||
|
dedes
|
||||||
|
denny
|
||||||
|
destarianto
|
||||||
|
dewanto
|
||||||
|
dewi
|
||||||
|
dia
|
||||||
|
dian
|
||||||
|
didit
|
||||||
|
dwi
|
||||||
|
elly
|
||||||
|
ely
|
||||||
|
ery
|
||||||
|
etikasari
|
||||||
|
fahriyannur
|
||||||
|
faisal
|
||||||
|
fatimatuzzahra
|
||||||
|
gede
|
||||||
|
gumilang
|
||||||
|
hariyanto
|
||||||
|
hariyono
|
||||||
|
hartadi
|
||||||
|
hendra
|
||||||
|
hermawan
|
||||||
|
huda
|
||||||
|
husin
|
||||||
|
ika
|
||||||
|
jullev
|
||||||
|
khafidurrohman
|
||||||
|
khen
|
||||||
|
kurnia
|
||||||
|
kurniasari
|
||||||
|
lukie
|
||||||
|
lutfi
|
||||||
|
maryuni
|
||||||
|
mei
|
||||||
|
mochammad
|
||||||
|
mukhamad
|
||||||
|
mulyadi
|
||||||
|
munih
|
||||||
|
perdanasari
|
||||||
|
phoa
|
||||||
|
pradana
|
||||||
|
pramuditha
|
||||||
|
pratama
|
||||||
|
prawidya
|
||||||
|
puspitasari
|
||||||
|
putra
|
||||||
|
putranto
|
||||||
|
putro
|
||||||
|
rahmat
|
||||||
|
rakhmad
|
||||||
|
ratih
|
||||||
|
reza
|
||||||
|
rifki
|
||||||
|
riskiawan
|
||||||
|
rizaldi
|
||||||
|
rosyady
|
||||||
|
sarwo
|
||||||
|
setiawan
|
||||||
|
setiyawan
|
||||||
|
seto
|
||||||
|
setyohadi
|
||||||
|
shabrina
|
||||||
|
shinta
|
||||||
|
surateno
|
||||||
|
susanto
|
||||||
|
syamsul
|
||||||
|
taufiq
|
||||||
|
trias
|
||||||
|
trismayanti
|
||||||
|
ulil
|
||||||
|
utomo
|
||||||
|
victor
|
||||||
|
wahyu
|
||||||
|
widianta
|
||||||
|
widiastuti
|
||||||
|
wijanarko
|
||||||
|
wiryawan
|
||||||
|
yuana
|
||||||
|
yufit
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "crud-lecturo",
|
||||||
|
"private_key_id": "ea5aa9e10a97ed76b2e4adc449f9016c95fde558",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvu7tlqXiC+Li4\n8R5MGahlc5szalNi6T6PKbM2sRgoLTXt4lEMmpXW+06FaXwE4tK6B/7bafgC9gJ3\neH5/I4t/RW2u+dFnioTYZpWxKBoo57y7drqXYnAqKt65mz36/Zo228DtPomsftUI\nt65+ZCCyKl+5kz4TdZYAK3L6cjhWsI7AGOXK4n0DMFU1W7BkflL4VeUEoBMFQ3C5\nb1YEcAEwhiH4iswBOfG9oXan/PDKvM7chB42iA1w1FPhQsYqJg5qG2vc6sFABwRU\np4skgywR8ICMx9KruGiJcjoBylOLhQZhlNUZO7gMOVUQ09mkkpYkciB5EniVX4Vb\nMtt1LBV5AgMBAAECggEABMrkXHqokcvUaoGsrZnLbBX9V+5tNFwqPXap7BK5D+eo\n2gzyUjCJLxhF8aEa6SvB4ExuW14EY0QQAzbsuq0wZxgUGqq2bCPFpadNAL4miu7E\nFu1GW/Fjyoop0eITtwKzRyqO6aQdD1s3lmR4O5eThXlCT6vIvjFkq0cEEdzZhxk4\ncIYAATFA6hi7tBkuJ5k3IeaqBWmUabEaN8daZaKlveamnPpdczkAbaEfuNK1oqJj\nEvm9MMogbCy8TEIcf+bRFtQKD4tcM/nXs42jR+WtgMns2zLxNq6X4V+GXe/bJW1f\nJzMN5qep7iocjzztcJFhpD+L+1fGd7yrNfYz5gKpLQKBgQDqjgWRH0sH0ESkcwrT\ntFRonpVbxtZMzpiwfyqX3pm3a6lK6FVdMhIFwRd4UR4PSSXMeFOm5n2idmfDXkX1\nqIgJJCLQAzpS6yRmI2+f2n5tj/gIYgW9eOaQ+WmqDvpijb+OreK/OCGOo9P+HTiu\n63QUvV2ptweqocVr/wvZbXnfnQKBgQC/zPAw5vrr3lUuj6Lev7sjzlAM106QUsEX\nl66BsotoPBhY3XfBeTo30TQquZ+M39BN4/myF/CO1KbZG4Tna/CcfXF+L27b2SZZ\ncptt/t8JYBISQVysxK+NhjYz/6/pvcmmTEevbBqZYF5sqTDY28h7/S8xCBPtSCpD\nD8pC2RXcjQKBgB5Pbg674YFH+6bYqvyCoCnI3Ho4rrbMN9UPCd0ISgr24bCpZ2ac\nstGFi1fj+6N0C2tp3T2DKZcWAACyLQ460iGERu9ki2PtuQ1t5N3eaVoVMbM7n4xF\nlF4FrQ7p0pdrw+ZXOCcHxOZe62U6N3n7OUv40KK+4UG+l/mFwu09BXPNAoGACG25\nIG0GGddrZuBpB+DlGG24ltffW/hHBAJmaMyv06TQbRdOa/In3MwUvsvpdwde4A1k\nq67ho2U079WFwaW8rSPWGPV8qayhQs0Gh47rvj26jZLRv8Xk8RM0zpQt5tewRN6V\nR+A4SwUxIRVOUDlYVhqKOF1igsrpEIlhGg7wJwECgYEA2xlH4Bcaa/HeOW03qxff\nB4T5cZ8uVC3SAK/jUFkoGif9DcGd6YrvC2wSBQAckEKQGGEHpAThWNIE8kvYl/dG\nafDOZcc5n6XQcMIpr3lE5MZbIofQwsfRFmOUKyPuTq2rcI3RVwfrDahuHdcfJupt\num1OhEROqd2adpbPwuzTta4=\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "python-api-lecturo@crud-lecturo.iam.gserviceaccount.com",
|
||||||
|
"client_id": "117109659614344922671",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/python-api-lecturo%40crud-lecturo.iam.gserviceaccount.com",
|
||||||
|
"universe_domain": "googleapis.com"
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
https://scholar.google.com/citations?hl=id&user=syVa7R8AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=-jgD1c4AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=KckJULoAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=GZJNO6IAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=jE_5vN0AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=Q8IYj98AAAAJ
|
||||||
|
https://scholar.google.com/citations?user=P4JAcewAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=TqLEbF8AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=Da5GCfEAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=PB28zZoAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=-i3IY0UAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=lwpP6o0AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=KNWFXYIAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=kRvwDyIAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=anx47hMAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=pyvbaBwAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=Ee-sjb4AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=LXr3VFMAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=jaO51N8AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=80NQTI8AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=3uvRWKEAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=jyeop8MAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=LKN2VWUAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=ojOfuZ0AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=Wp45VDgAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=aBEA4lcAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=hYQK0I8AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=iBfZZc4AAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=Yn7_99QAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=kA3H6iwAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=HW6RZloAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=zIZeddIAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=wUdKcnwAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=a9IR-sgAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=Takkq9IAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=nz1O0lIAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=MlqTaXcAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=a_KipnEAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=MKFkTIgAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=H63iZREAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=dlGopSkAAAAJ
|
||||||
|
https://scholar.google.com/citations?hl=id&user=2SEuI6gAAAAJ
|
|
@ -0,0 +1,9 @@
|
||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
|
@ -0,0 +1 @@
|
||||||
|
* text=auto eol=lf
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
# frontend
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
|
@ -0,0 +1,19 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{js,mjs,jsx,vue}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'app/files-to-ignore',
|
||||||
|
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
|
||||||
|
},
|
||||||
|
|
||||||
|
js.configs.recommended,
|
||||||
|
...pluginVue.configs['flat/essential'],
|
||||||
|
skipFormatting,
|
||||||
|
]
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vite App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --fix",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"apexcharts": "^4.7.0",
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
|
"lucide-vue-next": "^0.477.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-chartjs": "^5.3.2",
|
||||||
|
"vue-echarts": "^7.0.3",
|
||||||
|
"vue-router": "^4.5.0",
|
||||||
|
"vue3-apexcharts": "^1.8.0",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.20.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.20.1",
|
||||||
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"prettier": "^3.5.1",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"vite": "^6.1.0",
|
||||||
|
"vite-plugin-vue-devtools": "^7.7.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
After Width: | Height: | Size: 2.3 MiB |
After Width: | Height: | Size: 942 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 5.0 MiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
|
@ -0,0 +1,87 @@
|
||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
'Fira Sans',
|
||||||
|
'Droid Sans',
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 276 B |
|
@ -0,0 +1,59 @@
|
||||||
|
@import './base.css';
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.green {
|
||||||
|
text-decoration: none;
|
||||||
|
color: hsla(160, 100%, 37%, 1);
|
||||||
|
transition: 0.4s;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
a:hover {
|
||||||
|
background-color: hsla(160, 100%, 37%, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: 10px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
|
@ -0,0 +1,17 @@
|
||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex justify-center items-center">
|
||||||
|
<div class="bg-gray-800 border-2 border-gray-600 rounded-lg w-11/12 lg:w-4/5 p-6 relative">
|
||||||
|
<button @click="$emit('close')" class="absolute top-2 right-2 text-white text-lg">×</button>
|
||||||
|
<div class="mb-4">
|
||||||
|
<slot name="header" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-4 max-h-[60vh] overflow-y-auto">
|
||||||
|
<slot name="body" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-2">
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import './assets/main.css'
|
||||||
|
import './assets/tailwind.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
axios.defaults.baseURL = 'http://localhost:8000'
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import LoginView from "@/views/LoginView.vue";
|
||||||
|
import DashboardView from '@/views/DashboardView.vue';
|
||||||
|
import ScraperView from '@/views/ScraperView.vue';
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: LoginView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
name: 'dashboard',
|
||||||
|
component: DashboardView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/scraper',
|
||||||
|
name: 'scraper',
|
||||||
|
component: ScraperView,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
|
@ -0,0 +1,670 @@
|
||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 w-full h-full flex bg-update-bg bg-cover bg-center">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="absolute left-5 top-5 h-1/4 bg-gray-700 bg-opacity-90 border-r-4 border-gray-500 pixel-border shadow-lg w-16 flex flex-col items-center p-4">
|
||||||
|
<nav class="flex flex-col space-y-6 text-center mt-4">
|
||||||
|
<a href="/dashboard" class="text-white hover:text-yellow-400 transition">
|
||||||
|
<LayoutDashboard class="w-8 h-8" />
|
||||||
|
</a>
|
||||||
|
<a href="/scraper" class="text-white hover:text-blue-400 transition">
|
||||||
|
<Bot class="w-8 h-8" />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="flex-1 ml-20 overflow-y-auto p-4">
|
||||||
|
<Modal v-if="isModalOpen" @close="isModalOpen = false">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-xl font-bold text-white">Edit Data {{ modalTitle }}</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full text-sm text-white table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="(header, index) in tableHeaders" :key="index" class="px-4 py-2 border-b border-gray-500">{{ header }}</th>
|
||||||
|
<th class="px-4 py-2 border-b border-gray-500">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(row, rowIndex) in editableData" :key="rowIndex">
|
||||||
|
<td v-for="(header, colIndex) in tableHeaders" :key="colIndex" class="px-4 py-2 border-b border-gray-600">
|
||||||
|
<input v-model="editableData[rowIndex][header]" class="bg-gray-700 text-white px-2 py-1 w-full rounded" />
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 border-b border-gray-600 text-center">
|
||||||
|
<button @click="removeRow(rowIndex)" class="text-red-500 hover:underline">Hapus</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button @click="addRow" class="mt-4 bg-green-500 hover:bg-green-400 text-white px-4 py-2 rounded">Tambah Baris</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button @click="saveChanges" class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded">Simpan Perubahan</button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
<div class="w-full bg-gray-700 bg-opacity-90 border-4 border-gray-500 pixel-border flex flex-col items-center p-6">
|
||||||
|
<h2 class="text-white text-xl font-bold mb-6">DASHBOARD</h2>
|
||||||
|
<div class="flex items-center gap-4 mb-10">
|
||||||
|
<button @click="refreshData" class="text-white bg-green-600 hover:bg-green-500 px-4 py-2 rounded-2xl">
|
||||||
|
Refresh Grafik
|
||||||
|
</button>
|
||||||
|
<button @click="clearCache" class="text-white bg-red-600 hover:bg-red-500 px-4 py-2 rounded-2xl">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
|
||||||
|
|
||||||
|
<!-- Pengabdian -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Pengabdian</h3>
|
||||||
|
<div class="relative flex justify-end gap-2">
|
||||||
|
<a
|
||||||
|
:href="`http://localhost:8000/download/pengabdian`"
|
||||||
|
class="text-xs bg-green-600 hover:bg-green-500 text-white px-2 py-1 rounded"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Excel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="refreshSingleData('pengabdian')"
|
||||||
|
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
title="Reload Grafik"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal('pengabdian')" class="text-sm text-yellow-400 hover:underline mb-10">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="pengabdian.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="pengabdianChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HKI -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">HKI</h3>
|
||||||
|
<div class="relative flex justify-end gap-2">
|
||||||
|
<a
|
||||||
|
:href="`http://localhost:8000/download/hki`"
|
||||||
|
class="text-xs bg-green-600 hover:bg-green-500 text-white px-2 py-1 rounded"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Excel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="refreshSingleData('hki')"
|
||||||
|
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
title="Reload Grafik"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal('hki')" class="text-sm text-yellow-400 hover:underline mb-10">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropHKI"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="hki.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="hkiChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Penelitian -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Penelitian</h3>
|
||||||
|
<div class="relative flex justify-end gap-2">
|
||||||
|
<a
|
||||||
|
:href="`http://localhost:8000/download/penelitian`"
|
||||||
|
class="text-xs bg-green-600 hover:bg-green-500 text-white px-2 py-1 rounded"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Excel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="refreshSingleData('penelitian')"
|
||||||
|
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
title="Reload Grafik"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal('penelitian')" class="text-sm text-yellow-400 hover:underline mb-10">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropPenelitian"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="penelitian.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="penelitianChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scopus -->
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between mt-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Scopus</h3>
|
||||||
|
<div class="relative flex justify-end gap-2">
|
||||||
|
<a
|
||||||
|
:href="`http://localhost:8000/download/scopus`"
|
||||||
|
class="text-xs bg-green-600 hover:bg-green-500 text-white px-2 py-1 rounded"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Excel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="refreshSingleData('scopus')"
|
||||||
|
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
title="Reload Grafik"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal('scopus')" class="text-sm text-yellow-400 hover:underline mb-10">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropScopus"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="scopus.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="scopusChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between mt-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Scholar</h3>
|
||||||
|
<div class="relative flex justify-end gap-2">
|
||||||
|
<a
|
||||||
|
:href="`http://localhost:8000/download/scholar`"
|
||||||
|
class="text-xs bg-green-600 hover:bg-green-500 text-white px-2 py-1 rounded"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Excel
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
@click="refreshSingleData('scholar')"
|
||||||
|
class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded"
|
||||||
|
title="Reload Grafik"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="openModal('scholar')" class="text-sm text-yellow-400 hover:underline mb-10">Edit</button>
|
||||||
|
<div
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDropScholar"
|
||||||
|
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="scholar.length > 0" class="mt-4">
|
||||||
|
<BarChart :data="scholarChartData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-[500px] col-span-1 md:col-span-2 mt-10">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-lg font-semibold">Kontribusi Luaran Publikasi Dosen per Tahun</h3>
|
||||||
|
<select v-model="selectedYear" class="bg-gray-700 text-white p-2 rounded">
|
||||||
|
<option v-for="year in availableYears" :key="year" :value="year">{{ year }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="w-80 h-80 mx-auto">
|
||||||
|
<canvas id="luaranPieChart" class="w-80 h-80" width="320" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-700 bg-opacity-90 border-4 border-gray-500 pixel-border flex flex-col items-center p-6">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Modal from '@/components/Modal.vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { LayoutDashboard, Bot, Settings, LogOut } from "lucide-vue-next"
|
||||||
|
import { computed, defineComponent, h } from 'vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as XLSX from 'xlsx'
|
||||||
|
import { Bar } from 'vue-chartjs'
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
Title, Tooltip, Legend,
|
||||||
|
BarElement, CategoryScale, LinearScale
|
||||||
|
} from 'chart.js'
|
||||||
|
import { onMounted, watch} from 'vue'
|
||||||
|
import Chart from 'chart.js/auto';
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCleanedFile('pengabdian', 'pengabdian_cleaned.xlsx', pengabdian)
|
||||||
|
fetchCleanedFile('penelitian', 'penelitian_cleaned.xlsx', penelitian)
|
||||||
|
fetchCleanedFile('hki', 'hki_cleaned.xlsx', hki)
|
||||||
|
fetchCleanedFile('scholar', 'scholar_cleaned.xlsx', scholar)
|
||||||
|
fetchCleanedFile('scopus', 'scopus_cleaned.xlsx', scopus)
|
||||||
|
getYearsFromData();
|
||||||
|
renderPieChart();
|
||||||
|
})
|
||||||
|
|
||||||
|
function refreshData() {
|
||||||
|
fetchCleanedFile('pengabdian', 'pengabdian_cleaned.xlsx', pengabdian)
|
||||||
|
fetchCleanedFile('penelitian', 'penelitian_cleaned.xlsx', penelitian)
|
||||||
|
fetchCleanedFile('hki', 'hki_cleaned.xlsx', hki)
|
||||||
|
fetchCleanedFile('scholar', 'scholar_cleaned.xlsx', scholar)
|
||||||
|
fetchCleanedFile('scopus', 'scopus_cleaned.xlsx', scopus)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||||
|
|
||||||
|
const pengabdian = ref([])
|
||||||
|
const hki = ref([])
|
||||||
|
const penelitian = ref([])
|
||||||
|
const scopus = ref([])
|
||||||
|
const scholar = ref([])
|
||||||
|
const isModalOpen = ref(false)
|
||||||
|
const modalTitle = ref('')
|
||||||
|
const editableData = ref([])
|
||||||
|
const tableHeaders = ref([])
|
||||||
|
const currentTargetArray = ref(null)
|
||||||
|
|
||||||
|
const selectedYear = ref(new Date().getFullYear());
|
||||||
|
const availableYears = ref([]);
|
||||||
|
let pieChartInstance = null;
|
||||||
|
|
||||||
|
function clearCache() {
|
||||||
|
pengabdian.value = []
|
||||||
|
hki.value = []
|
||||||
|
penelitian.value = []
|
||||||
|
scopus.value = []
|
||||||
|
scholar.value = []
|
||||||
|
|
||||||
|
editableData.value = []
|
||||||
|
tableHeaders.value = []
|
||||||
|
currentTargetArray.value = null
|
||||||
|
|
||||||
|
selectedYear.value = null
|
||||||
|
|
||||||
|
// Clear grafik, misal render ulang grafik kosong
|
||||||
|
renderPieChart()
|
||||||
|
|
||||||
|
// Jika kamu punya fungsi render grafik bar, panggil juga refresh grafik kosongnya
|
||||||
|
// Misal:
|
||||||
|
// pengabdianChartData.value = {}
|
||||||
|
// dan update chart (tergantung cara implementasi chartmu)
|
||||||
|
console.log("Cache data cleared")
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSingleData(type) {
|
||||||
|
switch(type) {
|
||||||
|
case 'pengabdian':
|
||||||
|
fetchCleanedFile('pengabdian', 'pengabdian_cleaned.xlsx', pengabdian);
|
||||||
|
break;
|
||||||
|
case 'hki':
|
||||||
|
fetchCleanedFile('hki', 'hki_cleaned.xlsx', hki);
|
||||||
|
break;
|
||||||
|
case 'penelitian':
|
||||||
|
fetchCleanedFile('penelitian', 'penelitian_cleaned.xlsx', penelitian);
|
||||||
|
break;
|
||||||
|
case 'scopus':
|
||||||
|
fetchCleanedFile('scopus', 'scopus_cleaned.xlsx', scopus);
|
||||||
|
break;
|
||||||
|
case 'scholar':
|
||||||
|
fetchCleanedFile('scholar', 'scholar_cleaned.xlsx', scholar);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getYearsFromData() {
|
||||||
|
const allYears = new Set();
|
||||||
|
[...hki.value, ...pengabdian.value, ...penelitian.value].forEach(item => {
|
||||||
|
const year = item?.Tahun || item?.tahun;
|
||||||
|
if (year) allYears.add(Number(year));
|
||||||
|
});
|
||||||
|
availableYears.value = Array.from(allYears).sort((a, b) => b - a);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countDataByYear(dataset, year) {
|
||||||
|
return dataset.filter(item => {
|
||||||
|
const y = item?.Tahun || item?.tahun;
|
||||||
|
return String(y) === String(year);
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPieChart() {
|
||||||
|
const year = selectedYear.value;
|
||||||
|
|
||||||
|
const hkiCount = countDataByYear(hki.value, year);
|
||||||
|
const pengabdianCount = countDataByYear(pengabdian.value, year);
|
||||||
|
const penelitianCount = countDataByYear(penelitian.value, year);
|
||||||
|
|
||||||
|
const ctx = document.getElementById('luaranPieChart').getContext('2d');
|
||||||
|
if (pieChartInstance) {
|
||||||
|
pieChartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
pieChartInstance = new Chart(ctx, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: ['HKI', 'Pengabdian', 'Penelitian'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Luaran',
|
||||||
|
data: [hkiCount, pengabdianCount, penelitianCount],
|
||||||
|
backgroundColor: ['#38bdf8', '#facc15', '#34d399'],
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: {
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([hki, pengabdian, penelitian], () => {
|
||||||
|
getYearsFromData();
|
||||||
|
renderPieChart();
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
watch(selectedYear, () => {
|
||||||
|
renderPieChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
function openModal(type) {
|
||||||
|
modalTitle.value = type.charAt(0).toUpperCase() + type.slice(1)
|
||||||
|
isModalOpen.value = true
|
||||||
|
|
||||||
|
let sourceData
|
||||||
|
if (type === 'pengabdian') {
|
||||||
|
sourceData = pengabdian.value
|
||||||
|
currentTargetArray.value = pengabdian
|
||||||
|
} else if (type === 'hki') {
|
||||||
|
sourceData = hki.value
|
||||||
|
currentTargetArray.value = hki
|
||||||
|
} else if (type === 'penelitian') {
|
||||||
|
sourceData = penelitian.value
|
||||||
|
currentTargetArray.value = penelitian
|
||||||
|
} else if (type === 'scopus') {
|
||||||
|
sourceData = scopus.value
|
||||||
|
currentTargetArray.value = scopus
|
||||||
|
} else if (type === 'scholar') {
|
||||||
|
sourceData = scholar.value
|
||||||
|
currentTargetArray.value = scholar
|
||||||
|
}
|
||||||
|
|
||||||
|
editableData.value = JSON.parse(JSON.stringify(sourceData))
|
||||||
|
tableHeaders.value = Object.keys(sourceData[0] || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRow() {
|
||||||
|
const newRow = {}
|
||||||
|
tableHeaders.value.forEach(h => newRow[h] = '')
|
||||||
|
editableData.value.push(newRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRow(index) {
|
||||||
|
editableData.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges() {
|
||||||
|
currentTargetArray.value.value = JSON.parse(JSON.stringify(editableData.value));
|
||||||
|
isModalOpen.value = false;
|
||||||
|
|
||||||
|
const folder = modalTitle.value.toLowerCase();
|
||||||
|
const filename = modalTitle.value + '_cleaned.xlsx';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post('http://localhost:8000/save-cleaned-file', {
|
||||||
|
folder,
|
||||||
|
filename,
|
||||||
|
data: editableData.value
|
||||||
|
});
|
||||||
|
alert('File berhasil disimpan dan di-overwrite di server!');
|
||||||
|
// Reload data dari server supaya update di frontend
|
||||||
|
refreshData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gagal menyimpan file:', error);
|
||||||
|
alert('Gagal menyimpan file. Cek console untuk detail.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function fetchCleanedFile(folder, filename, targetArray) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`http://localhost:8000/cleaned-files/${folder}/${filename}`, {
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = new Uint8Array(response.data)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
|
||||||
|
targetArray.value = json
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Gagal mengambil file cleaned (${folder}/${filename}):`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleDrop(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
pengabdian.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDropScholar(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
scholar.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scholarChartData = computed(() => {
|
||||||
|
const citationsByYear = {}
|
||||||
|
for (const item of scholar.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
const citation = parseInt(item['Total Kutipan']) || 0
|
||||||
|
citationsByYear[year] = (citationsByYear[year] || 0) + citation
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(citationsByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Total Kutipan Scholar per Tahun',
|
||||||
|
backgroundColor: '#f472b6', // pink
|
||||||
|
data: Object.values(citationsByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const pengabdianChartData = computed(() => {
|
||||||
|
const countByYear = {}
|
||||||
|
for (const item of pengabdian.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
countByYear[year] = (countByYear[year] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah Pengabdian per Tahun',
|
||||||
|
backgroundColor: '#facc15',
|
||||||
|
data: Object.values(countByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleDropHKI(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
hki.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hkiChartData = computed(() => {
|
||||||
|
const countByYear = {}
|
||||||
|
for (const item of hki.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
countByYear[year] = (countByYear[year] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah HKI per Tahun',
|
||||||
|
backgroundColor: '#38bdf8',
|
||||||
|
data: Object.values(countByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
function handleDropPenelitian(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
penelitian.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const penelitianChartData = computed(() => {
|
||||||
|
const countByYear = {}
|
||||||
|
for (const item of penelitian.value) {
|
||||||
|
const year = item.Tahun || 'Unknown'
|
||||||
|
countByYear[year] = (countByYear[year] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByYear),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah Penelitian per Tahun',
|
||||||
|
backgroundColor: '#34d399', // hijau
|
||||||
|
data: Object.values(countByYear),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
function handleDropScopus(e) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
const data = new Uint8Array(evt.target.result)
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' })
|
||||||
|
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||||
|
const json = XLSX.utils.sheet_to_json(sheet)
|
||||||
|
scopus.value = json
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopusChartData = computed(() => {
|
||||||
|
const countByQuartile = {}
|
||||||
|
for (const item of scopus.value) {
|
||||||
|
const quartile = item.Quartile || 'Unknown'
|
||||||
|
countByQuartile[quartile] = (countByQuartile[quartile] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: Object.keys(countByQuartile),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Jumlah Publikasi Scopus per Quartile',
|
||||||
|
backgroundColor: '#a78bfa', // ungu
|
||||||
|
data: Object.values(countByQuartile),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const BarChart = defineComponent({
|
||||||
|
name: 'BarChart',
|
||||||
|
props: ['data'],
|
||||||
|
setup(props) {
|
||||||
|
return () => h(Bar, { data: props.data, options: { responsive: true, plugins: { legend: { display: true }}} })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-pixel {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixel-border {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
position: relative;
|
||||||
|
border-width: 4px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow:
|
||||||
|
inset -2px -2px 0px rgba(0, 0, 0, 0.8),
|
||||||
|
inset 2px 2px 0px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 w-full h-full flex items-center justify-center bg-login-bg bg-cover bg-center overflow-hidden">
|
||||||
|
<div class="w-full max-w-md h-auto bg-gray-700 bg-opacity-90 border-4 border-gray-500 pixel-border flex flex-col items-center justify-start relative p-6">
|
||||||
|
<div class="w-3/5 h-1/6 bg-gray-500 bg-opacity-90 border-4 border-gray-400 pixel-border shadow-lg text-white flex items-center justify-center absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
|
<h2 class="text-center text-xl font-bold font-pixel text-[0.8rem] leading-none p-3">{{ isRegister ? 'Register' : 'Login' }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form v-if="!isRegister" class="w-full flex flex-col items-center mt-12" @submit.prevent="handleLogin">
|
||||||
|
<div class="w-3/4 mb-3">
|
||||||
|
<label for="username" class="block text-white font-pixel text-xs">Username</label>
|
||||||
|
<input type="text" id="username" v-model="username" class="w-full px-2 py-1 mt-1 bg-gray-600 text-white border-4 border-gray-500 pixel-border focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-3/4 mb-4">
|
||||||
|
<label for="password" class="block text-white font-pixel text-xs">Password</label>
|
||||||
|
<input type="password" id="password" v-model="password" class="w-full px-2 py-1 mt-1 bg-gray-600 text-white border-4 border-gray-500 pixel-border focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="px-4 py-1 bg-green-500 text-white font-pixel text-xs border-4 border-green-700 pixel-border hover:bg-green-600 transition">Login</button>
|
||||||
|
<p class="text-white text-xs mt-3 cursor-pointer font-pixel" @click="isRegister = true">Baru ada akun ? Klik <span class="text-blue-400 underline">Register</span></p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form v-if="isRegister" class="w-full flex flex-col items-center mt-12" @submit.prevent="handleRegister">
|
||||||
|
<div class="w-3/4 mb-3">
|
||||||
|
<label for="new-username" class="block text-white font-pixel text-xs">Username</label>
|
||||||
|
<input type="text" id="new-username" v-model="newUsername" class="w-full px-2 py-1 mt-1 bg-gray-600 text-white border-4 border-gray-500 pixel-border focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-3/4 mb-3">
|
||||||
|
<label for="email" class="block text-white font-pixel text-xs">Email</label>
|
||||||
|
<input type="email" id="email" v-model="email" class="w-full px-2 py-1 mt-1 bg-gray-600 text-white border-4 border-gray-500 pixel-border focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-3/4 mb-4">
|
||||||
|
<label for="new-password" class="block text-white font-pixel text-xs">Password</label>
|
||||||
|
<input type="password" id="new-password" v-model="newPassword" class="w-full px-2 py-1 mt-1 bg-gray-600 text-white border-4 border-gray-500 pixel-border focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="px-4 py-1 bg-blue-500 text-white font-pixel text-xs border-4 border-blue-700 pixel-border hover:bg-blue-600 transition">Register</button>
|
||||||
|
<p class="text-white text-xs mt-3 cursor-pointer font-pixel" @click="isRegister = false">Sudah punya akun? Klik <span class="text-blue-400 underline">Login</span></p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isRegister: false,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
newUsername: '',
|
||||||
|
email: '',
|
||||||
|
newPassword: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleLogin() {
|
||||||
|
console.log("Logging in with", this.username, this.password);
|
||||||
|
|
||||||
|
this.$router.push('/dashboard');
|
||||||
|
},
|
||||||
|
handleRegister() {
|
||||||
|
console.log("Registering with", this.newUsername, this.email, this.newPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.font-pixel {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
}
|
||||||
|
.pixel-border {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,309 @@
|
||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 w-full h-full flex bg-update-bg bg-cover bg-center">
|
||||||
|
<div class="absolute left-5 top-5 h-1/4 bg-gray-700 bg-opacity-90 border-r-4 border-gray-500 pixel-border shadow-lg w-16 flex flex-col items-center p-4">
|
||||||
|
<nav class="flex flex-col space-y-6 text-center mt-4">
|
||||||
|
<a href="/dashboard" class="text-white hover:text-yellow-400 transition">
|
||||||
|
<LayoutDashboard class="w-8 h-8" />
|
||||||
|
</a>
|
||||||
|
<a href="/scraper" class="text-white hover:text-blue-400 transition">
|
||||||
|
<Bot class="w-8 h-8" />
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full max-w-full gap-6 ml-10 p-4">
|
||||||
|
|
||||||
|
<!-- Scraping Section -->
|
||||||
|
<div class="w-full bg-gray-700 bg-opacity-90 border-4 border-gray-500 pixel-border flex flex-col items-center p-6">
|
||||||
|
<h2 class="text-center text-xl font-bold font-pixel text-white mb-4"></h2>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<button @click="startScrapeScopus" class="bg-blue-600 hover:bg-blue-700 text-white font-pixel px-4 py-2 rounded shadow">
|
||||||
|
Scrape Scopus
|
||||||
|
</button>
|
||||||
|
<button @click="startScrapeHki" class="bg-green-600 hover:bg-green-700 text-white font-pixel px-4 py-2 rounded shadow">
|
||||||
|
Scrape HKI
|
||||||
|
</button>
|
||||||
|
<button @click="startScrapeGoogleScholar" class="bg-purple-600 hover:bg-purple-700 text-white font-pixel px-4 py-2 rounded shadow">
|
||||||
|
Scrape Google Scholar
|
||||||
|
</button>
|
||||||
|
<button @click="startScrapePenelitian" class="bg-red-600 hover:bg-red-700 text-white font-pixel px-4 py-2 rounded shadow">
|
||||||
|
Scrape Penelitian
|
||||||
|
</button>
|
||||||
|
<button @click="startScrapePengabdian" class="bg-yellow-600 hover:bg-yellow-700 text-white font-pixel px-4 py-2 rounded shadow">
|
||||||
|
Scrape Pengabdian
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- <a v-if="latestFile" :href="`http://localhost:8000/scopus/download`" target="_blank" class="bg-green-600 hover:bg-green-700 text-white font-pixel px-4 py-2 rounded shadow">
|
||||||
|
Download Data
|
||||||
|
</a>
|
||||||
|
<router-link v-if="latestFile" to="/data-cleaning" class="bg-purple-600 hover:bg-purple-700 text-white font-pixel px-4 py-2 rounded shadow">
|
||||||
|
Open Cleaning
|
||||||
|
</router-link> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 text-sm font-pixel">
|
||||||
|
<div v-if="loading" class="text-yellow-300 animate-pulse">🔄 Sedang memproses ...</div>
|
||||||
|
<div v-if="statusMessage" class="text-green-400">{{ statusMessage }}</div>
|
||||||
|
<div v-if="errorMessage" class="text-red-400">{{ errorMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-full p-6 bg-gray-800 border-4 border-dashed border-gray-600 rounded-lg text-center text-white font-pixel"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
>
|
||||||
|
<p>📂 Tarik dan lepas file Excel di sini atau klik untuk upload</p>
|
||||||
|
<input type="file" accept=".xlsx" class="hidden" ref="fileInput" @change="handleFileUpload" />
|
||||||
|
<button @click="$refs.fileInput.click()" class="mt-4 bg-blue-700 hover:bg-blue-800 px-4 py-2 rounded">
|
||||||
|
Upload Manual
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Modal Konfirmasi -->
|
||||||
|
<div v-if="showModal" class="fixed inset-0 bg-black bg-opacity-60 flex justify-center items-center z-50">
|
||||||
|
<div class="bg-gray-700 rounded-lg shadow-lg w-96 p-6">
|
||||||
|
<h3 class="text-lg font-bold mb-4">Pilih Kategori Grafik</h3>
|
||||||
|
<p class="mb-4 text-sm text-gray-700">File berhasil dibersihkan. Masukkan sebagai grafik apa?</p>
|
||||||
|
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||||
|
<button v-for="option in chartOptions" :key="option" @click="submitCleanedFile(option)"
|
||||||
|
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded font-pixel text-sm">
|
||||||
|
{{ option }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button @click="showModal = false" class="text-gray-500 hover:text-red-500 text-sm">Batal</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { LayoutDashboard, Bot, Settings, LogOut } from "lucide-vue-next"
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const statusMessage = ref("")
|
||||||
|
const errorMessage = ref("")
|
||||||
|
const latestFile = ref("")
|
||||||
|
const tableData = ref([])
|
||||||
|
const showModal = ref(false)
|
||||||
|
const cleanedFile = ref(null)
|
||||||
|
const chartOptions = ["HKI", "Penelitian", "Pengabdian", "Scopus", "Scholar"]
|
||||||
|
|
||||||
|
const selectChart = (option) => {
|
||||||
|
selectedChart.value = option
|
||||||
|
showModal.value = false
|
||||||
|
// Simpan info chart untuk dashboard (bisa via localStorage, Vuex, atau query parameter)
|
||||||
|
localStorage.setItem("uploadedChartTarget", option)
|
||||||
|
alert(`✅ File akan dimasukkan ke grafik: ${option}`) // Ganti dengan router push jika ingin langsung ke dashboard
|
||||||
|
// Optionally, arahkan ke DashboardView.vue
|
||||||
|
// router.push('/dashboard')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const triggerDownload = (filename, source) => {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = `http://localhost:8000/${source}/download`
|
||||||
|
link.download = filename
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
const startScrapeScopus = async () => {
|
||||||
|
loading.value = true
|
||||||
|
statusMessage.value = ""
|
||||||
|
errorMessage.value = ""
|
||||||
|
try {
|
||||||
|
await axios.get(`http://localhost:8000/scrape/scopus`)
|
||||||
|
statusMessage.value = "✅ Scraping selesai!"
|
||||||
|
await fetchLatestData()
|
||||||
|
if (latestFile.value) {
|
||||||
|
triggerDownload(latestFile.value)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorMessage.value = "❌ Gagal scraping: " + err.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startScrapeGoogleScholar = async () => {
|
||||||
|
loading.value = true
|
||||||
|
statusMessage.value = ""
|
||||||
|
errorMessage.value = ""
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`http://localhost:8000/scrape/scholar`)
|
||||||
|
console.log(response.data)
|
||||||
|
statusMessage.value = response.data.message || "✅ Scraping Scholar selesai!"
|
||||||
|
await fetchLatestData()
|
||||||
|
if (latestFile.value) {
|
||||||
|
triggerDownload(latestFile.value)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
errorMessage.value = "❌ Gagal scraping Scholar: " + (err.response?.data?.error || err.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startScrapeHki = async () => {
|
||||||
|
loading.value = true
|
||||||
|
statusMessage.value = ""
|
||||||
|
errorMessage.value = ""
|
||||||
|
try {
|
||||||
|
await axios.get(`http://localhost:8000/scrape/hki`)
|
||||||
|
statusMessage.value = "✅ Scraping HKI selesai!"
|
||||||
|
await fetchLatestData()
|
||||||
|
if (latestFile.value) {
|
||||||
|
triggerDownload(latestFile.value)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorMessage.value = "❌ Gagal scraping HKI: " + err.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startScrapePenelitian = async () => {
|
||||||
|
loading.value = true
|
||||||
|
statusMessage.value = ""
|
||||||
|
errorMessage.value = ""
|
||||||
|
try {
|
||||||
|
await axios.get(`http://localhost:8000/scrape/penelitian`)
|
||||||
|
statusMessage.value = "✅ Scraping Penelitian selesai!"
|
||||||
|
await fetchLatestData()
|
||||||
|
if (latestFile.value) {
|
||||||
|
triggerDownload(latestFile.value)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorMessage.value = "❌ Gagal scraping Penelitian: " + err.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startScrapePengabdian = async () => {
|
||||||
|
loading.value = true
|
||||||
|
statusMessage.value = ""
|
||||||
|
errorMessage.value = ""
|
||||||
|
try {
|
||||||
|
await axios.get(`http://localhost:8000/scrape/pengabdian`) // Ganti dengan endpoint scraping Pengabdian
|
||||||
|
statusMessage.value = "✅ Scraping Pengabdian selesai!"
|
||||||
|
await fetchLatestData('pengabdian')
|
||||||
|
if (latestFile.value) {
|
||||||
|
triggerDownload(latestFile.value)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorMessage.value = "❌ Gagal scraping Pengabdian: " + err.message
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchLatestData = async () => {
|
||||||
|
try {
|
||||||
|
const resFile = await axios.get(`http://localhost:8000/scopus/latest-file`)
|
||||||
|
latestFile.value = resFile.data.file
|
||||||
|
if (latestFile.value) {
|
||||||
|
const resData = await axios.get(`http://localhost:8000/scopus/preview`)
|
||||||
|
tableData.value = resData.data.data
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = (e) => {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
if (file && file.name.endsWith('.xlsx')) {
|
||||||
|
uploadExcel(file)
|
||||||
|
} else {
|
||||||
|
errorMessage.value = "❌ File bukan format .xlsx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileUpload = (e) => {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file && file.name.endsWith('.xlsx')) {
|
||||||
|
uploadExcel(file)
|
||||||
|
} else {
|
||||||
|
errorMessage.value = "❌ File bukan format .xlsx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startScrapeAndDownload = async (type) => {
|
||||||
|
const response = await fetch(`http://localhost:8000/scrape-download/${type}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${type}_data.xlsx`;
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} else {
|
||||||
|
alert(`Gagal scraping ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadExcel = async (file) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response = await axios.post("http://localhost:8000/upload-excel", formData, { responseType: 'blob' });
|
||||||
|
// Simpan file di blob untuk diproses lagi setelah memilih grafik
|
||||||
|
cleanedFile.value = new Blob([response.data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
|
||||||
|
showModal.value = true; // Tampilkan modal
|
||||||
|
statusMessage.value = "✅ File berhasil dibersihkan. Pilih grafik tujuan.";
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error response:", err.response);
|
||||||
|
errorMessage.value = "❌ Upload gagal: " + (err.response?.data?.error || err.message);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitCleanedFile = async (chartTypeRaw) => {
|
||||||
|
const chartType = chartTypeRaw.toLowerCase()
|
||||||
|
if (!cleanedFile.value) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", cleanedFile.value, `${chartType}_cleaned.xlsx`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`http://localhost:8000/submit-cleaned/${chartType}`, formData);
|
||||||
|
statusMessage.value = `✅ File berhasil dikirim ke grafik ${chartTypeRaw}`;
|
||||||
|
showModal.value = false;
|
||||||
|
cleanedFile.value = null;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
errorMessage.value = `❌ Gagal mengirim ke grafik ${chartTypeRaw}: ` + (err.response?.data?.error || err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(fetchLatestData)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-pixel {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
}
|
||||||
|
.pixel-border {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
position: relative;
|
||||||
|
border-width: 4px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow:
|
||||||
|
inset -2px -2px 0px rgba(0, 0, 0, 0.8),
|
||||||
|
inset 2px 2px 0px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{vue,js,ts,jsx,tsx}"
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily:{
|
||||||
|
pixel: ["'Press Start 2P'", "cursive"],
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
'login-bg': "url('/bgta_n.png')",
|
||||||
|
'dashboard-bg': "url('/bgpixel3.png')",
|
||||||
|
'update-bg': "url('/update_bg.png')",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require('tailwind-scrollbar')],
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueDevTools(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,12 @@
|
||||||
|
import gspread
|
||||||
|
from google.oauth2.service_account import Credentials
|
||||||
|
|
||||||
|
SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
|
||||||
|
CREDS_FILE = "D:/lecturertask/credentials.json" # Sesuaikan path JSON kamu
|
||||||
|
|
||||||
|
def get_sheet(spreadsheet_id, worksheet_name):
|
||||||
|
credentials = Credentials.from_service_account_file(CREDS_FILE, scopes=SCOPES)
|
||||||
|
client = gspread.authorize(credentials)
|
||||||
|
spreadsheet = client.open_by_key(spreadsheet_id)
|
||||||
|
worksheet = spreadsheet.worksheet(worksheet_name)
|
||||||
|
return worksheet
|
|
@ -0,0 +1,379 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
|
from fastapi import FastAPI, File, UploadFile, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Dict
|
||||||
|
import subprocess
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
import re
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from google_sheets_helper import get_sheet
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
# CORS Middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["http://localhost:5173"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
DATA_FOLDER = "D:\\lecturertask"
|
||||||
|
BASE_FILENAME = "data_scopus"
|
||||||
|
FILE_EXT = ".xlsx"
|
||||||
|
|
||||||
|
BASE_DIR = "D:/lecturertask/download_files_scraper"
|
||||||
|
|
||||||
|
folder_map = {
|
||||||
|
"cleaned_pengabdian.xlsx": "pengabdian",
|
||||||
|
"cleaned_penelitian.xlsx": "penelitian",
|
||||||
|
"cleaned_hki.xlsx": "hki",
|
||||||
|
"cleaned_scholar.xlsx": "scholar",
|
||||||
|
"cleaned_scopus.xlsx": "scopus",
|
||||||
|
}
|
||||||
|
|
||||||
|
file_map = {
|
||||||
|
"pengabdian": os.path.join(BASE_DIR, "pengabdian", "pengabdian_cleaned.xlsx"),
|
||||||
|
"penelitian": os.path.join(BASE_DIR, "penelitian", "penelitian_cleaned.xlsx"),
|
||||||
|
"hki": os.path.join(BASE_DIR, "hki", "hki_cleaned.xlsx"),
|
||||||
|
"scopus": os.path.join(BASE_DIR, "scopus", "scopus_cleaned.xlsx"),
|
||||||
|
"scholar": os.path.join(BASE_DIR, "scholar", "scholar_cleaned.xlsx"),
|
||||||
|
}
|
||||||
|
|
||||||
|
class SaveFileRequest(BaseModel):
|
||||||
|
folder: str
|
||||||
|
filename: str
|
||||||
|
data: List[Dict]
|
||||||
|
|
||||||
|
@router.post("/google-sheet/update/{category}")
|
||||||
|
def update_sheet_from_local(category: str):
|
||||||
|
path_map = {
|
||||||
|
"penelitian": "D:/lecturertask/download_files_scraper/penelitian/penelitian_cleaned.xlsx",
|
||||||
|
"pengabdian": "D:/lecturertask/download_files_scraper/pengabdian/pengabdian_cleaned.xlsx",
|
||||||
|
"hki": "D:/lecturertask/download_files_scraper/hki/hki_cleaned.xlsx",
|
||||||
|
"scopus": "D:/lecturertask/download_files_scraper/scopus/scopus_cleaned.xlsx",
|
||||||
|
"scholar": "D:/lecturertask/download_files_scraper/scholar/scholar_cleaned.xlsx",
|
||||||
|
}
|
||||||
|
|
||||||
|
spreadsheet_id = "SPREADSHEET_ID_KAMU"
|
||||||
|
worksheet_name = category
|
||||||
|
|
||||||
|
if category not in path_map:
|
||||||
|
raise HTTPException(status_code=400, detail="Kategori tidak valid")
|
||||||
|
|
||||||
|
file_path = path_map[category]
|
||||||
|
df = pd.read_excel(file_path)
|
||||||
|
|
||||||
|
worksheet = get_sheet(spreadsheet_id, worksheet_name)
|
||||||
|
worksheet.clear()
|
||||||
|
|
||||||
|
# Set header + data
|
||||||
|
worksheet.update([df.columns.values.tolist()] + df.values.tolist())
|
||||||
|
|
||||||
|
return {"message": f"Data dari {category} berhasil dikirim ke Google Sheets."}
|
||||||
|
|
||||||
|
@app.post("/save-cleaned-file")
|
||||||
|
async def save_cleaned_file(req: SaveFileRequest):
|
||||||
|
# Tentukan path file
|
||||||
|
base_path = r"D:\lecturertask\download_files_scraper"
|
||||||
|
folder_path = os.path.join(base_path, req.folder)
|
||||||
|
os.makedirs(folder_path, exist_ok=True)
|
||||||
|
file_path = os.path.join(folder_path, req.filename)
|
||||||
|
|
||||||
|
# Convert list of dict ke DataFrame
|
||||||
|
df = pd.DataFrame(req.data)
|
||||||
|
|
||||||
|
# Simpan ke Excel (overwrite)
|
||||||
|
df.to_excel(file_path, index=False)
|
||||||
|
|
||||||
|
return {"message": "File berhasil disimpan", "path": file_path}
|
||||||
|
|
||||||
|
@app.delete("/delete_all_excel")
|
||||||
|
def delete_all_excel_files():
|
||||||
|
folder_path = "D:/lecturertask/download_files_scraper"
|
||||||
|
files = glob.glob(os.path.join(folder_path, "*.xlsx"))
|
||||||
|
for file in files:
|
||||||
|
os.remove(file)
|
||||||
|
return {"message": "Semua file Excel berhasil dihapus."}
|
||||||
|
|
||||||
|
@app.get("/download/{file_type}")
|
||||||
|
def download_file(file_type: str):
|
||||||
|
if file_type in file_map:
|
||||||
|
file_path = file_map[file_type]
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
file_like = open(file_path, mode="rb")
|
||||||
|
return StreamingResponse(
|
||||||
|
file_like,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename={os.path.basename(file_path)}"}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="File not found.")
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Invalid file type.")
|
||||||
|
|
||||||
|
@app.get("/cleaned-files/{folder}/{filename}")
|
||||||
|
def get_cleaned_file(folder: str, filename: str):
|
||||||
|
file_path = os.path.join(BASE_DIR, folder, filename)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
return FileResponse(file_path, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
return {"error": "File not found"}
|
||||||
|
|
||||||
|
@app.get("/list-files/{category}")
|
||||||
|
def list_files(category: str):
|
||||||
|
folder_path = os.path.join(BASE_FOLDER, category.lower())
|
||||||
|
if not os.path.exists(folder_path):
|
||||||
|
raise HTTPException(status_code=404, detail="Folder tidak ditemukan")
|
||||||
|
|
||||||
|
files = os.listdir(folder_path)
|
||||||
|
return {"files": files}
|
||||||
|
|
||||||
|
@app.get("/download-file/{category}/{filename}")
|
||||||
|
def download_file(category: str, filename: str):
|
||||||
|
file_path = os.path.join(BASE_FOLDER, category.lower(), filename)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise HTTPException(status_code=404, detail="File tidak ditemukan")
|
||||||
|
|
||||||
|
return FileResponse(path=file_path, filename=filename, media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_file(file_pattern: str):
|
||||||
|
files = glob.glob(os.path.join(DATA_FOLDER, file_pattern))
|
||||||
|
if not files:
|
||||||
|
return None
|
||||||
|
latest_file = max(files, key=os.path.getctime)
|
||||||
|
return latest_file
|
||||||
|
|
||||||
|
# Home route
|
||||||
|
@app.get("/")
|
||||||
|
def home():
|
||||||
|
return {"message": "Welcome to Scraper penelitian API"}
|
||||||
|
|
||||||
|
def scrape_and_download(script_name: str, file_pattern: str):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["python", f"D:\\lecturertask\\scraper\\{script_name}"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return JSONResponse(content={"error": result.stderr}, status_code=500)
|
||||||
|
|
||||||
|
latest_file = get_latest_file(file_pattern)
|
||||||
|
if latest_file:
|
||||||
|
return FileResponse(latest_file, filename=os.path.basename(latest_file))
|
||||||
|
return JSONResponse(content={"error": "File hasil scraping tidak ditemukan."}, status_code=404)
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/submit-cleaned/{chart_type}")
|
||||||
|
async def submit_cleaned_file(chart_type: str, file: UploadFile = File(...)):
|
||||||
|
chart_type = chart_type.lower()
|
||||||
|
folder_map = {
|
||||||
|
"hki": "hki",
|
||||||
|
"penelitian": "penelitian",
|
||||||
|
"pengabdian": "pengabdian",
|
||||||
|
"scopus": "scopus",
|
||||||
|
"scholar": "scholar"
|
||||||
|
}
|
||||||
|
folder = folder_map.get(chart_type)
|
||||||
|
if not folder:
|
||||||
|
raise HTTPException(status_code=400, detail="Kategori tidak valid")
|
||||||
|
|
||||||
|
save_path = f"download_files_scraper/{folder}/{file.filename}"
|
||||||
|
with open(save_path, "wb") as f:
|
||||||
|
f.write(await file.read())
|
||||||
|
|
||||||
|
return {"message": f"File berhasil disimpan ke grafik {chart_type}"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload-excel")
|
||||||
|
async def upload_excel(file: UploadFile = File(...)):
|
||||||
|
try:
|
||||||
|
file_content = await file.read()
|
||||||
|
df = pd.read_excel(BytesIO(file_content))
|
||||||
|
|
||||||
|
with open("D:/lecturertask/cleaner_tokens.txt", "r") as f:
|
||||||
|
cleaner_tokens = [line.strip().lower() for line in f.readlines()]
|
||||||
|
|
||||||
|
def contains_name(row):
|
||||||
|
for token in cleaner_tokens:
|
||||||
|
pattern = r"\b" + re.escape(token) + r"\b"
|
||||||
|
if any(re.search(pattern, str(cell), re.IGNORECASE) for cell in row):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
df_cleaned = df[df.apply(contains_name, axis=1)]
|
||||||
|
|
||||||
|
existing_files = [f for f in os.listdir(DATA_FOLDER) if f.startswith("cleaned_excel_file_scraping") and f.endswith(FILE_EXT)]
|
||||||
|
max_num = 0
|
||||||
|
for filename in existing_files:
|
||||||
|
try:
|
||||||
|
num = int(filename.replace("cleaned_excel_file_scraping", "").replace(FILE_EXT, ""))
|
||||||
|
if num > max_num:
|
||||||
|
max_num = num
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
next_num = max_num + 1
|
||||||
|
new_filename = f"cleaned_excel_file_scraping{next_num}{FILE_EXT}"
|
||||||
|
save_path = os.path.join(DATA_FOLDER, new_filename)
|
||||||
|
|
||||||
|
print(f"Path penyimpanan file: {save_path}")
|
||||||
|
|
||||||
|
df_cleaned.to_excel(save_path, index=False, engine="openpyxl")
|
||||||
|
|
||||||
|
return FileResponse(save_path, media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', filename=new_filename)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_message = traceback.format_exc()
|
||||||
|
return JSONResponse(status_code=500, content={"error": error_message})
|
||||||
|
|
||||||
|
@app.get("/scrape/scholar")
|
||||||
|
def scrape_scholar():
|
||||||
|
return scrape_data("scraper_scholar.py", "Scraping Scholar selesai.")
|
||||||
|
|
||||||
|
@app.get("/scholar/download")
|
||||||
|
def download_latest_scholar():
|
||||||
|
return download_file("data_scholar*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape/scopus")
|
||||||
|
def scrape_scopus():
|
||||||
|
return scrape_data("scraper_scopus.py", "Scraping Scopus selesai.")
|
||||||
|
|
||||||
|
@app.get("/scopus/download")
|
||||||
|
def download_latest_file():
|
||||||
|
return download_file(f"{BASE_FILENAME}*{FILE_EXT}")
|
||||||
|
|
||||||
|
@app.get("/scrape/pengabdian")
|
||||||
|
def scrape_pengabdian():
|
||||||
|
return scrape_data("scraper_pengabdian.py", "Scraping Pengabdian selesai.")
|
||||||
|
|
||||||
|
@app.get("/pengabdian/download")
|
||||||
|
def download_latest_pengabdian():
|
||||||
|
return download_file("data_pengabdian*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape/hki")
|
||||||
|
def scrape_hki():
|
||||||
|
return scrape_data("scraper_HKI.py", "Scraping HKI selesai.")
|
||||||
|
|
||||||
|
@app.get("/hki/download")
|
||||||
|
def download_latest_hki():
|
||||||
|
return download_file("data_hki*.xlsx")
|
||||||
|
@app.get("/scrape/penelitian")
|
||||||
|
def scrape_penelitian():
|
||||||
|
return scrape_data("scraper_penelitian.py", "Scraping Penelitian selesai.")
|
||||||
|
|
||||||
|
@app.get("/penelitian/download")
|
||||||
|
def download_latest_penelitian():
|
||||||
|
return download_file("data_penelitian*.xlsx")
|
||||||
|
|
||||||
|
|
||||||
|
# Generic function to scrape data
|
||||||
|
def scrape_data(script_name: str, success_message: str):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["python", f"D:\\lecturertask\\scraper\\{script_name}"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return {"message": success_message}
|
||||||
|
return JSONResponse(content={"error": result.stderr}, status_code=500)
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
@app.get("/scrape-download/scopus")
|
||||||
|
def scrape_download_scopus():
|
||||||
|
return scrape_and_download("scraper_scopus.py", "data_scopus*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/scholar")
|
||||||
|
def scrape_download_scholar():
|
||||||
|
return scrape_and_download("scraper_scholar.py", "data_scholar*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/pengabdian")
|
||||||
|
def scrape_download_pengabdian():
|
||||||
|
return scrape_and_download("scraper_pengabdian.py", "data_pengabdian*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/hki")
|
||||||
|
def scrape_download_hki():
|
||||||
|
return scrape_and_download("scraper_HKI.py", "data_hki*.xlsx")
|
||||||
|
|
||||||
|
@app.get("/scrape-download/penelitian")
|
||||||
|
def scrape_download_penelitian():
|
||||||
|
return scrape_and_download("scraper_penelitian.py", "data_penelitian*.xlsx")
|
||||||
|
|
||||||
|
|
||||||
|
# Generic function to download file
|
||||||
|
def download_file(file_pattern: str):
|
||||||
|
latest_file = get_latest_file(file_pattern)
|
||||||
|
if latest_file:
|
||||||
|
return FileResponse(latest_file, filename=os.path.basename(latest_file))
|
||||||
|
return JSONResponse(content={"error": "File tidak ditemukan."}, status_code=404)
|
||||||
|
|
||||||
|
# Generic function to preview file
|
||||||
|
def preview_file(file_pattern: str):
|
||||||
|
latest_file = get_latest_file(file_pattern)
|
||||||
|
if not latest_file:
|
||||||
|
return {"data": []}
|
||||||
|
df = pd.read_excel(latest_file)
|
||||||
|
return {"data": df.to_dict(orient="records")}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload-pengabdian")
|
||||||
|
async def upload_pengabdian_excel(file: UploadFile = File(...)):
|
||||||
|
contents = await file.read()
|
||||||
|
path = os.path.join(DATA_FOLDER, f"uploaded_pengabdian_{file.filename}")
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(contents)
|
||||||
|
return {"message": "File berhasil diunggah", "filename": file.filename}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload-dashboard")
|
||||||
|
def upload_dashboard_file(file: UploadFile = File(...)):
|
||||||
|
contents = file.file.read()
|
||||||
|
filename = file.filename.lower()
|
||||||
|
|
||||||
|
# Simpan file sementara
|
||||||
|
path = f"uploaded_files/{file.filename}"
|
||||||
|
os.makedirs("uploaded_files", exist_ok=True)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
# Baca data dan tentukan tipe
|
||||||
|
df = pd.read_excel(path)
|
||||||
|
detected_type = None
|
||||||
|
|
||||||
|
if "judul pengabdian" in df.columns.str.lower():
|
||||||
|
detected_type = "pengabdian"
|
||||||
|
elif "judul penelitian" in df.columns.str.lower():
|
||||||
|
detected_type = "penelitian"
|
||||||
|
elif "inventor" in df.columns.str.lower():
|
||||||
|
detected_type = "hki"
|
||||||
|
elif "source title" in df.columns.str.lower():
|
||||||
|
detected_type = "scopus"
|
||||||
|
elif "title" in df.columns.str.lower() and "citations" in df.columns.str.lower():
|
||||||
|
detected_type = "scholar"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"detected_type": detected_type,
|
||||||
|
"filename": file.filename,
|
||||||
|
"columns": df.columns.tolist(),
|
||||||
|
"rows": df.head(5).to_dict(orient="records")
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
|
|
@ -0,0 +1,67 @@
|
||||||
|
{
|
||||||
|
"name": "lecturertask",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"node_modules/@types/prismjs": {
|
||||||
|
"version": "1.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||||
|
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prism-react-renderer": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prismjs": "^1.26.0",
|
||||||
|
"clsx": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "19.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||||
|
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tailwind-scrollbar": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-j2ZfUI7p8xmSQdlqaCxEb4Mha8ErvWjDVyu2Ke4IstWprQ/6TmIz1GSLE62vsTlXwnMLYhuvbFbIFzaJGOGtMg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prism-react-renderer": "^2.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.13.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": "4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tailwindcss": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Installation
|
||||||
|
> `npm install --save @types/prismjs`
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
This package contains type definitions for prismjs (http://prismjs.com/).
|
||||||
|
|
||||||
|
# Details
|
||||||
|
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/prismjs.
|
||||||
|
|
||||||
|
### Additional Details
|
||||||
|
* Last updated: Wed, 23 Oct 2024 03:36:41 GMT
|
||||||
|
* Dependencies: none
|
||||||
|
|
||||||
|
# Credits
|
||||||
|
These definitions were written by [Michael Schmidt](https://github.com/RunDevelopment), [ExE Boss](https://github.com/ExE-Boss), [Erik Lieben](https://github.com/eriklieben), [Andre Wiggins](https://github.com/andrewiggins), and [Michał Miszczyszyn](https://github.com/typeofweb).
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const core: Record<string, any>;
|
||||||
|
export const languages: Record<string, any>;
|
||||||
|
export const plugins: Record<string, any>;
|
||||||
|
export const themes: Record<string, any>;
|
|
@ -0,0 +1,19 @@
|
||||||
|
interface LoadLanguages {
|
||||||
|
/**
|
||||||
|
* Set this to `true` to prevent all warning messages `loadLanguages` logs.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
silent: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the given languages and adds them to the current Prism instance.
|
||||||
|
*
|
||||||
|
* If no languages are provided, _all_ Prism languages will be loaded.
|
||||||
|
*/
|
||||||
|
(languages?: string | string[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const loadLanguages: LoadLanguages;
|
||||||
|
|
||||||
|
export = loadLanguages;
|
|
@ -0,0 +1,402 @@
|
||||||
|
export as namespace Prism;
|
||||||
|
export const languages: Languages;
|
||||||
|
export const plugins: Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, if Prism is in a web worker, it assumes that it is in a worker it created itself, so it uses
|
||||||
|
* `addEventListener` to communicate with its parent instance. However, if you're using Prism manually in your
|
||||||
|
* own worker, you don't want it to do this.
|
||||||
|
*
|
||||||
|
* By setting this value to `true`, Prism will not add its own listeners to the worker.
|
||||||
|
*
|
||||||
|
* You obviously have to change this value before Prism executes. To do this, you can add an
|
||||||
|
* empty Prism object into the global scope before loading the Prism script like this:
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
export let disableWorkerMessageHandler: boolean | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, Prism will attempt to highlight all code elements (by calling {@link Prism.highlightAll}) on the
|
||||||
|
* current page after the page finished loading. This might be a problem if e.g. you wanted to asynchronously load
|
||||||
|
* additional languages or plugins yourself.
|
||||||
|
*
|
||||||
|
* By setting this value to `true`, Prism will not automatically highlight all code elements on the page.
|
||||||
|
*
|
||||||
|
* You obviously have to change this value before the automatic highlighting started. To do this, you can add an
|
||||||
|
* empty Prism object into the global scope before loading the Prism script like this:
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
export let manual: boolean | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function which will be invoked after an element was successfully highlighted.
|
||||||
|
*
|
||||||
|
* @param element The element successfully highlighted.
|
||||||
|
*/
|
||||||
|
export type HighlightCallback = (element: Element) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the most high-level function in Prism’s API.
|
||||||
|
* It fetches all the elements that have a `.language-xxxx` class and then calls {@link Prism.highlightElement} on
|
||||||
|
* each one of them.
|
||||||
|
*
|
||||||
|
* This is equivalent to `Prism.highlightAllUnder(document, async, callback)`.
|
||||||
|
*
|
||||||
|
* @param [async=false] Same as in {@link Prism.highlightAllUnder}.
|
||||||
|
* @param [callback] Same as in {@link Prism.highlightAllUnder}.
|
||||||
|
*/
|
||||||
|
export function highlightAll(async?: boolean, callback?: HighlightCallback): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all the descendants of `container` that have a `.language-xxxx` class and then calls
|
||||||
|
* {@link Prism.highlightElement} on each one of them.
|
||||||
|
*
|
||||||
|
* The following hooks will be run:
|
||||||
|
* 1. `before-highlightall`
|
||||||
|
* 2. All hooks of {@link Prism.highlightElement} for each element.
|
||||||
|
*
|
||||||
|
* @param container The root element, whose descendants that have a `.language-xxxx` class will be highlighted.
|
||||||
|
* @param [async=false] Whether each element is to be highlighted asynchronously using Web Workers.
|
||||||
|
* @param [callback] An optional callback to be invoked on each element after its highlighting is done.
|
||||||
|
*/
|
||||||
|
export function highlightAllUnder(container: ParentNode, async?: boolean, callback?: HighlightCallback): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights the code inside a single element.
|
||||||
|
*
|
||||||
|
* The following hooks will be run:
|
||||||
|
* 1. `before-sanity-check`
|
||||||
|
* 2. `before-highlight`
|
||||||
|
* 3. All hooks of {@link Prism.highlightElement}. These hooks will only be run by the current worker if `async` is `true`.
|
||||||
|
* 4. `before-insert`
|
||||||
|
* 5. `after-highlight`
|
||||||
|
* 6. `complete`
|
||||||
|
*
|
||||||
|
* @param element The element containing the code.
|
||||||
|
* It must have a class of `language-xxxx` to be processed, where `xxxx` is a valid language identifier.
|
||||||
|
* @param [async=false] Whether the element is to be highlighted asynchronously using Web Workers
|
||||||
|
* to improve performance and avoid blocking the UI when highlighting very large chunks of code. This option is
|
||||||
|
* [disabled by default](https://prismjs.com/faq.html#why-is-asynchronous-highlighting-disabled-by-default).
|
||||||
|
*
|
||||||
|
* Note: All language definitions required to highlight the code must be included in the main `prism.js` file for
|
||||||
|
* asynchronous highlighting to work. You can build your own bundle on the
|
||||||
|
* [Download page](https://prismjs.com/download.html).
|
||||||
|
* @param [callback] An optional callback to be invoked after the highlighting is done.
|
||||||
|
* Mostly useful when `async` is `true`, since in that case, the highlighting is done asynchronously.
|
||||||
|
*/
|
||||||
|
export function highlightElement(element: Element, async?: boolean, callback?: HighlightCallback): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low-level function, only use if you know what you’re doing. It accepts a string of text as input
|
||||||
|
* and the language definitions to use, and returns a string with the HTML produced.
|
||||||
|
*
|
||||||
|
* The following hooks will be run:
|
||||||
|
* 1. `before-tokenize`
|
||||||
|
* 2. `after-tokenize`
|
||||||
|
* 3. `wrap`: On each {@link Prism.Token}.
|
||||||
|
*
|
||||||
|
* @param text A string with the code to be highlighted.
|
||||||
|
* @param grammar An object containing the tokens to use.
|
||||||
|
*
|
||||||
|
* Usually a language definition like `Prism.languages.markup`.
|
||||||
|
* @param language The name of the language definition passed to `grammar`.
|
||||||
|
* @returns The highlighted HTML.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Prism.highlight('var foo = true;', Prism.languages.js, 'js');
|
||||||
|
*/
|
||||||
|
export function highlight(text: string, grammar: Grammar, language: string): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the heart of Prism, and the most low-level function you can use. It accepts a string of text as input
|
||||||
|
* and the language definitions to use, and returns an array with the tokenized code.
|
||||||
|
*
|
||||||
|
* When the language definition includes nested tokens, the function is called recursively on each of these tokens.
|
||||||
|
*
|
||||||
|
* This method could be useful in other contexts as well, as a very crude parser.
|
||||||
|
*
|
||||||
|
* @param text A string with the code to be highlighted.
|
||||||
|
* @param grammar An object containing the tokens to use.
|
||||||
|
*
|
||||||
|
* Usually a language definition like `Prism.languages.markup`.
|
||||||
|
* @returns An array of strings, tokens and other arrays.
|
||||||
|
*/
|
||||||
|
export function tokenize(text: string, grammar: Grammar): Array<string | Token>;
|
||||||
|
|
||||||
|
export interface Environment extends Record<string, any> {
|
||||||
|
selector?: string | undefined;
|
||||||
|
element?: Element | undefined;
|
||||||
|
language?: string | undefined;
|
||||||
|
grammar?: Grammar | undefined;
|
||||||
|
code?: string | undefined;
|
||||||
|
highlightedCode?: string | undefined;
|
||||||
|
type?: string | undefined;
|
||||||
|
content?: string | undefined;
|
||||||
|
tag?: string | undefined;
|
||||||
|
classes?: string[] | undefined;
|
||||||
|
attributes?: Record<string, string> | undefined;
|
||||||
|
parent?: Array<string | Token> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace util {
|
||||||
|
interface Identifier {
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encode raw strings in tokens in preparation to display as HTML */
|
||||||
|
function encode(tokens: TokenStream): TokenStream;
|
||||||
|
|
||||||
|
/** Determine the type of the object */
|
||||||
|
function type(o: null): "Null";
|
||||||
|
function type(o: undefined): "Undefined";
|
||||||
|
function type(o: boolean | Boolean): "Boolean"; // eslint-disable-line @typescript-eslint/no-wrapper-object-types
|
||||||
|
function type(o: number | Number): "Number"; // eslint-disable-line @typescript-eslint/no-wrapper-object-types
|
||||||
|
function type(o: string | String): "String"; // eslint-disable-line @typescript-eslint/no-wrapper-object-types
|
||||||
|
function type(o: Function): "Function"; // eslint-disable-line @typescript-eslint/no-wrapper-object-types
|
||||||
|
function type(o: RegExp): "RegExp";
|
||||||
|
function type(o: any[]): "Array";
|
||||||
|
function type(o: any): string;
|
||||||
|
|
||||||
|
/** Get the unique id of this object or give it one if it does not have one */
|
||||||
|
function objId(obj: any): Identifier;
|
||||||
|
|
||||||
|
/** Deep clone a language definition (e.g. to extend it) */
|
||||||
|
function clone<T>(o: T): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GrammarValue = RegExp | TokenObject | Array<RegExp | TokenObject>;
|
||||||
|
export type Grammar = GrammarRest | Record<string, GrammarValue>;
|
||||||
|
export interface GrammarRest {
|
||||||
|
keyword?: GrammarValue | undefined;
|
||||||
|
number?: GrammarValue | undefined;
|
||||||
|
function?: GrammarValue | undefined;
|
||||||
|
string?: GrammarValue | undefined;
|
||||||
|
boolean?: GrammarValue | undefined;
|
||||||
|
operator?: GrammarValue | undefined;
|
||||||
|
punctuation?: GrammarValue | undefined;
|
||||||
|
atrule?: GrammarValue | undefined;
|
||||||
|
url?: GrammarValue | undefined;
|
||||||
|
selector?: GrammarValue | undefined;
|
||||||
|
property?: GrammarValue | undefined;
|
||||||
|
important?: GrammarValue | undefined;
|
||||||
|
style?: GrammarValue | undefined;
|
||||||
|
comment?: GrammarValue | undefined;
|
||||||
|
"class-name"?: GrammarValue | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional grammar object that will appended to this grammar.
|
||||||
|
*/
|
||||||
|
rest?: Grammar | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expansion of a simple `RegExp` literal to support additional properties.
|
||||||
|
*/
|
||||||
|
export interface TokenObject {
|
||||||
|
/**
|
||||||
|
* The regular expression of the token.
|
||||||
|
*/
|
||||||
|
pattern: RegExp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `true`, then the first capturing group of `pattern` will (effectively) behave as a lookbehind
|
||||||
|
* group meaning that the captured text will not be part of the matched text of the new token.
|
||||||
|
*/
|
||||||
|
lookbehind?: boolean | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the token is greedy.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
greedy?: boolean | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional alias or list of aliases.
|
||||||
|
*/
|
||||||
|
alias?: string | string[] | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The nested tokens of this token.
|
||||||
|
*
|
||||||
|
* This can be used for recursive language definitions.
|
||||||
|
*
|
||||||
|
* Note that this can cause infinite recursion.
|
||||||
|
*/
|
||||||
|
inside?: Grammar | undefined;
|
||||||
|
}
|
||||||
|
export type Languages = LanguageMapProtocol & LanguageMap;
|
||||||
|
export interface LanguageMap {
|
||||||
|
/**
|
||||||
|
* Get a defined language's definition.
|
||||||
|
*/
|
||||||
|
[language: string]: Grammar;
|
||||||
|
}
|
||||||
|
export interface LanguageMapProtocol {
|
||||||
|
/**
|
||||||
|
* Creates a deep copy of the language with the given id and appends the given tokens.
|
||||||
|
*
|
||||||
|
* If a token in `redef` also appears in the copied language, then the existing token in the copied language
|
||||||
|
* will be overwritten at its original position.
|
||||||
|
*
|
||||||
|
* @param id The id of the language to extend. This has to be a key in `Prism.languages`.
|
||||||
|
* @param redef The new tokens to append.
|
||||||
|
* @returns The new language created.
|
||||||
|
* @example
|
||||||
|
* Prism.languages['css-with-colors'] = Prism.languages.extend('css', {
|
||||||
|
* 'color': /\b(?:red|green|blue)\b/
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
extend(id: string, redef: Grammar): Grammar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts tokens _before_ another token in a language definition or any other grammar.
|
||||||
|
*
|
||||||
|
* As this needs to recreate the object (we cannot actually insert before keys in object literals),
|
||||||
|
* we cannot just provide an object, we need an object and a key.
|
||||||
|
*
|
||||||
|
* If the grammar of `inside` and `insert` have tokens with the same name, the tokens in `inside` will be ignored.
|
||||||
|
*
|
||||||
|
* All references of the old object accessible from `Prism.languages` or `insert` will be replace with the new one.
|
||||||
|
*
|
||||||
|
* @param inside The property of `root` that contains the object to be modified.
|
||||||
|
*
|
||||||
|
* This is usually a language id.
|
||||||
|
* @param before The key to insert before.
|
||||||
|
* @param insert An object containing the key-value pairs to be inserted.
|
||||||
|
* @param [root] The object containing `inside`, i.e. the object that contains the object that will be modified.
|
||||||
|
*
|
||||||
|
* Defaults to `Prism.languages`.
|
||||||
|
* @returns The new grammar created.
|
||||||
|
* @example
|
||||||
|
* Prism.languages.insertBefore('markup', 'cdata', {
|
||||||
|
* 'style': { ... }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
insertBefore(inside: string, before: string, insert: Grammar, root?: LanguageMap): Grammar;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace hooks {
|
||||||
|
/**
|
||||||
|
* @param env The environment variables of the hook.
|
||||||
|
*/
|
||||||
|
type HookCallback = (env: Environment) => void;
|
||||||
|
type HookTypes = keyof HookEnvironmentMap;
|
||||||
|
|
||||||
|
interface HookEnvironmentMap {
|
||||||
|
"before-highlightall": RequiredEnvironment<"selector">;
|
||||||
|
|
||||||
|
"before-sanity-check": ElementEnvironment;
|
||||||
|
"before-highlight": ElementEnvironment;
|
||||||
|
|
||||||
|
"before-insert": ElementHighlightedEnvironment;
|
||||||
|
"after-highlight": ElementHighlightedEnvironment;
|
||||||
|
complete: ElementHighlightedEnvironment;
|
||||||
|
|
||||||
|
"before-tokenize": TokenizeEnvironment;
|
||||||
|
"after-tokenize": TokenizeEnvironment;
|
||||||
|
|
||||||
|
wrap: RequiredEnvironment<"type" | "content" | "tag" | "classes" | "attributes" | "language">;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequiredEnvironment<T extends keyof Environment, U extends Environment = Environment> =
|
||||||
|
& U
|
||||||
|
& Required<Pick<U, T>>;
|
||||||
|
type ElementEnvironment = RequiredEnvironment<"element" | "language" | "grammar" | "code">;
|
||||||
|
type ElementHighlightedEnvironment = RequiredEnvironment<"highlightedCode", ElementEnvironment>;
|
||||||
|
type TokenizeEnvironment = RequiredEnvironment<"code" | "grammar" | "language">;
|
||||||
|
|
||||||
|
interface RegisteredHooks {
|
||||||
|
[hook: string]: HookCallback[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const all: RegisteredHooks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the given callback to the list of callbacks for the given hook.
|
||||||
|
*
|
||||||
|
* The callback will be invoked when the hook it is registered for is run.
|
||||||
|
* Hooks are usually directly run by a highlight function but you can also run hooks yourself.
|
||||||
|
*
|
||||||
|
* One callback function can be registered to multiple hooks and the same hook multiple times.
|
||||||
|
*
|
||||||
|
* @param name The name of the hook.
|
||||||
|
* @param callback The callback function which is given environment variables.
|
||||||
|
*/
|
||||||
|
function add<K extends keyof HookEnvironmentMap>(name: K, callback: (env: HookEnvironmentMap[K]) => void): void;
|
||||||
|
function add(name: string, callback: HookCallback): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a hook invoking all registered callbacks with the given environment variables.
|
||||||
|
*
|
||||||
|
* Callbacks will be invoked synchronously and in the order in which they were registered.
|
||||||
|
*
|
||||||
|
* @param name The name of the hook.
|
||||||
|
* @param env The environment variables of the hook passed to all callbacks registered.
|
||||||
|
*/
|
||||||
|
function run<K extends keyof HookEnvironmentMap>(name: K, env: HookEnvironmentMap[K]): void;
|
||||||
|
function run(name: string, env: Environment): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TokenStream = string | Token | Array<string | Token>;
|
||||||
|
|
||||||
|
export class Token {
|
||||||
|
/**
|
||||||
|
* Creates a new token.
|
||||||
|
*
|
||||||
|
* @param type See {@link Prism.Token#type type}
|
||||||
|
* @param content See {@link Prism.Token#content content}
|
||||||
|
* @param [alias] The alias(es) of the token.
|
||||||
|
* @param [matchedStr=""] A copy of the full string this token was created from.
|
||||||
|
* @param [greedy=false] See {@link Prism.Token#greedy greedy}
|
||||||
|
*/
|
||||||
|
constructor(type: string, content: TokenStream, alias?: string | string[], matchedStr?: string, greedy?: boolean);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the token.
|
||||||
|
*
|
||||||
|
* This is usually the key of a pattern in a {@link Grammar}.
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The strings or tokens contained by this token.
|
||||||
|
*
|
||||||
|
* This will be a token stream if the pattern matched also defined an `inside` grammar.
|
||||||
|
*/
|
||||||
|
content: TokenStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The alias(es) of the token.
|
||||||
|
*
|
||||||
|
* @see TokenObject
|
||||||
|
*/
|
||||||
|
alias: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The length of the matched string or 0.
|
||||||
|
*/
|
||||||
|
length: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the pattern that created this token is greedy or not.
|
||||||
|
*
|
||||||
|
* @see TokenObject
|
||||||
|
*/
|
||||||
|
greedy: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given token or token stream to an HTML representation.
|
||||||
|
*
|
||||||
|
* The following hooks will be run:
|
||||||
|
* 1. `wrap`: On each {@link Prism.Token}.
|
||||||
|
*
|
||||||
|
* @param token The token or token stream to be converted.
|
||||||
|
* @param language The name of current language.
|
||||||
|
* @param [parent] The parent token stream, if any.
|
||||||
|
* @return The HTML representation of the token or token stream.
|
||||||
|
*/
|
||||||
|
static stringify(token: TokenStream, language: string, parent?: Array<string | Token>): string;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "@types/prismjs",
|
||||||
|
"version": "1.26.5",
|
||||||
|
"description": "TypeScript definitions for prismjs",
|
||||||
|
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/prismjs",
|
||||||
|
"license": "MIT",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Michael Schmidt",
|
||||||
|
"githubUsername": "RunDevelopment",
|
||||||
|
"url": "https://github.com/RunDevelopment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ExE Boss",
|
||||||
|
"githubUsername": "ExE-Boss",
|
||||||
|
"url": "https://github.com/ExE-Boss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Erik Lieben",
|
||||||
|
"githubUsername": "eriklieben",
|
||||||
|
"url": "https://github.com/eriklieben"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Andre Wiggins",
|
||||||
|
"githubUsername": "andrewiggins",
|
||||||
|
"url": "https://github.com/andrewiggins"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Michał Miszczyszyn",
|
||||||
|
"githubUsername": "typeofweb",
|
||||||
|
"url": "https://github.com/typeofweb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"main": "",
|
||||||
|
"types": "index.d.ts",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
|
||||||
|
"directory": "types/prismjs"
|
||||||
|
},
|
||||||
|
"scripts": {},
|
||||||
|
"dependencies": {},
|
||||||
|
"peerDependencies": {},
|
||||||
|
"typesPublisherContentHash": "cd00995a4fa9467c1dafb0979cd9ea5f3a8e2f89ea83fba6e1b0623b67c0135f",
|
||||||
|
"typeScriptVersion": "4.8"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export type ClassValue = ClassArray | ClassDictionary | string | number | bigint | null | boolean | undefined;
|
||||||
|
export type ClassDictionary = Record<string, any>;
|
||||||
|
export type ClassArray = ClassValue[];
|
||||||
|
|
||||||
|
export function clsx(...inputs: ClassValue[]): string;
|
||||||
|
export default clsx;
|