Skip to content

Commit

Permalink
trying out some zod stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
MrBrax committed Nov 9, 2023
1 parent 24d4875 commit edf8656
Show file tree
Hide file tree
Showing 14 changed files with 258 additions and 99 deletions.
15 changes: 13 additions & 2 deletions client-vue/.pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
3 changes: 2 additions & 1 deletion client-vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
"vite": "^4.5.0",
"vite-plugin-pwa": "^0.16.4",
"vitest": "^0.34.6",
"vue-tsc": "^1.8.22"
"vue-tsc": "^1.8.22",
"zod": "^3.22.4"
},
"packageManager": "[email protected]",
"engines": {
Expand Down
60 changes: 36 additions & 24 deletions client-vue/src/components/forms/ChannelAddForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
<div v-if="formData.provider == 'twitch'" class="field">
<label class="label">{{ t("forms.channel.login") }} <span class="required">*</span></label>
<div class="control has-addon">
<input ref="login" v-model="formData.login" class="input" type="text" name="login" required pattern="^[a-z0-9_]{3,25}$" @keyup="checkLogin" />
<d-button type="button" color="success" icon="sync" :disabled="!formData.login" @click="fetchLogin">
<input ref="login" v-model="formData.internalName" class="input" type="text" name="login" required pattern="^[a-z0-9_]{3,25}$" @keyup="checkTwitchLogin" />
<d-button type="button" color="success" icon="sync" :disabled="!formData.internalName" @click="fetchTwitchLogin">
{{ t("forms.channel.check") }}
</d-button>
</div>
Expand All @@ -31,17 +31,20 @@
<label class="label">{{ t("forms.channel.url") }}</label>
<div class="control has-addon">
<input v-model="channelUrl" class="input" type="text" :disabled="fetchingUrl" />
<d-button type="button" color="success" icon="sync" :loading="fetchingUrl" @click="getChannelId">
<d-button type="button" color="success" icon="sync" :loading="fetchingUrl" @click="getYouTubeChannelId">
{{ t("buttons.fetch") }}
</d-button>
</div>
<p class="input-help">
{{ t("forms.channel.youtube-url-help") }}
</p>
</div>

<!-- youtube channel id -->
<div v-if="formData.provider == 'youtube'" class="field">
<label class="label">{{ t("forms.channel.id") }} <span class="required">*</span></label>
<div class="control has-addon">
<input ref="channel_id" v-model="formData.channel_id" class="input" type="text" name="channel_id" required :disabled="fetchingUrl" />
<input ref="channel_id" v-model="formData.internalId" class="input" type="text" name="channel_id" required :disabled="fetchingUrl" />
<!--
<button class="button is-confirm" type="button" @click="fetchLogin" :disabled="!formData.login">
<span class="icon"><font-awesome-icon icon="sync" /></span>
Expand All @@ -58,8 +61,8 @@
<div v-if="formData.provider == 'kick'" class="field">
<label class="label">{{ t("forms.channel.slug") }} <span class="required">*</span></label>
<div class="control has-addon">
<input ref="slug" v-model="formData.slug" class="input" type="text" name="slug" required />
<button class="button is-confirm" type="button" :disabled="!formData.slug" @click="fetchKickSlug">
<input ref="slug" v-model="formData.internalName" class="input" type="text" name="slug" required />
<button class="button is-confirm" type="button" :disabled="!formData.internalName" @click="fetchKickSlug">
<span class="icon"><font-awesome-icon icon="sync" /></span>
<span>{{ t("forms.channel.check") }}</span>
</button>
Expand All @@ -86,7 +89,7 @@

<div v-if="userExists === false" class="field">
<div class="text-is-error">
{{ t("forms.channel.login-does-not-exist", [formData.login]) }}
{{ t("forms.channel.login-does-not-exist", [formData.internalName]) }}
</div>
</div>

Expand Down Expand Up @@ -203,7 +206,7 @@
<p>{{ t("forms.channel.subscriptions-warning") }}</p>
</div>

<FormSubmit :form-status="formStatus" :form-status-text="formStatusText">
<FormSubmit :form-status="formStatus" :form-status-text="formStatusText" :zod-errors="zodErrors">
<div class="control">
<d-button type="submit" color="success" icon="user-plus">
{{ t("forms.channel.add-channel") }}
Expand All @@ -225,6 +228,7 @@ import type { ApiResponse, ApiErrorResponse, IApiResponse } from "@common/Api/Ap
import { useI18n } from "vue-i18n";
import type { FormStatus } from "@/twitchautomator";
import type { KickUser } from "@common/KickAPI/Kick";
import type { ZodError } from "zod";
library.add(faUserPlus);
// emit
Expand All @@ -236,11 +240,14 @@ const { t } = useI18n();
// data
const formStatusText = ref<string>("Ready");
const formStatus = ref<FormStatus>("IDLE");
const zodErrors = ref<ZodError>();
const formData = ref({
provider: "twitch",
login: "",
channel_id: "",
slug: "",
// login: "",
// channel_id: "",
// slug: "",
internalId: "",
internalName: "",
quality: "",
match: "",
download_chat: false,
Expand Down Expand Up @@ -287,6 +294,9 @@ function submitForm(event: Event) {
if (axios.isAxiosError<ApiErrorResponse>(err) && err.response) {
formStatusText.value = err.response.data.message;
formStatus.value = err.response.data.status;
if (err.response.data.zodErrors) {
zodErrors.value = err.response.data.zodErrors;
}
}
});
Expand All @@ -297,9 +307,11 @@ function submitForm(event: Event) {
function resetForm() {
formData.value = {
provider: "twitch",
login: "",
channel_id: "",
slug: "",
// login: "",
// channel_id: "",
// slug: "",
internalId: "",
internalName: "",
quality: "",
match: "",
download_chat: false,
Expand All @@ -314,10 +326,10 @@ function resetForm() {
};
}
function checkLogin() {
const match = formData.value.login.match(/^https?:\/\/www.twitch.tv\/(\w+)/);
function checkTwitchLogin() {
const match = formData.value.internalName.match(/^https?:\/\/www.twitch.tv\/(\w+)/);
if (match) {
formData.value.login = match[1];
formData.value.internalName = match[1];
}
userExists.value = undefined;
}
Expand All @@ -340,9 +352,9 @@ validateQuality() {
}
},
*/
function fetchLogin() {
function fetchTwitchLogin() {
axios
.get<IApiResponse<UserData>>(`/api/v0/twitchapi/user/${formData.value.login}`)
.get<IApiResponse<UserData>>(`/api/v0/twitchapi/user/${formData.value.internalName}`)
.then((response) => {
const json = response.data;
const field = login.value;
Expand All @@ -351,9 +363,9 @@ function fetchLogin() {
}
if (json.status == "OK") {
channelData.value = json.data;
if (channelData.value && channelData.value.login !== formData.value.login) {
if (channelData.value && channelData.value.login !== formData.value.internalName) {
alert(t("messages.login-mismatch-fixing"));
formData.value.login = channelData.value.login;
formData.value.internalName = channelData.value.login;
}
field.setCustomValidity("");
field.reportValidity();
Expand All @@ -378,14 +390,14 @@ function fetchLogin() {
});
}
function getChannelId() {
function getYouTubeChannelId() {
fetchingUrl.value = true;
axios
.post<ApiResponse>("/api/v0/youtubeapi/channelid", { url: channelUrl.value })
.then((response) => {
const json = response.data;
if (json.status == "OK") {
formData.value.channel_id = json.data;
formData.value.internalId = json.data;
}
console.log("channel id", json);
})
Expand All @@ -399,7 +411,7 @@ function getChannelId() {
function fetchKickSlug() {
axios
.get<IApiResponse<KickUser>>(`/api/v0/kickapi/users/${formData.value.slug}`)
.get<IApiResponse<KickUser>>(`/api/v0/kickapi/users/${formData.value.internalName}`)
.then((response) => {
const json = response.data;
if (json.status == "OK") {
Expand Down
11 changes: 8 additions & 3 deletions client-vue/src/components/forms/ChannelUpdateForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
</p>
</div>

<FormSubmit :form-status="formStatus" :form-status-text="formStatusText">
<FormSubmit :form-status="formStatus" :form-status-text="formStatusText" :zod-errors="zodErrors">
<d-button color="success" type="submit" icon="save">
{{ t("buttons.save") }}
</d-button>
Expand Down Expand Up @@ -224,7 +224,7 @@ import FormSubmit from "@/components/reusables/FormSubmit.vue";
import { formatDate } from "@/mixins/newhelpers";
import { useStore } from "@/store";
import type { FormStatus } from "@/twitchautomator";
import type { ApiResponse } from "@common/Api/Api";
import type { ApiResponse, ApiErrorResponse } from "@common/Api/Api";
import type { ApiChannelConfig } from "@common/Api/Client";
import { VideoQualityArray } from "@common/Defs";
import type { HistoryEntry, HistoryEntryOnline } from "@common/History";
Expand All @@ -233,6 +233,7 @@ import { faList, faPencil, faSave, faTrash, faVideoSlash } from "@fortawesome/fr
import axios, { AxiosError } from "axios";
import { computed, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import type { ZodError } from "zod";
library.add(faSave, faList, faTrash, faVideoSlash, faPencil);
// props
Expand All @@ -250,6 +251,7 @@ const { t } = useI18n();
// data
const formStatusText = ref<string>("Ready");
const formStatus = ref<FormStatus>("IDLE");
const zodErrors = ref<ZodError>();
const formData = ref({
// ref or reactive?
quality: "",
Expand Down Expand Up @@ -332,10 +334,13 @@ function submitForm(event: Event) {
}
})
.catch((err: Error | AxiosError) => {
if (axios.isAxiosError(err) && err.response) {
if (axios.isAxiosError<ApiErrorResponse>(err) && err.response) {
console.error("channel update form error", err.response);
formStatusText.value = err.response.data.message;
formStatus.value = err.response.data.status;
if (err.response.data.zodErrors) {
zodErrors.value = err.response.data.zodErrors;
}
} else {
console.error("channel update form error", err);
alert(`Error: ${err.message}`);
Expand Down
14 changes: 13 additions & 1 deletion client-vue/src/components/reusables/FormSubmit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@
<span class="icon">
<font-awesome-icon :icon="formStatusIcon" :spin="formStatus == 'LOADING'" />
</span>
{{ computedText }}
<template v-if="zodErrors">
<ul>
<li v-for="error in zodErrors.issues" :key="error.path.toString()">
{{ error.path.toString() }}:
{{ error.message }}
</li>
</ul>
</template>
<template v-else>
{{ computedText }}
</template>
</div>
</div>
</template>
Expand All @@ -18,11 +28,13 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { faCheck, faExclamationTriangle, faFile, faSpinner } from "@fortawesome/free-solid-svg-icons";
import type { FormStatus } from "@/twitchautomator";
import { useI18n } from "vue-i18n";
import type { ZodError, } from "zod";
library.add(faCheck, faExclamationTriangle, faFile, faSpinner);
const props = defineProps<{
formStatusText: string;
formStatus: FormStatus;
zodErrors?: ZodError;
}>();
const { t } = useI18n();
Expand Down
3 changes: 2 additions & 1 deletion client-vue/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@
"subscriptions-warning": "Remember to delete all subscriptions before you stop using this application. Old subscriptions will not be deleted automatically, and will continue to send requests to your server.",
"slug": "Slug",
"slug_help": "The channel slug (username in url)",
"internal-id": "Internal ID"
"internal-id": "Internal ID",
"youtube-url-help": "As of now, the URL to the channel is the most convenient way of getting the ID. If it correctly identified the channel, the ID field below will populate automatically."
},
"config": {
"validate-external-url": "Validate external URL"
Expand Down
8 changes: 8 additions & 0 deletions client-vue/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5848,6 +5848,7 @@ __metadata:
vue-observe-visibility: "npm:next"
vue-router: "npm:^4.2.5"
vue-tsc: "npm:^1.8.22"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft

Expand Down Expand Up @@ -8829,3 +8830,10 @@ __metadata:
checksum: 2cac84540f65c64ccc1683c267edce396b26b1e931aa429660aefac8fbe0188167b7aee815a3c22fa59a28a58d898d1a2b1825048f834d8d629f4c2a5d443801
languageName: node
linkType: hard

"zod@npm:^3.22.4":
version: 3.22.4
resolution: "zod@npm:3.22.4"
checksum: 73622ca36a916f785cf528fe612a884b3e0f183dbe6b33365a7d0fc92abdbedf7804c5e2bd8df0a278e1472106d46674281397a3dd800fa9031dc3429758c6ac
languageName: node
linkType: hard
9 changes: 5 additions & 4 deletions common/Api/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { WinstonLogLine } from "@common/Log";
import type { ChannelConfig } from "../Config";
import { settingsFields } from "../ServerConfig";
import type { AboutData } from "./About";
import type { ApiChannels, ApiFile, ApiGame, ApiJob, ApiLogLine, ApiVods } from "./Client";
import type { ApiChannels, ApiFile, ApiGame, ApiJob, ApiVods } from "./Client";

export interface ApiResponse {
data: any;
Expand All @@ -13,6 +13,7 @@ export interface ApiResponse {
export interface ApiErrorResponse {
status: "ERROR";
message: string;
zodErrors?: any;
}

export interface ApiGenericResponse {
Expand Down Expand Up @@ -42,7 +43,7 @@ export interface ApiSettingsResponse extends ApiResponse {
channels: ChannelConfig[];
favourite_games: string[];
// fields: Record<string, SettingField<any>>;
fields: typeof settingsFields,
fields: typeof settingsFields;
version: string;
server: string;
websocket_url: string;
Expand Down Expand Up @@ -104,7 +105,7 @@ export interface ApiJobsResponse extends ApiResponse {
export interface ApiFilesResponse extends ApiResponse {
data: {
files: ApiFile[];
}
};
}

export interface ApiAuthResponse {
Expand All @@ -131,4 +132,4 @@ export interface IApiResponse<T> {

export interface ApiAboutResponse extends ApiResponse {
data: AboutData;
}
}
6 changes: 5 additions & 1 deletion common/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@ export type SettingField =
export interface BaseChannelConfig {
provider: Providers;
uuid: string;

/** The unique ID of the channel. Is usually a number, but can be a string for some providers. */
internalId: string;

/** The username or login of the channel. Some providers use this instead of the internal ID. */
internalName: string;
quality: VideoQuality[];
match: string[];
Expand Down Expand Up @@ -167,7 +171,7 @@ export interface KickChannelConfig extends BaseChannelConfig {
provider: "kick";

/** @deprecated */
slug: string;
slug?: string;
}

export type ChannelConfig =
Expand Down
Loading

0 comments on commit edf8656

Please sign in to comment.