Sort of working beta

This commit is contained in:
2025-02-06 17:30:45 +01:00
parent 5f42c707eb
commit 2ef114e154
97 changed files with 3093 additions and 106 deletions

View File

@ -3,8 +3,8 @@ import { appName } from '@/app.ts'
</script>
<template>
<router-link class="flex justify-center items-center gap-3" to="/">
<Link class="flex justify-center items-center gap-3" href="/">
<img src="@/Assets/logo.png" alt="logo DatBrowser" class="h-20" />
<h1 class="text-xl font-display select-none">{{ appName }}</h1>
</router-link>
</Link>
</template>

View File

@ -1,4 +1,8 @@
<script setup lang="ts">
import Description from '@/Components/ui/text/Description.vue';
</script>
<template>
<h2 class="text-xl"><slot name="title" /></h2>
<p class="text-dark-green"><slot name="description" /></p>
<Description><slot name="description" /></Description>
</template>

View File

@ -0,0 +1,139 @@
<script setup lang="ts">
import { Job, JobArtifact, JobInfo, JobInfoType, JobRunArtifact } from "@/types/Jobs/job";
import { router } from "@inertiajs/vue3";
import { onMounted, reactive, ref, watch } from "vue";
import Button from "../../ui/button/Button.vue";
import JobFormField from "./JobFormField.vue";
import LoadingSpinner from "@/Components/ui/feedback/spinner/LoadingSpinner.vue";
import { httpApi } from "@/lib/utils";
const props = withDefaults(defineProps<{
job: Job;
error?: string;
}>(), {
error: "",
});
const isSaving = ref(false);
const isTesting = ref(false);
const errorMessage = ref(props.error);
const jobSuccess = ref(true);
function submit() {
isSaving.value = true;
if (!testForm()) {
isTesting.value = false;
return;
}
router.patch("/jobs/" + props.job.id, {
is_active: isActiveJobInfo.value.value,
...props.job.job_infos.reduce((acc, jobInfo) => {
acc[jobInfo.id] = jobInfo.value;
return acc;
}, {} as Record<number, string | boolean>),
});
setTimeout(() => {
isSaving.value = false;
}, 200);
}
const isActiveJobInfo = ref<JobInfo>({
id: 0,
name: "Activer",
description: "Activer le job",
value: props.job.is_active,
is_required: false,
job_info_type: { name: "checkbox" } as JobInfoType,
} as JobInfo);
async function testJob() {
isTesting.value = true;
submit();
console.log("Testing job", props.job.id);
let response;
try {
response = await httpApi<{ artifact?: JobRunArtifact }>(
`/jobs/${props.job.id}/test`
);
jobSuccess.value = response.artifact?.success ?? false;
} catch (e) {
console.error("Testing the job failed : ", e);
jobSuccess.value = false;
} finally {
isTesting.value = false;
}
if (response?.artifact) {
alert(response.artifact.artifacts[0].name + " : " + response.artifact.artifacts[0].content);
}
}
function testForm(): boolean {
console.log("Testing the form validity");
const jobForm = document.querySelector('form[name="jobForm"]') as HTMLFormElement;
if (jobForm.checkValidity() == false) {
console.log("Form is not valid");
if (jobForm.reportValidity) {
jobForm.reportValidity()
}
else {
errorMessage.value = "Le formulaire n'est pas valide";
}
isTesting.value = false;
return false;
}
return true;
}
</script>
<template>
<form @submit.prevent="submit" class="flex p-3 flex-col gap-4" name="jobForm">
<JobFormField v-if="job.id != 1" :jobInfo="isActiveJobInfo" />
<JobFormField
:jobInfo="jobInfo"
v-for="jobInfo of job.job_infos"
v-key="jobInfo.id"
/>
<Transition>
<p v-if="errorMessage" class="text-destructive">{{ errorMessage }}</p>
</Transition>
<Button
type="submit"
variant="secondary"
:disabled="isSaving || isTesting"
>
Enregistrer
<LoadingSpinner v-if="isSaving" />
</Button>
<Button
variant="outline"
:disabled="isSaving || isTesting"
@click.prevent="testJob"
>
Tester
<LoadingSpinner v-if="isTesting" />
</Button>
</form>
</template>
<style lang="scss" scoped>
/* we will explain what these classes do next! */
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import Checkbox from '@/Components/ui/checkbox/Checkbox.vue';
import VModelCheckbox from '@/Components/ui/checkbox/VModelCheckbox.vue';
import Input from '@/Components/ui/input/Input.vue';
import { Label } from '@/Components/ui/label';
import Description from '@/Components/ui/text/Description.vue';
import { JobInfo } from '@/types/Jobs/job';
import { watch } from 'vue';
const props = defineProps<{
jobInfo: JobInfo;
}>();
const jobInfoType = props.jobInfo.job_info_type.name;
</script>
<template>
<div>
<Label :for="'' + jobInfo.id" class="text">{{ jobInfo.name }}<span v-if="jobInfo.is_required" class="cursor-help" title="Requis" aria-label="Requis">*</span></Label>
<Description>{{ jobInfo.description }}</Description>
<Input v-if="jobInfoType != 'checkbox'" :type="jobInfoType" :id="'' + jobInfo.id" :name="'' + jobInfo.id" :placeholder="jobInfo.placeholder" v-model="jobInfo.value as string" :required="jobInfo.is_required" />
<VModelCheckbox v-else :id="'' + jobInfo.id" :class="''" v-model="jobInfo.value as boolean" />
</div>
</template>

View File

