update big

update
This commit is contained in:
Sultan A 2025-06-03 06:52:29 +07:00
parent 4b251ec50c
commit b507cd40d6
10529 changed files with 1945642 additions and 238 deletions

0
.gitignore vendored Normal file
View File

1
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1 @@
{}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

436
backup/DashboardView.vue Normal file
View File

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

379
backup/main copy.py Normal file
View File

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

321
backup/main.py Normal file
View File

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

File diff suppressed because it is too large Load Diff

BIN
bg/bgpixel3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
bg/bgta_n.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

BIN
bg/pixelbg1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
bg/pixelbg2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
bg/update_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

BIN
chromedriver.exe Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
cleaned_file.xlsx Normal file

Binary file not shown.

103
cleaner_tokens.txt Normal file
View File

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

13
credentials.json Normal file
View File

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

BIN
data_hki16.xlsx Normal file

Binary file not shown.

BIN
data_hki17.xlsx Normal file

Binary file not shown.

BIN
data_hki6.xlsx Normal file

Binary file not shown.

BIN
data_penelitian4.xlsx Normal file

Binary file not shown.

BIN
data_penelitian5.xlsx Normal file

Binary file not shown.

BIN
data_pengabdian1.xlsx Normal file

Binary file not shown.

BIN
data_pengabdian2.xlsx Normal file

Binary file not shown.

BIN
data_pengabdian3.xlsx Normal file

Binary file not shown.

BIN
data_scholar1.xlsx Normal file

Binary file not shown.

BIN
data_scholar2.xlsx Normal file

Binary file not shown.

BIN
data_scopus1.xlsx Normal file

Binary file not shown.

BIN
data_scopus2.xlsx Normal file

Binary file not shown.

BIN
data_scopus3.xlsx Normal file

Binary file not shown.

BIN
data_scopus4.xlsx Normal file

Binary file not shown.

42
dosenTI.txt Normal file
View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

9
frontend/.editorconfig Normal file
View File

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

1
frontend/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

30
frontend/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

8
frontend/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode"
]
}

35
frontend/README.md Normal file
View File

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

19
frontend/eslint.config.js Normal file
View File

@ -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,
]

14
frontend/index.html Normal file
View File

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

8
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

5624
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
frontend/package.json Normal file
View File

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

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
frontend/public/bgta_n.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

3
frontend/src/App.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
<RouterView />
</template>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -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">&times;</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>

14
frontend/src/main.js Normal file
View File

@ -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')

View File

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

View File

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

View File

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

View File

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

View File

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

18
frontend/vite.config.js Normal file
View File

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

12
google_sheets_helper.py Normal file
View File

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

379
main.py Normal file
View File

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

67
node_modules/.package-lock.json generated vendored Normal file
View File

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

21
node_modules/@types/prismjs/LICENSE generated vendored Normal file
View File

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

15
node_modules/@types/prismjs/README.md generated vendored Normal file
View File

@ -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).

4
node_modules/@types/prismjs/components.d.ts generated vendored Normal file
View File

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

19
node_modules/@types/prismjs/components/index.d.ts generated vendored Normal file
View File

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

402
node_modules/@types/prismjs/index.d.ts generated vendored Normal file
View File

@ -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 Prisms 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 youre 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;
}

46
node_modules/@types/prismjs/package.json generated vendored Normal file
View File

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

6
node_modules/clsx/clsx.d.mts generated vendored Normal file
View File

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

Some files were not shown because too many files have changed in this diff Show More