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