@ -8,7 +8,7 @@ defineProps<{
</script>
<template>
<MainNavItem :link="`/job/${job.id}`">
<MainNavItem :link="`/jobs/${job.id}`">
{{ job.name }}
</MainNavItem>
</template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils.ts'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'radix-vue'
import { type ButtonVariants, buttonVariants } from '.'

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils.ts'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils.ts'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils.ts'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils.ts'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils.ts'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils.ts'
import { cn } from '@/lib/utils';
const props = defineProps<{
class?: HTMLAttributes['class']

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue'
import { cn } from '@/lib/utils'
import { Check } from 'lucide-vue-next'
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CheckboxRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CheckboxRoot
v-bind="forwarded"
:class="
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class)"
>
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
<slot>
<Check class="h-4 w-4" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
const props = withDefaults(defineProps<{
class?: string;
}>(), {
class: '',
});
const model = defineModel<boolean>({type: Boolean, default: false});
</script>
<template>
<input type="checkbox" :checked="model" @click="() => (model = !model)" class="peer h-7 w-7 shrink-0 rounded-sm border border-gray ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 checked:bg-secondary checked:text-dark-green transition cursor-pointer">
</template>

View File

@ -0,0 +1 @@
export { default as Checkbox } from './Checkbox.vue'

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { Loader2 } from 'lucide-vue-next'
import type { PrimitiveProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { spinnerVariants, type SpinnerVariants } from '.'
interface Props extends PrimitiveProps {
size?: SpinnerVariants['size']
class?: HTMLAttributes['class']
}
const props = defineProps<Props>()
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
:class="cn(spinnerVariants({ size }), props.class)"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
</svg>
</template>
<style scoped></style>

View File

@ -0,0 +1,19 @@
import { cva, type VariantProps } from 'class-variance-authority'
export const spinnerVariants = cva('animate-spin lucide lucide-loader-circle-icon', {
variants: {
size: {
default: 'w-4 h-4 m-2',
xs: 'w-1 h-1 m-1',
sm: 'w-2 h-2 m-1',
lg: 'w-7 h-7 m-3',
xl: 'w-12 h-12 m-4',
icon: 'w-10 h-10 m-4'
}
},
defaultVariants: {
size: 'default'
}
})
export type SpinnerVariants = VariantProps<typeof spinnerVariants>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useVModel } from '@vueuse/core'
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes['class']
}>()
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<input v-model="modelValue" :class="cn('flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', props.class)">
</template>

View File

@ -0,0 +1 @@
export { default as Input } from './Input.vue'

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { Label, type LabelProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<Label
v-bind="delegatedProps"
:class="
cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
props.class,
)
"
>
<slot />
</Label>
</template>

View File

@ -0,0 +1 @@
export { default as Label } from './Label.vue'

View File

@ -0,0 +1,3 @@
<template>
<p class="text-dark-green"><slot/></p>
</template>

View File

@ -4,12 +4,17 @@ import BaseLayout from "./BaseLayout.vue";
import MainNavJobLink from "@/Components/Layout/MainNav/MainNavJobLink.vue";
import { Job } from "@/types/Jobs/job";
import MainNavItem from "@/Components/Layout/MainNav/MainNavItem.vue";
import { httpApi } from "@/lib/utils";
import { onMounted, ref } from "vue";
let jobs = [
{ id: 1, name: "Hellcase" },
{ id: 2, name: "Jeu gratuit Epic Games" },
{ id: 3, name: "Envoyer un post instagram" },
];
const jobs = ref<Job[]>([]);
async function fetchJobs() {
let jobsRaw = await httpApi<Job[]>("/jobs");
jobs.value = jobsRaw.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
}
onMounted(fetchJobs);
</script>
<template>
@ -19,16 +24,16 @@ let jobs = [
<ul class="flex flex-col gap-2">
<MainNavItem link="/"> Accueil </MainNavItem>
<MainNavJobLink
:job="job as Job"
v-for="job in jobs"
:key="job.id"
:job="job"
/>
</ul>
</nav>
</ScrollArea>
<ScrollArea class="flex-1 h-full overflow-auto">
<main>
<main class="p-3">
<slot />
</main>
</ScrollArea>

View File

@ -1,10 +1,12 @@
<script setup lang="ts">
import JobCard from '../Components/ui/job/JobCard.vue'
import JobForm from '../Components/Layout/Job/JobForm.vue'
import JobCard from '../Components/Layout/Job/JobCard.vue'
import { Job } from "@/types/Jobs/job";
import { Head } from "@inertiajs/vue3";
defineProps<{
job: Job;
error?: string;
}>();
</script>
@ -12,4 +14,6 @@ defineProps<{
<Head title="Welcome" />
<JobCard :job="job" />
<JobForm :job="job" :error="error" />
</template>

View File

@ -4,3 +4,14 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export async function httpApi<T>(route: string): Promise<T> {
let response = await fetch(import.meta.env.VITE_APP_URL + "/api" + route);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}

View File

@ -3,5 +3,39 @@ export type Job = {
name: string;
description: string;
is_active: boolean;
job_infos: JobInfo[];
created_at: Date;
}
export type JobInfo = {
id: number;
name: string;
description: string;
placeholder: string;
value: string | boolean;
is_required: boolean;
job_info_type: JobInfoType;
job_id: number;
}
export type JobInfoType = {
id: number;
name: string;
created_at: Date;
}
export type JobRunArtifact = {
jobId: number;
artifacts: JobArtifact[];
success: boolean;
}
export type JobArtifact = {
name: string;
content: string;
}

View File

@ -12,7 +12,11 @@
<!-- Scripts -->
@routes
@vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"])
@if(config('app.env') === 'production')
@vite(['resources/js/app.ts'])
@else
@vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"])
@endif
@inertiaHead
</head>
<body class="font-sans antialiased">