memi-board 0.3.1 → 0.4.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,146 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import { useMemiBoardConfig } from 'memi-board'
4
+ import { useMemiBoardAuth } from 'memi-board'
5
+
6
+ const config = useMemiBoardConfig()
7
+ const providers = computed(() => config.auth?.providers ?? ['google', 'apple'])
8
+ const hasOAuthProvider = computed(() => providers.value.includes('google') || providers.value.includes('apple'))
9
+
10
+ const { signInWithGoogle, signInWithApple, signInWithEmail, signUpWithEmail } = useMemiBoardAuth()
11
+
12
+ const mode = ref<'signin' | 'signup'>('signin')
13
+ const email = ref('')
14
+ const password = ref('')
15
+ const displayName = ref('')
16
+ const loading = ref(false)
17
+ const error = ref('')
18
+
19
+ async function handleGoogle() {
20
+ loading.value = true
21
+ error.value = ''
22
+ try {
23
+ await signInWithGoogle()
24
+ }
25
+ catch (e) {
26
+ error.value = (e as Error).message
27
+ }
28
+ finally {
29
+ loading.value = false
30
+ }
31
+ }
32
+
33
+ async function handleApple() {
34
+ loading.value = true
35
+ error.value = ''
36
+ try {
37
+ await signInWithApple()
38
+ }
39
+ catch (e) {
40
+ error.value = (e as Error).message
41
+ }
42
+ finally {
43
+ loading.value = false
44
+ }
45
+ }
46
+
47
+ async function handleEmailSubmit() {
48
+ loading.value = true
49
+ error.value = ''
50
+ try {
51
+ if (mode.value === 'signin') {
52
+ await signInWithEmail(email.value, password.value)
53
+ }
54
+ else {
55
+ await signUpWithEmail(email.value, password.value, displayName.value || undefined)
56
+ }
57
+ }
58
+ catch (e) {
59
+ error.value = (e as Error).message
60
+ }
61
+ finally {
62
+ loading.value = false
63
+ }
64
+ }
65
+ </script>
66
+
67
+ <template>
68
+ <div class="flex flex-col gap-4 max-w-sm">
69
+ <UButton
70
+ v-if="providers.includes('google')"
71
+ icon="i-simple-icons-google"
72
+ color="neutral"
73
+ variant="outline"
74
+ label="Google로 로그인"
75
+ block
76
+ :loading="loading"
77
+ @click="handleGoogle"
78
+ />
79
+
80
+ <UButton
81
+ v-if="providers.includes('apple')"
82
+ icon="i-simple-icons-apple"
83
+ color="neutral"
84
+ variant="outline"
85
+ label="Apple로 로그인"
86
+ block
87
+ :loading="loading"
88
+ @click="handleApple"
89
+ />
90
+
91
+ <template v-if="providers.includes('emailPassword')">
92
+ <div
93
+ v-if="hasOAuthProvider"
94
+ class="flex items-center gap-2 text-xs text-muted"
95
+ >
96
+ <div class="flex-1 border-t border-default" />
97
+ 또는
98
+ <div class="flex-1 border-t border-default" />
99
+ </div>
100
+
101
+ <form
102
+ class="flex flex-col gap-3"
103
+ @submit.prevent="handleEmailSubmit"
104
+ >
105
+ <UInput
106
+ v-if="mode === 'signup'"
107
+ v-model="displayName"
108
+ placeholder="이름"
109
+ />
110
+ <UInput
111
+ v-model="email"
112
+ type="email"
113
+ placeholder="이메일"
114
+ required
115
+ />
116
+ <UInput
117
+ v-model="password"
118
+ type="password"
119
+ placeholder="비밀번호"
120
+ required
121
+ />
122
+ <UButton
123
+ type="submit"
124
+ block
125
+ :loading="loading"
126
+ :label="mode === 'signin' ? '로그인' : '회원가입'"
127
+ />
128
+ </form>
129
+
130
+ <UButton
131
+ variant="link"
132
+ size="sm"
133
+ color="neutral"
134
+ :label="mode === 'signin' ? '계정이 없으신가요? 회원가입' : '이미 계정이 있으신가요? 로그인'"
135
+ @click="mode = mode === 'signin' ? 'signup' : 'signin'"
136
+ />
137
+ </template>
138
+
139
+ <p
140
+ v-if="error"
141
+ class="text-sm text-error"
142
+ >
143
+ {{ error }}
144
+ </p>
145
+ </div>
146
+ </template>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import { versionHistory } from 'memi-board'
3
+ </script>
4
+
5
+ <template>
6
+ <div class="flex flex-col gap-6">
7
+ <div
8
+ v-for="entry in versionHistory"
9
+ :key="entry.version"
10
+ class="flex flex-col gap-2"
11
+ >
12
+ <div class="flex items-center gap-2">
13
+ <UBadge
14
+ :label="`v${entry.version}`"
15
+ color="primary"
16
+ variant="subtle"
17
+ />
18
+ <span class="text-sm text-muted">{{ entry.date }}</span>
19
+ </div>
20
+ <ul class="list-disc list-inside text-sm leading-relaxed space-y-1">
21
+ <li
22
+ v-for="item in entry.highlights"
23
+ :key="item"
24
+ >
25
+ {{ item }}
26
+ </li>
27
+ </ul>
28
+ <p
29
+ v-if="entry.note"
30
+ class="text-xs text-muted"
31
+ >
32
+ {{ entry.note }}
33
+ </p>
34
+ </div>
35
+ </div>
36
+ </template>
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "memi-board",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
- "description": "Firebase 기반 게시판 Vue 컴포넌트/composable 패키지 — Nuxt 4 + Nuxt UI 프로젝트에 posts CRUD, 첨부파일, 댓글, 권한, AI 검열을 바로 붙임",
5
+ "description": "Firebase 게시판 — core composable + thin Nuxt module(UI SFC). Nuxt 4 + Nuxt UI 호스트에 붙임",
6
6
  "license": "MIT",
