initial commit
Can connect with Discord, login, logout, and delete account
This commit is contained in:
38
src/App.vue
Normal file
38
src/App.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import AppHeader from './components/AppHeader.vue';
|
||||
import AppFooter from './components/AppFooter.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppHeader />
|
||||
<main class="flex-col-center gap-3rem">
|
||||
<RouterView />
|
||||
</main>
|
||||
<AppFooter />
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
main {
|
||||
justify-content: flex-start;
|
||||
margin: 5rem;
|
||||
align-content: center;
|
||||
padding-top: 4.5rem !important;
|
||||
padding-bottom: 7rem !important;
|
||||
}
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
@media (max-width: 1000px) {
|
||||
main {
|
||||
margin: 3% 5%;
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
src/assets/_layouts.less
Normal file
24
src/assets/_layouts.less
Normal file
@@ -0,0 +1,24 @@
|
||||
.hero {
|
||||
display: block;
|
||||
padding: 3rem 50%;
|
||||
}
|
||||
|
||||
.two-col-img-text {
|
||||
// must contain a <figure> or <img> and something else.
|
||||
.flex-row-center;
|
||||
padding: 1rem;
|
||||
gap: 4rem;
|
||||
width: 100%;
|
||||
|
||||
* {
|
||||
grid-area: text;
|
||||
}
|
||||
|
||||
figure,
|
||||
img {
|
||||
grid-area: figure;
|
||||
max-width: 100%;
|
||||
max-height: 10rem;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
}
|
||||
64
src/assets/_mixins.less
Normal file
64
src/assets/_mixins.less
Normal file
@@ -0,0 +1,64 @@
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
.flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
.flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-row-center {
|
||||
.flex-row;
|
||||
.flex-center;
|
||||
}
|
||||
|
||||
.flex-col-center {
|
||||
.flex-col;
|
||||
.flex-center;
|
||||
}
|
||||
|
||||
.flex-spread {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.themed(@property, @light, @dark) {
|
||||
@{property}: @light;
|
||||
html.dark & {
|
||||
@{property}: @dark;
|
||||
}
|
||||
}
|
||||
|
||||
.gen-gap(@n, @i: 1) when (@i =< @n) {
|
||||
.gap-@{i}rem {
|
||||
gap: (@i * 1rem);
|
||||
}
|
||||
|
||||
.gen-gap(@n, (@i + 1));
|
||||
}
|
||||
.gen-gap(10);
|
||||
|
||||
.gen-grid-two-col-width(@right: 10, @left: 90) when (@right =< 90) {
|
||||
.two-col-@{left}-@{right} {
|
||||
grid-template-columns: (@left * 1%) (@right * 1%);
|
||||
}
|
||||
|
||||
.gen-grid-two-col-width((@right + 10), (@left - 10));
|
||||
}
|
||||
.gen-grid-two-col-width();
|
||||
|
||||
.gen-margin(@n, @i: 1) when (@i =< @n) {
|
||||
.margin-@{i}rem {
|
||||
margin: (@i * 1rem);
|
||||
}
|
||||
.gen-margin(@n, (@i + 1));
|
||||
}
|
||||
.gen-margin(10);
|
||||
167
src/assets/_reset.less
Normal file
167
src/assets/_reset.less
Normal file
@@ -0,0 +1,167 @@
|
||||
// From https://byby.dev/normalize-css on 2024-01-22
|
||||
|
||||
html {
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kdb,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none;
|
||||
text-decoration: underline;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
sub {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
appearance: button;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button:-moz-focusring,
|
||||
[type='button']:-moz-focusring,
|
||||
[type='reset']:-moz-focusring,
|
||||
[type='submit']:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
legend {
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
display: table;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[type='checkbox'],
|
||||
[type='radio'] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[type='number']::-webkit-inner-spin-button,
|
||||
[type='number']::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type='search'] {
|
||||
appearance: textfield;
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type='search']::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
165
src/assets/_theme.less
Normal file
165
src/assets/_theme.less
Normal file
@@ -0,0 +1,165 @@
|
||||
@import '_mixins';
|
||||
|
||||
@light-text: #081611; // 925
|
||||
@light-background: #e4f6ed; // 75
|
||||
@light-primary: #276d4f; // 750
|
||||
@light-secondary: #8cd1d4; // 350
|
||||
@light-accent: #4195af; // 550
|
||||
|
||||
@light-text-50: #ecf8f4;
|
||||
@light-text-100: #daf1e9;
|
||||
@light-text-200: #b4e4d3;
|
||||
@light-text-300: #8fd6bd;
|
||||
@light-text-400: #69c9a7;
|
||||
@light-text-500: #44bb91;
|
||||
@light-text-600: #369674;
|
||||
@light-text-700: #297057;
|
||||
@light-text-800: #1b4b3a;
|
||||
@light-text-900: #0e251d;
|
||||
@light-text-950: #07130f;
|
||||
|
||||
@light-background-50: #ecf9f2;
|
||||
@light-background-100: #d9f2e6;
|
||||
@light-background-200: #b3e6cc;
|
||||
@light-background-300: #8cd9b3;
|
||||
@light-background-400: #66cc99;
|
||||
@light-background-500: #40bf80;
|
||||
@light-background-600: #339966;
|
||||
@light-background-700: #26734d;
|
||||
@light-background-800: #194d33;
|
||||
@light-background-900: #0d261a;
|
||||
@light-background-950: #06130d;
|
||||
|
||||
@light-primary-50: #ecf8f3;
|
||||
@light-primary-100: #daf1e7;
|
||||
@light-primary-200: #b4e4cf;
|
||||
@light-primary-300: #8fd6b7;
|
||||
@light-primary-400: #69c99f;
|
||||
@light-primary-500: #44bb87;
|
||||
@light-primary-600: #36966c;
|
||||
@light-primary-700: #297051;
|
||||
@light-primary-800: #1b4b36;
|
||||
@light-primary-900: #0e251b;
|
||||
@light-primary-950: #07130e;
|
||||
|
||||
@light-secondary-50: #ecf8f8;
|
||||
@light-secondary-100: #daf0f1;
|
||||
@light-secondary-200: #b5e1e3;
|
||||
@light-secondary-300: #8fd2d6;
|
||||
@light-secondary-400: #6ac3c8;
|
||||
@light-secondary-500: #45b4ba;
|
||||
@light-secondary-600: #379095;
|
||||
@light-secondary-700: #296c70;
|
||||
@light-secondary-800: #1c484a;
|
||||
@light-secondary-900: #0e2425;
|
||||
@light-secondary-950: #071213;
|
||||
|
||||
@light-accent-50: #ecf5f8;
|
||||
@light-accent-100: #daecf1;
|
||||
@light-accent-200: #b5d9e3;
|
||||
@light-accent-300: #8fc5d6;
|
||||
@light-accent-400: #6ab2c8;
|
||||
@light-accent-500: #459fba;
|
||||
@light-accent-600: #377f95;
|
||||
@light-accent-700: #295f70;
|
||||
@light-accent-800: #1c404a;
|
||||
@light-accent-900: #0e2025;
|
||||
@light-accent-950: #071013;
|
||||
|
||||
@light-gray-10: rgba(black, 0.1);
|
||||
@light-gray-20: rgba(black, 0.2);
|
||||
@light-gray-30: rgba(black, 0.3);
|
||||
@light-gray-40: rgba(black, 0.4);
|
||||
@light-gray-50: rgba(black, 0.5);
|
||||
@light-gray-60: rgba(black, 0.6);
|
||||
@light-gray-70: rgba(black, 0.7);
|
||||
@light-gray-80: rgba(black, 0.8);
|
||||
@light-gray-90: rgba(black, 0.9);
|
||||
@light-gray-100: rgba(black, 1);
|
||||
|
||||
@dark-text: #e9f7f2; // 1000
|
||||
@dark-background: #091b12; // 75
|
||||
@dark-primary: #92d8ba;
|
||||
@dark-secondary: #2b6f73;
|
||||
@dark-accent: #50a5be;
|
||||
|
||||
@dark-text-50: #07130f;
|
||||
@dark-text-100: #0e251d;
|
||||
@dark-text-200: #1b4b3a;
|
||||
@dark-text-300: #297057;
|
||||
@dark-text-400: #369674;
|
||||
@dark-text-500: #44bb91;
|
||||
@dark-text-600: #69c9a7;
|
||||
@dark-text-700: #8fd6bd;
|
||||
@dark-text-800: #b4e4d3;
|
||||
@dark-text-900: #daf1e9;
|
||||
@dark-text-950: #ecf8f4;
|
||||
|
||||
@dark-background-50: #06130d;
|
||||
@dark-background-100: #0d261a;
|
||||
@dark-background-200: #194d33;
|
||||
@dark-background-300: #26734d;
|
||||
@dark-background-400: #339966;
|
||||
@dark-background-500: #40bf80;
|
||||
@dark-background-600: #66cc99;
|
||||
@dark-background-700: #8cd9b3;
|
||||
@dark-background-800: #b3e6cc;
|
||||
@dark-background-900: #d9f2e6;
|
||||
@dark-background-950: #ecf9f2;
|
||||
|
||||
@dark-primary-50: #07130e;
|
||||
@dark-primary-100: #0e251b;
|
||||
@dark-primary-200: #1b4b36;
|
||||
@dark-primary-300: #297051;
|
||||
@dark-primary-400: #36966c;
|
||||
@dark-primary-500: #44bb87;
|
||||
@dark-primary-600: #69c99f;
|
||||
@dark-primary-700: #8fd6b7;
|
||||
@dark-primary-800: #b4e4cf;
|
||||
@dark-primary-900: #daf1e7;
|
||||
@dark-primary-950: #ecf8f3;
|
||||
|
||||
@dark-secondary-50: #071213;
|
||||
@dark-secondary-100: #0e2425;
|
||||
@dark-secondary-200: #1c484a;
|
||||
@dark-secondary-300: #296c70;
|
||||
@dark-secondary-400: #379095;
|
||||
@dark-secondary-500: #45b4ba;
|
||||
@dark-secondary-600: #6ac3c8;
|
||||
@dark-secondary-700: #8fd2d6;
|
||||
@dark-secondary-800: #b5e1e3;
|
||||
@dark-secondary-900: #daf0f1;
|
||||
@dark-secondary-950: #ecf8f8;
|
||||
|
||||
@dark-accent-50: #071013;
|
||||
@dark-accent-100: #0e2025;
|
||||
@dark-accent-200: #1c404a;
|
||||
@dark-accent-300: #295f70;
|
||||
@dark-accent-400: #377f95;
|
||||
@dark-accent-500: #459fba;
|
||||
@dark-accent-600: #6ab2c8;
|
||||
@dark-accent-700: #8fc5d6;
|
||||
@dark-accent-800: #b5d9e3;
|
||||
@dark-accent-900: #daecf1;
|
||||
@dark-accent-950: #ecf5f8;
|
||||
|
||||
@dark-gray-10: rgba(white, 0.1);
|
||||
@dark-gray-20: rgba(white, 0.2);
|
||||
@dark-gray-30: rgba(white, 0.3);
|
||||
@dark-gray-40: rgba(white, 0.4);
|
||||
@dark-gray-50: rgba(white, 0.5);
|
||||
@dark-gray-60: rgba(white, 0.6);
|
||||
@dark-gray-70: rgba(white, 0.7);
|
||||
@dark-gray-80: rgba(white, 0.8);
|
||||
@dark-gray-90: rgba(white, 0.9);
|
||||
@dark-gray-100: rgba(white, 1);
|
||||
|
||||
html {
|
||||
background-color: @light-background;
|
||||
color: @light-text;
|
||||
|
||||
&.dark {
|
||||
background-color: @dark-background;
|
||||
color: @dark-text;
|
||||
}
|
||||
}
|
||||
80
src/assets/components/buttons.less
Normal file
80
src/assets/components/buttons.less
Normal file
@@ -0,0 +1,80 @@
|
||||
@import 'titles';
|
||||
@import '../_mixins';
|
||||
|
||||
button,
|
||||
.button {
|
||||
background-color: @light-primary;
|
||||
color: @light-background;
|
||||
border: none;
|
||||
padding: 0.7rem 1.2rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
border-radius: 10rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
|
||||
.themed(background-color, @light-primary, @dark-primary);
|
||||
.themed(color, @light-background, @dark-background);
|
||||
|
||||
&:hover {
|
||||
transition: all 0.2s ease-in-out;
|
||||
.themed(box-shadow,
|
||||
0.1rem 0.1rem 2rem rgba(@light-accent, 0.8),
|
||||
0.1rem 0.1rem 2rem rgba(@dark-accent, 0.8));
|
||||
}
|
||||
&.no-shadow {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.accent {
|
||||
background-color: @light-accent;
|
||||
html.dark & {
|
||||
background-color: @dark-accent;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
.themed(background-color, @light-secondary, @dark-secondary);
|
||||
.themed(color, @light-text, @dark-text);
|
||||
}
|
||||
|
||||
&.faded {
|
||||
.themed(background-color, @light-background-300, @dark-background-800);
|
||||
.themed(color, @light-text, @dark-background);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.themed(background-color, @light-background-100, @dark-background-100);
|
||||
.themed(color, @light-text-600, @dark-text-600);
|
||||
}
|
||||
|
||||
&.h2 {
|
||||
padding: (@h2-size / 2) (@h2-size * (2 / 3));
|
||||
}
|
||||
|
||||
&.h3 {
|
||||
padding: (@h3-size / 2) (@h3-size * (2 / 3));
|
||||
}
|
||||
|
||||
&.h4 {
|
||||
padding: (@h4-size / 2) (@h4-size * (2 / 3));
|
||||
}
|
||||
|
||||
header & {
|
||||
background-color: inherit !important;
|
||||
border-radius: 0.5rem;
|
||||
.themed(border, solid @light-gray-10 1px, solid @dark-gray-10 1px);
|
||||
|
||||
&:hover {
|
||||
.themed(box-shadow,
|
||||
0.1rem 0.1rem 0.5rem @light-gray-10,
|
||||
0.1rem 0.1rem 0.5rem @dark-gray-10);
|
||||
}
|
||||
}
|
||||
|
||||
&.raise:hover {
|
||||
transform: translate(0, -3px);
|
||||
}
|
||||
}
|
||||
26
src/assets/components/cards.less
Normal file
26
src/assets/components/cards.less
Normal file
@@ -0,0 +1,26 @@
|
||||
@import '../_mixins';
|
||||
|
||||
.card {
|
||||
border-radius: 2rem;
|
||||
padding: 2rem;
|
||||
.themed(background-color, @light-background-100, @dark-background-100);
|
||||
|
||||
&.more {
|
||||
.themed(background-color, @light-background-200, @dark-background-200);
|
||||
}
|
||||
|
||||
&.accent {
|
||||
.themed(background-color, @light-accent, @dark-accent);
|
||||
.themed(color, @light-background, @dark-background);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
.themed(background-color, @light-primary, @dark-primary);
|
||||
.themed(color, @light-background, @dark-background);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
.themed(background-color, @light-secondary, @dark-secondary);
|
||||
.themed(color, @light-text, @dark-text);
|
||||
}
|
||||
}
|
||||
26
src/assets/components/highlight.less
Normal file
26
src/assets/components/highlight.less
Normal file
@@ -0,0 +1,26 @@
|
||||
@import '../_mixins';
|
||||
|
||||
.highlight {
|
||||
z-index: 5;
|
||||
position: relative;
|
||||
.flex-col;
|
||||
justify-content: flex-end;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 50%;
|
||||
width: 108%;
|
||||
left: -4%;
|
||||
display: block;
|
||||
z-index: -5;
|
||||
opacity: 30%;
|
||||
position: absolute;
|
||||
transition: all 0.3s ease-in-out;
|
||||
background: linear-gradient(180deg, transparent 50%, @dark-accent 50%);
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
4
src/assets/components/links.less
Normal file
4
src/assets/components/links.less
Normal file
@@ -0,0 +1,4 @@
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
57
src/assets/components/titles.less
Normal file
57
src/assets/components/titles.less
Normal file
@@ -0,0 +1,57 @@
|
||||
.title {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 700;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.h1,
|
||||
h1,
|
||||
.h2,
|
||||
h2,
|
||||
.h3,
|
||||
h3,
|
||||
.h4,
|
||||
h4,
|
||||
.h5,
|
||||
h5 {
|
||||
.title;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
@h1-size: 4.21rem;
|
||||
.h1,
|
||||
h1 {
|
||||
font-size: @h1-size;
|
||||
}
|
||||
|
||||
@h2-size: 3.158rem;
|
||||
.h2,
|
||||
h2 {
|
||||
font-size: @h2-size;
|
||||
}
|
||||
|
||||
@h3-size: 2.369rem;
|
||||
.h3,
|
||||
h3 {
|
||||
font-size: @h3-size;
|
||||
}
|
||||
|
||||
@h4-size: 1.777rem;
|
||||
.h4,
|
||||
h4 {
|
||||
font-size: @h4-size;
|
||||
}
|
||||
|
||||
@h5-size: 1.333rem;
|
||||
.h5,
|
||||
h5 {
|
||||
font-size: @h5-size;
|
||||
}
|
||||
|
||||
.small,
|
||||
small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
29
src/assets/main.less
Normal file
29
src/assets/main.less
Normal file
@@ -0,0 +1,29 @@
|
||||
@import '_reset';
|
||||
@import '@fontsource/roboto';
|
||||
@import '@fontsource/poppins';
|
||||
@import '_theme';
|
||||
@import '_mixins';
|
||||
@import '_layouts';
|
||||
|
||||
@import 'components/buttons';
|
||||
@import 'components/cards';
|
||||
@import 'components/highlight';
|
||||
@import 'components/links';
|
||||
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
scroll-padding-top: 3rem;
|
||||
}
|
||||
|
||||
ul.no-style {
|
||||
list-style: none;
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
26
src/components/AppFooter.vue
Normal file
26
src/components/AppFooter.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<footer class="flex-row flex-spread card">
|
||||
<div id="copyright" class="small">Copyright © {{ currentYear }} Lucien Cartier-Tilet</div>
|
||||
<div id="source">
|
||||
<a class="highlight small" href="https://labs.phundrak.com/phundrak/gege-jdr">Source code</a>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const currentYear = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
<style scoped="" lang="less">
|
||||
@import '@/assets/_theme';
|
||||
|
||||
footer {
|
||||
margin: 2rem;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
46
src/components/AppHeader.vue
Normal file
46
src/components/AppHeader.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<header class="flex-row-center flex-spread">
|
||||
<RouterLink class="title h4" :to="{ name: 'home' }">GéDR</RouterLink>
|
||||
<div class="buttons flex-row gap-1rem">
|
||||
<button @click="toggleDark()" class="secondary">{{ isDark ? 'Dark' : 'Light' }}</button>
|
||||
<button v-if="!loggedIn" @click="login()" class="secondary">Login</button>
|
||||
<RouterLink v-else :to="{ name: 'account' }" class="button secondary">Account</RouterLink>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useDark, useToggle } from '@vueuse/core';
|
||||
import { usePocketbaseStore } from '@/stores/pocketbase';
|
||||
import { ref } from 'vue';
|
||||
import router from '@/router';
|
||||
|
||||
const isDark = useDark();
|
||||
const toggleDark = useToggle(isDark);
|
||||
|
||||
const pbStore = usePocketbaseStore();
|
||||
const loggedIn = ref(pbStore.loggedIn);
|
||||
|
||||
const login = () => {
|
||||
pbStore.login().subscribe({
|
||||
next: () => router.go(0),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import '@/assets/_theme';
|
||||
|
||||
header {
|
||||
padding: 1rem 2rem;
|
||||
height: 4.5rem;
|
||||
.themed(background-color, @light-background, @dark-background);
|
||||
.themed(border-bottom, solid @light-gray-10 1px, solid @dark-gray-10 1px);
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
103
src/components/AppModal.vue
Normal file
103
src/components/AppModal.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="modal-backdrop">
|
||||
<div class="modal flex-col">
|
||||
<header class="modal-header flex h3">
|
||||
<slot name="header">Default modal title</slot>
|
||||
</header>
|
||||
<button class="btn-close" type="button" @click="close">x</button>
|
||||
<section class="modal-body">
|
||||
<slot name="body">Default modal body</slot>
|
||||
</section>
|
||||
<footer class="modal-footer flex-col">
|
||||
<slot name="footer"></slot>
|
||||
<button v-if="props.type === 'info'" class="accent" type="button" @click="close">
|
||||
Fermer
|
||||
</button>
|
||||
<div v-else class="flex-row gap-1rem action-buttons">
|
||||
<button class="faded" type="button" @click="close">Annuler</button>
|
||||
<button type="button" @click="agree">OK</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
type: 'info' | 'action';
|
||||
}>();
|
||||
const emit = defineEmits(['close', 'agree']);
|
||||
const close = () => {
|
||||
emit('close');
|
||||
};
|
||||
const agree = () => {
|
||||
emit('agree');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import '@/assets/_theme';
|
||||
@outer-padding: 3rem;
|
||||
|
||||
.modal-backdrop {
|
||||
z-index: 10;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
.themed(background-color, @light-gray-80, @light-gray-80);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 11;
|
||||
.themed(background, @light-background, @dark-background);
|
||||
.themed(color, @light-text, @dark-text);
|
||||
.themed(
|
||||
box-shadow,
|
||||
0.2rem 0.2rem 2rem 1px @light-background-100,
|
||||
0.2rem 0.2rem 4rem 1px @dark-background-300
|
||||
);
|
||||
overflow-x: auto;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-footer {
|
||||
padding: @outer-padding;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
position: relative;
|
||||
border-bottom: 1px solit @dark-gray-80;
|
||||
justify-content: space-between;
|
||||
.themed(color, @light-primary, @dark-primary);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid @dark-gray-80;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
position: relative;
|
||||
padding: (@outer-padding / 2) @outer-padding;
|
||||
max-width: 50rem;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
.themed(color, @light-background, @dark-text);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
6
src/components/LoggedInHome.vue
Normal file
6
src/components/LoggedInHome.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>Bonjour {{ pbStore.username }} !</template>
|
||||
|
||||
<script setup="" lang="ts">
|
||||
import { usePocketbaseStore } from '@/stores/pocketbase';
|
||||
const pbStore = usePocketbaseStore();
|
||||
</script>
|
||||
18
src/components/LoggedOutHome.vue
Normal file
18
src/components/LoggedOutHome.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="two-col-img-text gap-4rem">
|
||||
<img alt="Logo de Gégé" src="/assets/gege.png" />
|
||||
<h1>GéDR</h1>
|
||||
</div>
|
||||
<h2 class="card">
|
||||
L'application web pour vous accompagner pour votre JDR avec Gégé, le bot discord.
|
||||
</h2>
|
||||
<div class="flex-col-center gap-1rem">
|
||||
<h3>Pourquoi ?</h3>
|
||||
<ul class="flex-row gap-1rem no-style">
|
||||
<li class="card more">Accéder à ses fiche personnage</li>
|
||||
<li class="card more">Faire évoluer ses personnage</li>
|
||||
<li class="card more">Créer ses personnages</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="h4 raise margin-3rem">Se connecter avec Discord</button>
|
||||
</template>
|
||||
25
src/main.ts
Normal file
25
src/main.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import '@/assets/main.less';
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
import { usePocketbaseStore } from '@/stores/pocketbase';
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const pbStore = usePocketbaseStore();
|
||||
if (!pbStore.loggedIn && ['home', 'login'].every((path) => path != to.name)) {
|
||||
next({ name: 'home' });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
app.mount('#app');
|
||||
19
src/router/index.ts
Normal file
19
src/router/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('@/views/HomeView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
component: () => import('@/views/AccountView.vue'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
||||
48
src/stores/pocketbase.ts
Normal file
48
src/stores/pocketbase.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import PocketBase, { BaseAuthStore, type RecordAuthResponse, type RecordModel } from 'pocketbase';
|
||||
import { defineStore } from 'pinia';
|
||||
import { from, map, Observable, tap } from 'rxjs';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
export const usePocketbaseStore = defineStore('pocketbase', () => {
|
||||
const pb = new PocketBase(import.meta.env.VITE_PB_URL);
|
||||
const authData = ref<RecordAuthResponse<RecordModel> | null>(null);
|
||||
const authStore = computed<BaseAuthStore>(() => pb.authStore);
|
||||
const loggedIn = computed<boolean>(() => pb.authStore.isValid);
|
||||
const username = computed<string | null>(() => authStore.value.model?.username);
|
||||
|
||||
function login(): Observable<RecordAuthResponse<RecordModel>> {
|
||||
return from(pb.collection('users').authWithOAuth2({ provider: 'discord' })).pipe(
|
||||
map((auth) => (authData.value = auth))
|
||||
);
|
||||
}
|
||||
|
||||
function refresh(): Observable<RecordAuthResponse<RecordModel>> {
|
||||
return from(pb.collection('users').authRefresh()).pipe(tap((auth) => (authData.value = auth)));
|
||||
}
|
||||
|
||||
function logout() {
|
||||
pb.authStore.clear();
|
||||
authData.value = null;
|
||||
}
|
||||
|
||||
function deleteAccount(): Observable<boolean> {
|
||||
return from(pb.collection('users').delete(pb.authStore.model?.id)).pipe(
|
||||
tap((deleted) => {
|
||||
if (deleted) {
|
||||
logout();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
authData,
|
||||
authStore,
|
||||
loggedIn,
|
||||
username,
|
||||
login,
|
||||
refresh,
|
||||
logout,
|
||||
deleteAccount,
|
||||
};
|
||||
});
|
||||
78
src/views/AccountView.vue
Normal file
78
src/views/AccountView.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="flex-col-center">
|
||||
<h1>Hello {{ username }} !</h1>
|
||||
<div>
|
||||
<h2>Actions</h2>
|
||||
<ul class="no-style flex-col-center gap-1rem">
|
||||
<li>
|
||||
<button @click="logout()">Logout</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="openModal()">Delete Account</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="modal-fade">
|
||||
<AppModal :type="'action'" v-show="showModal" @close="closeModal()" @agree="deleteAccount">
|
||||
<template v-slot:header>Suppression de compte</template>
|
||||
<template v-slot:body>
|
||||
<p>
|
||||
Voulez-vous vraiment supprimer votre compte ? Cette action est irréversible et
|
||||
supprimera toutes les données liées à votre compte.
|
||||
</p>
|
||||
<p>
|
||||
Vous pourrez à tout moment vous recréer un compte vierge si vous décidez à un moment
|
||||
d'utiliser à nouveau ce site web.
|
||||
</p>
|
||||
</template>
|
||||
</AppModal>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePocketbaseStore } from '@/stores/pocketbase';
|
||||
import router from '@/router';
|
||||
import AppModal from '@/components/AppModal.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const pbStore = usePocketbaseStore();
|
||||
const username = pbStore.username;
|
||||
const showModal = ref<boolean>(false);
|
||||
|
||||
const logout = () => {
|
||||
pbStore.logout();
|
||||
router.go(0);
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
showModal.value = true;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
const deleteAccount = () => {
|
||||
pbStore.deleteAccount().subscribe({
|
||||
next: () => {
|
||||
closeModal();
|
||||
pbStore.logout();
|
||||
router.go(0);
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.modal-fade-enter,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5 ease;
|
||||
}
|
||||
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.5 ease;
|
||||
}
|
||||
</style>
|
||||
16
src/views/HomeView.vue
Normal file
16
src/views/HomeView.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<LoggedInHome v-if="pbStore.loggedIn" />
|
||||
<LoggedOutHome v-else />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { usePocketbaseStore } from '@/stores/pocketbase';
|
||||
|
||||
import LoggedOutHome from '@/components/LoggedOutHome.vue';
|
||||
import LoggedInHome from '@/components/LoggedInHome.vue';
|
||||
|
||||
const pbStore = usePocketbaseStore();
|
||||
pbStore.refresh().subscribe({
|
||||
error: (err) => console.log('Could not refresh account data:', err),
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user