@stephenchenorg/astro 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <slot :error />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import type { Rule } from '../types'
7
+ import { inject, ref } from 'vue'
8
+ import { formValidatorInjectionKey } from '../injectionKey'
9
+
10
+ const props = defineProps<{
11
+ id: string
12
+ rules?: Rule[]
13
+ }>()
14
+
15
+ const error = ref<string | undefined>(undefined)
16
+
17
+ const formValidator = inject(formValidatorInjectionKey)
18
+ if (!formValidator) {
19
+ throw new Error('FormValidator is not provided in the context.')
20
+ }
21
+
22
+ if (props.rules) {
23
+ formValidator.appendRules(props.id, props.rules)
24
+ }
25
+
26
+ formValidator.onErrorsUpdated(errors => {
27
+ error.value = errors[props.id]?.[0]
28
+ })
29
+ </script>
@@ -0,0 +1,22 @@
1
+ import type { PropType } from 'vue';
2
+ import type { FormErrors } from '../types';
3
+ import { FormValidator } from '../form-validator';
4
+ export interface FormValidatorProviderExposed {
5
+ formValidator: () => FormValidator;
6
+ }
7
+ declare const FormValidatorProvider: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
8
+ errors: {
9
+ type: PropType<FormErrors>;
10
+ default: () => {};
11
+ };
12
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
13
+ [key: string]: any;
14
+ }>[] | undefined, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
15
+ errors: {
16
+ type: PropType<FormErrors>;
17
+ default: () => {};
18
+ };
19
+ }>> & Readonly<{}>, {
20
+ errors: FormErrors;
21
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
22
+ export default FormValidatorProvider;
@@ -0,0 +1,24 @@
1
+ import { defineComponent, onMounted, provide } from "vue";
2
+ import { FormValidator } from "../form-validator.js";
3
+ import { formValidatorInjectionKey } from "../injectionKey.js";
4
+ const FormValidatorProvider = defineComponent({
5
+ name: "FormValidatorProvider",
6
+ props: {
7
+ errors: {
8
+ type: Object,
9
+ default: () => ({})
10
+ }
11
+ },
12
+ setup(props, { slots, expose }) {
13
+ const formValidator = new FormValidator();
14
+ provide(formValidatorInjectionKey, formValidator);
15
+ onMounted(() => {
16
+ formValidator.setErrors(props.errors);
17
+ });
18
+ expose({
19
+ formValidator: () => formValidator
20
+ });
21
+ return () => slots.default?.();
22
+ }
23
+ });
24
+ export default FormValidatorProvider;
@@ -0,0 +1,12 @@
1
+ import type { FormErrors, Rule } from './types';
2
+ export declare class FormValidator {
3
+ rules: Record<string, Rule[]>;
4
+ errors: FormErrors;
5
+ errorsUpdatedCallbacks: ((errors: FormErrors) => void)[];
6
+ validate(data: Record<string, any>): boolean;
7
+ prependRules(field: string, rules: Rule | Rule[]): void;
8
+ appendRules(field: string, rules: Rule | Rule[]): void;
9
+ setErrors(errors: FormErrors): void;
10
+ resetErrors(): void;
11
+ onErrorsUpdated(callback: (errors: FormErrors) => void): void;
12
+ }
@@ -0,0 +1,47 @@
1
+ export class FormValidator {
2
+ rules = {};
3
+ errors = {};
4
+ errorsUpdatedCallbacks = [];
5
+ validate(data) {
6
+ const errors = {};
7
+ let isValid = true;
8
+ for (const field in this.rules) {
9
+ const fieldRules = this.rules[field];
10
+ const value = data[field];
11
+ for (const rule of fieldRules) {
12
+ if (!rule.validate(value)) {
13
+ isValid = false;
14
+ if (!errors[field]) {
15
+ errors[field] = [];
16
+ }
17
+ errors[field].push(rule.message);
18
+ break;
19
+ }
20
+ }
21
+ }
22
+ this.setErrors(errors);
23
+ return isValid;
24
+ }
25
+ prependRules(field, rules) {
26
+ if (!this.rules[field]) {
27
+ this.rules[field] = [];
28
+ }
29
+ this.rules[field].unshift(...Array.isArray(rules) ? rules : [rules]);
30
+ }
31
+ appendRules(field, rules) {
32
+ if (!this.rules[field]) {
33
+ this.rules[field] = [];
34
+ }
35
+ this.rules[field].push(...Array.isArray(rules) ? rules : [rules]);
36
+ }
37
+ setErrors(errors) {
38
+ this.errors = structuredClone(errors);
39
+ this.errorsUpdatedCallbacks.forEach((callback) => callback(this.errors));
40
+ }
41
+ resetErrors() {
42
+ this.setErrors({});
43
+ }
44
+ onErrorsUpdated(callback) {
45
+ this.errorsUpdatedCallbacks.push(callback);
46
+ }
47
+ }
@@ -0,0 +1,4 @@
1
+ export { default as FormField } from './components/FormField.vue';
2
+ export { default as FormValidatorProvider } from './components/FormValidatorProvider';
3
+ export * from './form-validator';
4
+ export * from './types';
@@ -0,0 +1,4 @@
1
+ export { default as FormField } from "./components/FormField.vue";
2
+ export { default as FormValidatorProvider } from "./components/FormValidatorProvider.js";
3
+ export * from "./form-validator.js";
4
+ export * from "./types.js";
@@ -0,0 +1,3 @@
1
+ import type { InjectionKey } from 'vue';
2
+ import type { FormValidator } from './form-validator';
3
+ export declare const formValidatorInjectionKey: InjectionKey<FormValidator>;
@@ -0,0 +1 @@
1
+ export const formValidatorInjectionKey = Symbol("");
@@ -0,0 +1,5 @@
1
+ export type FormErrors = Record<string, string[]>;
2
+ export interface Rule {
3
+ validate: (value: any) => boolean;
4
+ message: string;
5
+ }
File without changes
@@ -1,5 +1,4 @@
1
- import Image from './components/Image.astro';
2
- import ResponsiveImage from './components/ResponsiveImage.astro';
1
+ export { default as Image } from './components/Image.astro';
2
+ export { default as ResponsiveImage } from './components/ResponsiveImage.astro';
3
3
  export * from './fragments';
4
4
  export * from './types';
5
- export { Image, ResponsiveImage };
@@ -1,5 +1,4 @@
1
- import Image from "./components/Image.astro";
2
- import ResponsiveImage from "./components/ResponsiveImage.astro";
1
+ export { default as Image } from "./components/Image.astro";
2
+ export { default as ResponsiveImage } from "./components/ResponsiveImage.astro";
3
3
  export * from "./fragments.js";
4
4
  export * from "./types.js";
5
- export { Image, ResponsiveImage };
@@ -1,5 +1,4 @@
1
- import PageFieldRender from './components/PageFieldRender.astro';
1
+ export { default as PageFieldRender } from './components/PageFieldRender.astro';
2
2
  export * from './field';
3
3
  export * from './seo-meta';
4
4
  export * from './types';
5
- export { PageFieldRender };
@@ -1,5 +1,4 @@
1
- import PageFieldRender from "./components/PageFieldRender.astro";
1
+ export { default as PageFieldRender } from "./components/PageFieldRender.astro";
2
2
  export * from "./field/index.js";
3
3
  export * from "./seo-meta/index.js";
4
4
  export * from "./types.js";
5
- export { PageFieldRender };
@@ -0,0 +1,2 @@
1
+ export { ProductSkuManager } from './productSku';
2
+ export * from './types';
@@ -0,0 +1,2 @@
1
+ export { ProductSkuManager } from "./productSku.js";
2
+ export * from "./types.js";
@@ -0,0 +1,39 @@
1
+ import type { ProductAttribute, ProductSpecification } from './types';
2
+ export declare class ProductSkuManager {
3
+ /**
4
+ * 商品規格選項列表
5
+ */
6
+ attributes: ProductAttribute[];
7
+ /**
8
+ * 商品規格列表
9
+ */
10
+ specifications: ProductSpecification[];
11
+ /**
12
+ * 已經選中商品規格選項的參數 ID
13
+ */
14
+ selectedSkuAttrs: Record<number, {
15
+ itemId: number;
16
+ label: string;
17
+ }>;
18
+ /**
19
+ * 匹配到的商品規格 SKU 物件
20
+ */
21
+ specification: ProductSpecification | undefined;
22
+ constructor({ attributes, specifications }: {
23
+ attributes: ProductAttribute[];
24
+ specifications: ProductSpecification[];
25
+ });
26
+ /**
27
+ * 選擇商品規格選項
28
+ */
29
+ selectSkuAttr(skuAttrId: number, skuItemId: number, label: string): void;
30
+ getSpecificationLabel(): string;
31
+ /**
32
+ * 確認商品規格選項都已經選擇
33
+ */
34
+ areAllSkuAttrsSelected(): boolean;
35
+ /**
36
+ * 確認商品規格還有庫存
37
+ */
38
+ isSpecificationInStock(stock: number): boolean;
39
+ }
@@ -0,0 +1,52 @@
1
+ export class ProductSkuManager {
2
+ /**
3
+ * 商品規格選項列表
4
+ */
5
+ attributes = [];
6
+ /**
7
+ * 商品規格列表
8
+ */
9
+ specifications = [];
10
+ /**
11
+ * 已經選中商品規格選項的參數 ID
12
+ */
13
+ selectedSkuAttrs = {};
14
+ /**
15
+ * 匹配到的商品規格 SKU 物件
16
+ */
17
+ specification = void 0;
18
+ constructor({ attributes, specifications }) {
19
+ this.attributes = attributes;
20
+ this.specifications = specifications;
21
+ if (specifications.length === 1 && specifications[0].combination_key === null) {
22
+ this.specification = specifications[0];
23
+ }
24
+ }
25
+ /**
26
+ * 選擇商品規格選項
27
+ */
28
+ selectSkuAttr(skuAttrId, skuItemId, label) {
29
+ this.selectedSkuAttrs[skuAttrId] = {
30
+ itemId: skuItemId,
31
+ label
32
+ };
33
+ const skuAttrIds = Object.values(this.selectedSkuAttrs).map((attr) => attr.itemId).sort((a, b) => a - b);
34
+ this.specification = this.specifications.find((specification) => specification.combination_key === skuAttrIds.join("-"));
35
+ }
36
+ getSpecificationLabel() {
37
+ return Object.values(this.selectedSkuAttrs).map((attr) => attr.label).join(", ");
38
+ }
39
+ /**
40
+ * 確認商品規格選項都已經選擇
41
+ */
42
+ areAllSkuAttrsSelected() {
43
+ return Object.keys(this.selectedSkuAttrs).length === this.attributes.length;
44
+ }
45
+ /**
46
+ * 確認商品規格還有庫存
47
+ */
48
+ isSpecificationInStock(stock) {
49
+ const specificationStock = this.specification?.inventory || 0;
50
+ return specificationStock > 0 && specificationStock >= stock;
51
+ }
52
+ }
@@ -0,0 +1,20 @@
1
+ export interface ProductAttribute {
2
+ id: number;
3
+ title: string;
4
+ items: ProductAttributeItem[];
5
+ }
6
+ export interface ProductAttributeItem {
7
+ id: number;
8
+ title: string;
9
+ }
10
+ export interface ProductSpecification {
11
+ id: number;
12
+ /** 商品規格 Key */
13
+ combination_key: string | null;
14
+ /** 原價 */
15
+ listing_price: string;
16
+ /** 實際售價 */
17
+ selling_price: string;
18
+ /** 商品庫存數量 */
19
+ inventory: number;
20
+ }
File without changes
@@ -7,6 +7,7 @@ interface Props {
7
7
 
8
8
  const config: UrlConfig = {
9
9
  baseUrl: Astro.props.config.baseUrl,
10
+ hash: Astro.props.config.hash,
10
11
  params: Astro.props.config.params,
11
12
  defaultParams: Astro.props.config.defaultParams,
12
13
  }
@@ -22,8 +23,9 @@ import { urlConfigStore } from '@stephenchenorg/astro/query-params'
22
23
  const props = window.__astro_provide_url_config__
23
24
 
24
25
  const baseUrl: string = props?.baseUrl || location.pathname
26
+ const hash: string = props?.hash || ''
25
27
  const params: Record<string, any> = props?.params || {}
26
28
  const defaultParams: Record<string, any> = props?.defaultParams || {}
27
29
 
28
- urlConfigStore.set({ baseUrl, params, defaultParams })
30
+ urlConfigStore.set({ baseUrl, hash, params, defaultParams })
29
31
  </script>
@@ -1,7 +1,6 @@
1
- import ProvideUrlConfig from './components/ProvideUrlConfig.astro';
1
+ export { default as ProvideUrlConfig } from './components/ProvideUrlConfig.astro';
2
2
  export * from './config';
3
3
  export * from './store';
4
4
  export * from './types';
5
5
  export * from './url';
6
6
  export * from './utils';
7
- export { ProvideUrlConfig };
@@ -1,7 +1,6 @@
1
- import ProvideUrlConfig from "./components/ProvideUrlConfig.astro";
1
+ export { default as ProvideUrlConfig } from "./components/ProvideUrlConfig.astro";
2
2
  export * from "./config.js";
3
3
  export * from "./store.js";
4
4
  export * from "./types.js";
5
5
  export * from "./url.js";
6
6
  export * from "./utils.js";
7
- export { ProvideUrlConfig };
@@ -1,6 +1,7 @@
1
1
  import { atom } from "nanostores";
2
2
  export const urlConfigStore = atom({
3
3
  baseUrl: "",
4
+ hash: "",
4
5
  params: {},
5
6
  defaultParams: {}
6
7
  });
@@ -1,5 +1,6 @@
1
1
  export interface UrlConfig {
2
2
  baseUrl: string;
3
+ hash?: string;
3
4
  params: Record<string, any>;
4
5
  defaultParams?: Record<string, any>;
5
6
  }
@@ -3,8 +3,7 @@ import { urlConfigStore } from "./store.js";
3
3
  import { cleanParams, mergeUrlParams } from "./utils.js";
4
4
  export function queryParamsUrl(additionalParams, urlConfig = {
5
5
  baseUrl: "",
6
- params: {},
7
- defaultParams: {}
6
+ params: {}
8
7
  }, options = {}) {
9
8
  const {
10
9
  clear = false,
@@ -24,7 +23,7 @@ export function queryParamsUrl(additionalParams, urlConfig = {
24
23
  skipNull: true,
25
24
  sort: false
26
25
  });
27
- return `${config.baseUrl}${queryString ? "?" : ""}${queryString}`;
26
+ return `${config.baseUrl}${queryString ? "?" : ""}${queryString}${config.hash ? `#${config.hash}` : ""}`;
28
27
  }
29
28
  export function parseQueryParams(search) {
30
29
  return qs.parse(search);
@@ -1,6 +1,8 @@
1
1
  export function mergeUrlParams(baseParams, userParams) {
2
2
  return Object.keys(baseParams).reduce((result, key) => {
3
- if (Array.isArray(baseParams[key])) {
3
+ if (userParams[key] === null) {
4
+ result[key] = null;
5
+ } else if (Array.isArray(baseParams[key])) {
4
6
  result[key] = Array.from(/* @__PURE__ */ new Set([...baseParams[key], ...userParams[key] || []]));
5
7
  } else {
6
8
  result[key] = typeof userParams[key] !== "undefined" ? userParams[key] : baseParams[key];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stephenchenorg/astro",
3
3
  "type": "module",
4
- "version": "3.1.0",
4
+ "version": "4.0.0",
5
5
  "description": "Stephenchenorg Astro 前端通用套件",
6
6
  "license": "MIT",
7
7
  "homepage": "https://stephenchenorg-astro.netlify.app",
@@ -21,6 +21,10 @@
21
21
  "types": "./dist/company-setting/index.d.ts",
22
22
  "import": "./dist/company-setting/index.js"
23
23
  },
24
+ "./form-validator": {
25
+ "types": "./dist/form-validator/index.d.ts",
26
+ "import": "./dist/form-validator/index.js"
27
+ },
24
28
  "./image": {
25
29
  "types": "./dist/image/index.d.ts",
26
30
  "import": "./dist/image/index.js"
@@ -37,6 +41,10 @@
37
41
  "types": "./dist/pagination-vue/index.d.ts",
38
42
  "import": "./dist/pagination-vue/index.js"
39
43
  },
44
+ "./product-sku": {
45
+ "types": "./dist/product-sku/index.d.ts",
46
+ "import": "./dist/product-sku/index.js"
47
+ },
40
48
  "./query-params": {
41
49
  "types": "./dist/query-params/index.d.ts",
42
50
  "import": "./dist/query-params/index.js"
@@ -62,6 +70,7 @@
62
70
  "build": "mkdist --declaration --ext=js && sh ./scripts/postbuild.sh",
63
71
  "lint": "eslint \"*.{js,ts,json}\" \"src/**/*.ts\"",
64
72
  "code-check": "astro check && npm run lint",
73
+ "test": "vitest",
65
74
  "prepack": "npm run build",
66
75
  "release": "bumpp --commit \"Release v%s\" && npm publish"
67
76
  },
@@ -80,17 +89,19 @@
80
89
  "graphql": "^16.11.0",
81
90
  "graphql-tag": "^2.12.6",
82
91
  "nanostores": "^1.0.1",
83
- "query-string": "^9.2.0"
92
+ "query-string": "^9.2.2"
84
93
  },
85
94
  "devDependencies": {
86
95
  "@astrojs/check": "^0.9.4",
87
- "@ycs77/eslint-config": "^4.3.0",
88
- "astro": "^5.9.1",
89
- "bumpp": "^10.1.1",
90
- "eslint": "^9.28.0",
96
+ "@astrojs/vue": "^5.1.0",
97
+ "@ycs77/eslint-config": "^4.4.0",
98
+ "astro": "^5.11.0",
99
+ "bumpp": "^10.2.0",
100
+ "eslint": "^9.30.1",
91
101
  "eslint-plugin-astro": "^1.3.1",
92
102
  "mkdist": "^2.3.0",
93
103
  "typescript": "^5.8.3",
94
- "vue": "^3.5.16"
104
+ "vitest": "^3.2.4",
105
+ "vue": "^3.5.17"
95
106
  }
96
107
  }