7
7
  "author": "fkkmemi",
8
8
  "repository": {
@@ -13,8 +13,19 @@
13
13
  "url": "https://github.com/fkkmemi/memi-board/issues"
14
14
  },
15
15
  "homepage": "https://github.com/fkkmemi/memi-board#readme",
16
- "keywords": ["vue", "nuxt", "firebase", "firestore", "board", "community", "nuxt-ui"],
17
- "sideEffects": false,
16
+ "keywords": [
17
+ "vue",
18
+ "nuxt",
19
+ "firebase",
20
+ "firestore",
21
+ "board",
22
+ "community",
23
+ "nuxt-ui"
24
+ ],
25
+ "sideEffects": [
26
+ "**/*.vue",
27
+ "**/*.css"
28
+ ],
18
29
  "publishConfig": {
19
30
  "access": "public"
20
31
  },
@@ -22,6 +33,10 @@
22
33
  ".": {
23
34
  "types": "./dist/index.d.ts",
24
35
  "import": "./dist/index.js"
36
+ },
37
+ "./nuxt": {
38
+ "types": "./dist/module.d.ts",
39
+ "import": "./dist/module.mjs"
25
40
  }
26
41
  },
27
42
  "main": "./dist/index.js",
@@ -31,23 +46,31 @@
31
46
  ],
32
47
  "scripts": {
33
48
  "prepack": "pnpm build",
34
- "build": "vite build",
49
+ "build": "node scripts/build.mjs",
35
50
  "build:watch": "vite build --watch",
36
- "dev": "nuxi dev playground",
37
- "dev:build": "nuxi build playground",
38
- "dev:prepare": "nuxi prepare playground",
51
+ "dev": "pnpm build && nuxi dev playground",
52
+ "dev:build": "pnpm build && nuxi build playground",
53
+ "dev:prepare": "pnpm build && nuxi prepare playground",
39
54
  "typecheck": "vue-tsc --noEmit"
40
55
  },
