Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
dtd
/
civitas.ui
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit f46013e1
authored
Apr 28, 2025
by
Samuel Taniel Mulyadi
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'sam' into 'staging'
Sam See merge request
!22
2 parents
e5f2a991
753145f4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
111 additions
and
83 deletions
components/beranda/UserLib.vue
layouts/components/DefaultLayoutWithHorizontalNav.vue
middleware/acl.global.ts
plugins/keycloak.ts
components/beranda/UserLib.vue
View file @
f46013e
<
script
setup
lang=
"ts"
>
import
{
useKeycloakStore
}
from
"@/@core/stores/keycloakStore"
;
import
{
ref
}
from
"vue"
;
import
{
ref
,
computed
,
onMounted
}
from
"vue"
;
// Store Keycloak
const
keycloakStore
=
useKeycloakStore
();
...
...
@@ -10,6 +11,7 @@ const items = ref<any[]>([]);
const
loading
=
ref
(
false
);
const
searchQuery
=
ref
(
""
);
// Header tabel
const
logHeaders
=
[
{
title
:
"KOLEKSI"
,
key
:
"koleksi"
},
{
title
:
"TANGGAL PEMINJAMAN"
,
key
:
"tglpinjam"
},
...
...
@@ -17,13 +19,12 @@ const logHeaders = [
{
title
:
"DIPINJAM"
,
key
:
"dipinjam"
},
{
title
:
"PERPANJANGAN"
,
key
:
"perpanjangan"
},
{
title
:
"DENDA"
,
key
:
"denda"
},
{
title
:
"DENDA TOTAL"
,
key
:
"dendatot
o
al"
},
{
title
:
"DENDA TOTAL"
,
key
:
"dendatotal"
},
];
// Fungsi ambil data
async
function
getData
()
{
loading
.
value
=
true
;
items
.
value
=
[];
try
{
const
apiEndpoint
=
"https://api.ui.ac.id/my/lib"
;
const
response
=
await
fetch
(
apiEndpoint
,
{
...
...
@@ -31,16 +32,24 @@ async function getData() {
Authorization
:
`Bearer
${
keycloakStore
.
accessToken
}
`
,
},
});
if
(
!
response
.
ok
)
throw
new
Error
(
"Gagal mengambil data"
);
const
dataku
=
await
response
.
json
();
// Tambahkan properti tanggal, namaHari, mulai aktual, dan selesai aktual ke setiap item
items
.
value
=
dataku
.
map
((
item
:
any
)
=>
{
return
{
...
item
,
};
});
// dataku adalah array, tiap elemen punya properti root-level
const
requiredKeys
=
[
"denda"
,
"dendatotal"
,
"dipinjam"
,
"koleksi"
,
"perpanjangan"
,
"tglkembali"
,
"tglpinjam"
,
"username"
,
];
// ambil hanya objek yang memiliki semua key di root
items
.
value
=
Array
.
isArray
(
dataku
)
?
dataku
.
filter
((
r
:
any
)
=>
requiredKeys
.
every
(
k
=>
k
in
r
))
:
[];
}
catch
(
err
)
{
console
.
error
(
"Gagal mengambil data:"
,
err
);
}
finally
{
...
...
@@ -48,22 +57,32 @@ async function getData() {
}
}
// Panggil saat komponen mount
onMounted
(()
=>
{
getData
();
});
// Filter hasil pencarian
const
filteredItems
=
computed
(()
=>
{
if
(
!
searchQuery
.
value
)
return
items
.
value
;
const
query
=
searchQuery
.
value
.
toLowerCase
();
return
items
.
value
.
filter
((
item
)
=>
const
q
=
searchQuery
.
value
.
toLowerCase
();
return
items
.
value
.
filter
(
item
=>
logHeaders
.
some
(
(
header
)
=>
item
[
header
.
key
]
&&
String
(
item
[
header
.
key
]).
toLowerCase
().
includes
(
query
)
h
=>
item
[
h
.
key
]
&&
String
(
item
[
h
.
key
]).
toLowerCase
().
includes
(
q
)
)
);
});
</
script
>
<
template
>
<VCard
title=
"Status Peminjaman Buku"
class=
"recentnamaHariCard"
>
<div
class=
"search-container mb-4 pl-2 pr-2"
>
<AppCardActions
:title=
"`Status Peminjaman Buku`"
class=
"jadwalShift"
action-collapsed
action-remove
>
<!-- Search Input -->
<div
class=
"search-container mb-4 pl-2 pr-2"
>
<VTextField
v-model=
"searchQuery"
label=
"Search"
...
...
@@ -76,6 +95,7 @@ const filteredItems = computed(() => {
outlined
/>
</div>
<VDataTable
:headers=
"logHeaders"
:items=
"filteredItems"
...
...
@@ -85,5 +105,52 @@ const filteredItems = computed(() => {
:sort-by=
"['tglpinjam']"
:sort-asc=
"[true]"
></VDataTable>
</VCard>
</AppCardActions>
</
template
>
<
style
lang=
"scss"
>
.jadwalShift
{
.v-table
{
&--density-default
{
.v-table__wrapper
{
table
{
thead
{
th
{
background-color
:
#f5f5f5
;
border-block-end
:
2px
solid
#ddd
;
color
:
#2c2c2c
;
font-weight
:
600
;
padding-block
:
12px
;
padding-inline
:
1.5em
;
}
}
tbody
{
tr
{
td
{
border-block-end
:
1px
solid
#eee
;
min-block-size
:
auto
;
padding-block
:
8px
;
padding-inline
:
1em
;
vertical-align
:
top
;
&.vti-table__td--Jadwal
{
color
:
#4a4a4a
;
font-weight
:
500
;
}
}
}
}
}
}
}
}
}
.search-container
{
display
:
flex
;
justify-content
:
flex-end
;
}
</
style
>
\ No newline at end of file
layouts/components/DefaultLayoutWithHorizontalNav.vue
View file @
f46013e
<
script
lang=
"ts"
setup
>
import
{
HorizontalNavLayout
}
from
'@layouts'
import
{
computed
,
onMounted
,
onUnmounted
,
ref
}
from
'vue'
import
{
useKeycloakStore
}
from
'@/@core/stores/keycloakStore'
import
keycloakInstance
from
'@/keycloak'
import
Footer
from
'@/layouts/components/Footer.vue'
import
NavbarThemeSwitcher
from
'@/layouts/components/NavbarThemeSwitcher.vue'
import
NavbarTokenExpiredTime
from
'@/layouts/components/NavbarTokenExpiredTime.vue'
import
UserProfile
from
'@/layouts/components/UserProfile.vue'
import
navItems
from
'@/navigation/horizontal'
import
{
HorizontalNavLayout
}
from
'@layouts'
// Keycloak & state
const
keycloakStore
=
useKeycloakStore
()
const
authenticated
=
computed
(()
=>
keycloakStore
.
authenticated
)
const
now
=
ref
(
Math
.
floor
(
Date
.
now
()
/
1000
))
const
tokenLifetime
=
ref
(
0
)
let
timer
:
ReturnType
<
typeof
setInterval
>
onMounted
(()
=>
{
// Set tokenLifetime only once on mount
if
(
keycloakInstance
.
tokenParsed
?.
exp
&&
keycloakInstance
.
tokenParsed
?.
iat
)
tokenLifetime
.
value
=
keycloakInstance
.
tokenParsed
.
exp
-
keycloakInstance
.
tokenParsed
.
iat
// Start timer
timer
=
setInterval
(()
=>
{
now
.
value
=
Math
.
floor
(
Date
.
now
()
/
1000
)
},
1000
)
})
onUnmounted
(()
=>
{
clearInterval
(
timer
)
})
// Computed expiration values
const
computedExpIn
=
computed
(()
=>
{
return
authenticated
.
value
&&
keycloakInstance
.
tokenParsed
?.
exp
?
Math
.
max
(
keycloakInstance
.
tokenParsed
?.
exp
-
now
.
value
,
0
)
:
0
})
</
script
>
<
template
>
...
...
@@ -77,23 +44,7 @@ const computedExpIn = computed(() => {
<NavbarThemeSwitcher
/>
<!--
<NavbarShortcuts
/>
-->
<!--
<NavBarNotifications
class=
"me-2"
/>
-->
<!--
<div
class=
"pa-4"
>
<VAlert
v-if=
"authenticated"
type=
"info"
border=
"start"
variant=
"outlined"
class=
"mb-4"
>
<h3
class=
"text-h6 font-weight-bold"
>
Access token expires in
{{
computedExpIn
}}
sec,
Refresh token in
{{
computedRefExpIn
}}
sec,
Session:
{{
computedSessionTimer
}}
</h3>
</VAlert>
</div>
-->
<UserProfile
/>
</
template
>
...
...
middleware/acl.global.ts
View file @
f46013e
import
{
defineNuxtRouteMiddleware
,
navigateTo
}
from
"nuxt/app"
import
{
useKeycloakStore
}
from
"~/@core/stores/keycloakStore"
import
{
defineNuxtRouteMiddleware
,
navigateTo
}
from
'nuxt/app'
import
{
useKeycloakStore
}
from
'~/@core/stores/keycloakStore'
export
default
defineNuxtRouteMiddleware
(
(
to
)
=>
{
export
default
defineNuxtRouteMiddleware
(
to
=>
{
const
keycloakStore
=
useKeycloakStore
()
if
(
process
.
client
)
{
const
isLoginPage
=
to
.
path
===
'/login'
// Jika belum login dan bukan sedang menuju halaman login, redirect ke login
if
(
!
keycloakStore
.
authenticated
&&
!
isLoginPage
)
{
if
(
!
keycloakStore
.
authenticated
&&
!
isLoginPage
)
return
navigateTo
(
'/login'
)
}
// Kalau sudah login dan mencoba ke /login, bisa arahkan ke dashboard atau home
if
(
keycloakStore
.
authenticated
&&
isLoginPage
)
{
return
navigateTo
(
'/'
)
// atau ke '/dashboard' misalnya
}
if
(
keycloakStore
.
authenticated
&&
isLoginPage
)
return
navigateTo
(
'/profile'
)
// atau ke '/dashboard' misalnya
}
})
\ No newline at end of file
})
plugins/keycloak.ts
View file @
f46013e
...
...
@@ -4,9 +4,18 @@ import keycloakInstance from '@/keycloak'
export
default
defineNuxtPlugin
(
async
nuxtApp
=>
{
const
keycloakStore
=
useKeycloakStore
()
/* `const { } = nuxtApp` is a destructuring assignment in JavaScript/TypeScript. In this
case, it is extracting the `` property from the `nuxtApp` object and assigning it to a new
constant named ``. This allows you to access the `` object directly without having
to use `nuxtApp.` every time. It's a shorthand way of accessing nested properties in
objects. */
// const { $router } = nuxtApp
try
{
const
authenticated
=
await
keycloakInstance
.
init
({
onLoad
:
'check-sso'
,
checkLoginIframe
:
true
,
checkLoginIframeInterval
:
5
,
// in seconds, default is 5
})
keycloakStore
.
authenticated
=
authenticated
...
...
@@ -14,7 +23,9 @@ export default defineNuxtPlugin(async nuxtApp => {
if
(
authenticated
)
{
keycloakStore
.
refresh
()
console
.
log
(
'User is authenticated'
)
navigateTo
(
'/profile'
)
// $router.push('/profile')
// console.log('Suppose to navigate To')
setInterval
(()
=>
{
const
now
=
Math
.
floor
(
Date
.
now
()
/
1000
)
...
...
@@ -25,13 +36,14 @@ export default defineNuxtPlugin(async nuxtApp => {
keycloakInstance
.
logout
({
redirectUri
:
`
${
window
.
location
.
origin
}
/login`
})
}
else
{
console
.
log
(
'Token expires in:'
,
tokenExp
-
now
,
'seconds'
)
//
console.log('Token expires in:', tokenExp - now, 'seconds')
}
},
10
_000
)
}
else
{
console
.
log
(
'User is not authenticated'
)
navigateTo
(
'/login'
)
// $router.push('/login')
}
}
catch
(
error
)
{
...
...
Write
Preview
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment