mirror of
https://github.com/YunoHost/yunohost-portal.git
synced 2024-09-03 20:06:23 +02:00
Initial commit
This commit is contained in:
commit
aa8dc12194
11 changed files with 337 additions and 0 deletions
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
17
README.md
Normal file
17
README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
## POC for new YunoHost portal
|
||||
|
||||
- This is a based on [Nuxt v3](https://nuxt.com/)
|
||||
|
||||
### Setup
|
||||
|
||||
- I'm doing all this from inside a YunoHost LXC
|
||||
- You may want to open port 3000 (or disable the firewall) to access the dev server
|
||||
- You'll need NodeJS 18.14 (or higher)
|
||||
- And `yarn`
|
||||
- Run `yarn install`
|
||||
- Also make sure the new yunohost-portal-api is running and corresponding route is in nginx config
|
||||
|
||||
### Dev
|
||||
|
||||
- Run `yarn dev`
|
||||
- Access `http://1.2.3.4:3000/`
|
34
assets/img/logo-white.svg
Normal file
34
assets/img/logo-white.svg
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||
]>
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||
x="0px" y="0px" width="98px" height="85px" viewBox="-0.25 -0.25 98 85"
|
||||
overflow="visible" enable-background="new -0.25 -0.25 98 85" xml:space="preserve">
|
||||
<defs>
|
||||
</defs>
|
||||
<path fill="#FFFFFF" d="M97,51c-2.02,4.98-8.33,5.67-14,7c-0.609,6.29,3.05,10.95-1,16c-6.41-0.26-7.471-5.859-7-13c-1,0-2,0-3,0
|
||||
c-2.09,2.77,0.9,4.52,0,8c-1.12,4.34-7.88,7.91-11,7c-2.18-0.641-5.96-6.63-5-12c2.82-2.71,2.76,3.12,6,3c5.05-7.84-9.63-8.55-8-17
|
||||
c1.24-6.42,11.66-9.66,15-1c1.54,4.21-5.17,0.16-5,3c-0.279,1.62,0.95,1.72,1,3c2.52,0.77,1.68-2.16,3-3c1.859-1.17,3.09-0.75,6-1
|
||||
c2.45-2.55,1.08-8.92,4-11c3.87,0.46,6.08,2.59,6,7C91.01,46.109,94.3,46.05,97,51z"/>
|
||||
<path fill="#FFFFFF" d="M87,13c0.609,3.21,2.32,4.98,2,8c-0.34,3.21-2.9,8.83-4,9c-1.17,0.18-1.34,1.78-2,2
|
||||
c-4.66,1.57-12.391-1.48-14-7c-1.16-3.97,1.9-13.37,4-17c1.3-2.25,1.221-2.99,5-4c2.41-0.65,3.65-2.25,6,0
|
||||
c0.471,0.45,1.3,0.49,1.85,0.89c-0.199,0,2,3.14,2.15,4.11C88.32,11.07,86.77,11.78,87,13z M79,22c1.779-1.89,3.29-4.04,3-8
|
||||
C77.49,12.33,74.67,21.3,79,22z"/>
|
||||
<path fill="#FFFFFF" d="M67,21c-0.07,5.81,2.48,10.7,0,15c-6.73,1.06-7.24-4.1-11-6c-1.939,1.39-1.49,5.18-3,7
|
||||
c-3.78,0.44-4.69-1.97-7-3c2.47-7.81,1.26-18.98,2-26c8.58-0.58,7.68,8.32,12,12c0.52-4.34-0.359-15.52,3-20
|
||||
C70.33,3.29,67.09,12.99,67,21z"/>
|
||||
<path fill="#FFFFFF" d="M52,55c1.93,8.41,0.12,22.689-12,20c-1.59-0.35-8.42-5.22-9-7c-1.62-5,0.34-13.34,3-16
|
||||
C39.03,46.97,45.48,50.359,52,55z M39,66c4.55,0.96,6.3-4.2,4-7C39.37,59.03,38.61,61.939,39,66z"/>
|
||||
<path fill="#FFFFFF" d="M39,8c5.58,0.9,6.4,6.81,5,15c-1.43,8.38-3.02,14.59-9,15c-9.57,0.65-12.25-16.69-9-29
|
||||
c8.32,1.27,6.59,10.36,6,17c2.71,0.83,2.2-0.85,3-2C37.05,21.04,37.82,13.61,39,8z"/>
|
||||
<path fill="#FFFFFF" d="M28,62c0.1,5.67,4.4,11.33,2,17c-4.32-1.01-6.57-4.09-9-7c-3.15-0.48-2.26,3.07-6,2
|
||||
c-0.67,5.061,2.29,7.57-1,10c-4.7-0.63-6.66-4-8-8c-2.61-1.38-5.48-2.52-6-6c0.14-3.53,4.48-2.85,7-4c0.47-5.53-1.41-13.41,2-16
|
||||
c8.31,0.49,8.21,7.13,7,15c4.36,0.29,4.94-4.35,5-7c0.06-2.43-1.82-8.26,2-11c3.06-0.73,2.94,1.73,6,1
|
||||
C32.35,52.7,27.92,57.439,28,62z"/>
|
||||
<path fill="#FFFFFF" d="M24,12c1.07,7.07-3.86,8.14-6,12c0.21,6.88-0.47,12.86-2,18c-5.86-1.32-8.7-10.38-6-17
|
||||
c-0.33-3.52-5.26-4.22-7-8c-0.3-0.66-0.47-4.43-1-7C1.09,5.63,0.55,4.31,3,1c8.16-0.49,7.21,8.13,9,14c5.05,0.39,3.91-5.42,8-6
|
||||
C20.98,10.35,22.67,11,24,12z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
2
composables/states.ts
Normal file
2
composables/states.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const useApiEndpoint = () => "https://" + window.location.hostname + '/yunohost/portalapi'
|
||||
export const useIsLoggedIn = () => useState<boolean>('isLoggedIn', () => false)
|
31
layouts/default.vue
Normal file
31
layouts/default.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div class="p-10 min-h-screen">
|
||||
<div class="container mx-auto p-4">
|
||||
<slot />
|
||||
</div>
|
||||
<footer class="container fixed bottom-10 mx-10 pr-10 text-gray-400">
|
||||
<slot name="footer">
|
||||
<div class="flex flex-row items-end">
|
||||
<!-- FIXME: wrap this in an if: connected somehow ? -->
|
||||
<nav class="grow space-x-5 border-t mr-10 border-gray-500">
|
||||
<a>Edit my profile</a>
|
||||
<a href="//yunohost.org/docs" target="_blank">Documentation</a>
|
||||
<a href="//yunohost.org/help" target="_blank">Help</a>
|
||||
<a href="/yunohost/admin/" target="_blank">Administration</a>
|
||||
</nav>
|
||||
<img class="flex-none mr-10" src="/assets/img/logo-white.svg" />
|
||||
</div>
|
||||
</slot>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Source Sans 3';
|
||||
@apply bg-gray-700 text-gray-200;
|
||||
}
|
||||
.btn {
|
||||
@apply py-2 px-4 rounded;
|
||||
}
|
||||
</style>
|
15
nuxt.config.ts
Normal file
15
nuxt.config.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
ssr: false,
|
||||
modules: [
|
||||
'@nuxtjs/tailwindcss',
|
||||
'nuxt-icon',
|
||||
"@nuxtjs/google-fonts",
|
||||
],
|
||||
devtools: { enabled: true },
|
||||
googleFonts: {
|
||||
families: {
|
||||
'Source+Sans+3': [500, 900]
|
||||
}
|
||||
}
|
||||
})
|
18
package.json
Normal file
18
package.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/devtools": "latest",
|
||||
"@nuxtjs/google-fonts": "^3.0.1",
|
||||
"@nuxtjs/tailwindcss": "^5.3.5",
|
||||
"@types/node": "^18.16.19",
|
||||
"nuxt": "^3.6.2",
|
||||
"nuxt-icon": "^0.4.2"
|
||||
}
|
||||
}
|
117
pages/index.vue
Normal file
117
pages/index.vue
Normal file
|
@ -0,0 +1,117 @@
|
|||
<template #main>
|
||||
<div>
|
||||
<div class="flex flex-row items-center min-w-full">
|
||||
<span class="flex-none pr-5 ">
|
||||
<Icon name="mdi:account-circle" size="5em" class="text-gray-500" />
|
||||
</span>
|
||||
|
||||
<div class="grow">
|
||||
<h2 class="text-2xl font-extrabold leading-none tracking-tight">{{ me.username }}</h2>
|
||||
<h3>{{ me.fullname }}</h3>
|
||||
<h4 class="opacity-50">{{ me.mail }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="flex-none">
|
||||
<button class="btn bg-gray-800" @click.prevent="logout">
|
||||
<Icon name="mdi:logout" class="text-gray-500" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="apps" class="p-10">
|
||||
|
||||
<div v-if="Object.keys(me.apps).length == 0">
|
||||
<em class="text-gray-400">There is no app to list here, either because no web app yet is installed on the server, or because you don't have access to any. Please check with the admins of the server for more infos!</em>
|
||||
</div>
|
||||
|
||||
<ul class="flex space-x-4">
|
||||
<!-- NB : because of the usage of dynamic colors, gotta force tailwind to expose those, cf 'safelisting' -->
|
||||
<li v-for="app in me.apps" :class="'text-center leading-none p-5 card h-32 w-32 bg-' + app.color + '-500'">
|
||||
<a>
|
||||
<div class="text-6xl font-extrabold">{{ app.label.substring(0, 2) }}</div>
|
||||
<span class="leading-tight">{{ app.label }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!--
|
||||
<li class="text-center leading-none p-5 card h-32 w-32 bg-rose-500">
|
||||
<a class="">
|
||||
<div class="text-6xl font-extrabold">My</div>
|
||||
<span class="leading-tight">My Webapp</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="text-center leading-none p-5 card h-32 w-32 bg-indigo-500">
|
||||
<a class="">
|
||||
<div class="text-6xl font-extrabold">Ne</div>
|
||||
<span class="">Nextcloud</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="text-center leading-none p-5 card h-32 w-32 bg-yellow-500">
|
||||
<a class="">
|
||||
<div class="text-6xl font-extrabold">Ra</div>
|
||||
<span class="">Rainloop</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="text-center leading-none p-5 card h-32 w-32 bg-green-500">
|
||||
<a class="">
|
||||
<div class="text-6xl font-extrabold">Et</div>
|
||||
<span class="">Etherpad MyPads</span>
|
||||
</a>
|
||||
</li>
|
||||
-->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const apiEndpoint = useApiEndpoint()
|
||||
let isLoggedIn = useIsLoggedIn()
|
||||
let me = {};
|
||||
|
||||
const { data, error } = await useFetch(
|
||||
apiEndpoint + "/me",
|
||||
{credentials: 'include'}
|
||||
)
|
||||
|
||||
if (error.value && error.value.statusCode >= 400) {
|
||||
|
||||
isLoggedIn = false; // FIXME : not confident this actually mutates the state ...
|
||||
// FIXME : we probably want different handlings between 401/403, 500, 502, ...
|
||||
await navigateTo('/login')
|
||||
}
|
||||
else
|
||||
{
|
||||
me = data.value
|
||||
|
||||
Object.keys(me.apps).map(function(app_id, index) {
|
||||
var app_tile_colors = ['red', 'orange', 'yellow', 'lime', 'green', 'teal', 'indigo', 'sky', 'purple', 'rose']
|
||||
var randomColorNumber = parseInt(me.apps[app_id].label, 36) % app_tile_colors.length;
|
||||
me.apps[app_id].color = app_tile_colors[randomColorNumber]
|
||||
})
|
||||
}
|
||||
|
||||
console.log(me.apps)
|
||||
|
||||
async function logout() {
|
||||
|
||||
const { data, error } = await useFetch(apiEndpoint + "/logout", {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (error.value && error.value.statusCode != 200) {
|
||||
// FIXME : display an error or something
|
||||
}
|
||||
else {
|
||||
// FIXME : meh, turns out the cookie is still valid after successfully calling the route for some reason ... !?
|
||||
|
||||
isLoggedIn = false; // FIXME : not confident this actually mutates the state ...
|
||||
await navigateTo('/login')
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
</script>
|
67
pages/login.vue
Normal file
67
pages/login.vue
Normal file
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<div class="w-80 mx-auto pt-20">
|
||||
<img class="flex-none mx-auto w-1/2 p-5" src="/assets/img/logo-white.svg" />
|
||||
<form method="POST" @submit.prevent="login">
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-1/6">
|
||||
<label class="pl-3" for="login-username">
|
||||
<Icon name="mdi:account-circle" size="2em" class="text-gray-400" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="">
|
||||
<input class="text-gray-700 rounded py-2 px-4" id="login-username" type="text" placeholder="username" v-model="form.username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="w-1/6">
|
||||
<label class="pl-3" for="login-password">
|
||||
<Icon name="mdi:lock" size="2em" class="text-gray-400" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="">
|
||||
<input class="text-gray-700 rounded py-2 px-4" id="login-password" type="password" placeholder="******************" v-model="form.password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-full">
|
||||
<button class="w-full bg-indigo-500 hover:bg-indigo-400 font-bold py-2 px-4 rounded" type="submit">
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- <template #footer></template> -->
|
||||
|
||||
<script setup>
|
||||
const apiEndpoint = useApiEndpoint();
|
||||
let isLoggedIn = useIsLoggedIn();
|
||||
|
||||
let form = {
|
||||
username: "",
|
||||
password: ""
|
||||
}
|
||||
|
||||
async function login() {
|
||||
|
||||
const { data, error } = await useFetch(apiEndpoint + "/login", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": ""
|
||||
},
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: { credentials: form.username + ":" + form.password }
|
||||
});
|
||||
|
||||
if (error.value && error.value.statusCode != 200) {
|
||||
// FIXME : display an error or something
|
||||
}
|
||||
else {
|
||||
isLoggedIn = true; // FIXME : not confident this actually mutates the state ...
|
||||
await navigateTo('/')
|
||||
}
|
||||
};
|
||||
</script>
|
9
tailwind.config.js
Normal file
9
tailwind.config.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
// Safelisting some classes to avoid content purge
|
||||
safelist: [
|
||||
'safelisted',
|
||||
{
|
||||
pattern: /bg-.*-500/,
|
||||
},
|
||||
]
|
||||
}
|
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
Loading…
Reference in a new issue