41
56
  "peerDependencies": {
42
57
  "@nuxt/ui": "^4.0.0",
43
58
  "firebase": "^12.0.0",
59
+ "nuxt": ">=3.0.0 || >=4.0.0",
44
60
  "vue": "^3.5.0",
45
61
  "vue-router": "^4.0.0 || ^5.0.0",
46
62
  "vuefire": "^3.2.3"
47
63
  },
64
+ "peerDependenciesMeta": {
65
+ "nuxt": {
66
+ "optional": true
67
+ },
68
+ "@nuxt/ui": {
69
+ "optional": true
70
+ }
71
+ },
48
72
  "devDependencies": {
49
73
  "@nuxt/ui": "^4.9.0",
50
- "@vitejs/plugin-vue": "^6.0.8",
51
74
  "firebase": "^12.15.0",
52
75
  "nuxt": "^4.5.1",
53
76
  "nuxt-vuefire": "^1.1.2",
@@ -67,5 +90,8 @@
67
90
  "protobufjs",
68
91
  "vue-demi"
69
92
  ]
93
+ },
94
+ "dependencies": {
95
+ "@nuxt/kit": "^4.5.1"
70
96
  }
71
97
  }
@@ -1,17 +0,0 @@
1
- import { Attachment } from '../types';
2
- type __VLS_Props = {
3
- modelValue: Attachment[];
4
- /** 새 글 작성 중이면 아직 slug가 없으므로 임시 네임스페이스를 넘긴다. */
5
- postId: string;
6
- editable?: boolean;
7
- maxFiles?: number;
8
- };
9
- declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
10
- "update:modelValue": (attachments: Attachment[]) => any;
11
- }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
12
- "onUpdate:modelValue"?: ((attachments: Attachment[]) => any) | undefined;
13
- }>, {
14
- editable: boolean;
15
- maxFiles: number;
16
- }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
17
- export default _default;
@@ -1,5 +0,0 @@
1
- type __VLS_Props = {
2
- postId: string;
3
- };
4
- declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
5
- export default _default;
@@ -1,5 +0,0 @@
1
- type __VLS_Props = {
2
- postId: string;
3
- };
4
- declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
5
- export default _default;
@@ -1,11 +0,0 @@
1
- type __VLS_Props = {
2
- postId: string;
3
- };
4
- declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
5
- deleted: () => any;
6
- edit: (postId: string) => any;
7
- }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
8
- onDeleted?: (() => any) | undefined;
9
- onEdit?: ((postId: string) => any) | undefined;
10
- }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
11
- export default _default;
@@ -1,12 +0,0 @@
1
- type __VLS_Props = {
2
- /** 지정하면 수정 모드, 지정하지 않으면 새 글 작성 모드. */
3
- postId?: string;
4
- };
5
- declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
6
- saved: (id: string) => any;
7
- cancel: () => any;
8
- }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
9
- onSaved?: ((id: string) => any) | undefined;
10
- onCancel?: (() => any) | undefined;
11
- }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
12
- export default _default;
@@ -1,14 +0,0 @@
1
- import { PostModel } from '../types';
2
- type __VLS_Props = {
3
- pageSize?: number;
4
- /** 지정하면 각 글이 `${linkBase}/${id}`로 이동하는 링크가 된다. 지정하지 않으면 select 이벤트만 emit. */
5
- linkBase?: string;
6
- };
7
- declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
8
- select: (post: PostModel) => any;
9
- }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
10
- onSelect?: ((post: PostModel) => any) | undefined;
11
- }>, {
12
- pageSize: number;
13
- }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
14
- export default _default;
@@ -1,2 +0,0 @@
1
- declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
2
- export default _default;
@@ -1,2 +0,0 @@
1
- declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
2
- export default _default;