mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-10-31 21:44:12 +00:00 
			
		
		
		
	ページにいいねできるように
This commit is contained in:
		
							parent
							
								
									d6ccb1725b
								
							
						
					
					
						commit
						380749051d
					
				
					 18 changed files with 489 additions and 191 deletions
				
			
		|  | @ -1874,6 +1874,10 @@ pages: | ||||||
|   edit-this-page: "このページを編集" |   edit-this-page: "このページを編集" | ||||||
|   view-source: "ソースを表示" |   view-source: "ソースを表示" | ||||||
|   view-page: "ページを見る" |   view-page: "ページを見る" | ||||||
|  |   like: "いいね" | ||||||
|  |   unlike: "いいね解除" | ||||||
|  |   liked-pages: "いいねしたページ" | ||||||
|  |   my-pages: "自分のページ" | ||||||
|   inspector: "インスペクター" |   inspector: "インスペクター" | ||||||
|   content: "ページブロック" |   content: "ページブロック" | ||||||
|   variables: "変数" |   variables: "変数" | ||||||
|  |  | ||||||
							
								
								
									
										23
									
								
								migration/1558072954435-PageLike.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								migration/1558072954435-PageLike.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | ||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  | 
 | ||||||
|  | export class PageLike1558072954435 implements MigrationInterface { | ||||||
|  | 
 | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |         await queryRunner.query(`CREATE TABLE "page_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "pageId" character varying(32) NOT NULL, CONSTRAINT "PK_813f034843af992d3ae0f43c64c" PRIMARY KEY ("id"))`); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_0e61efab7f88dbb79c9166dbb4" ON "page_like" ("userId") `); | ||||||
|  |         await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa" ON "page_like" ("userId", "pageId") `); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "page" ADD "likedCount" integer NOT NULL DEFAULT 0`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_cf8782626dced3176038176a847" FOREIGN KEY ("pageId") REFERENCES "page"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async down(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_cf8782626dced3176038176a847"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "likedCount"`); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa"`); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_0e61efab7f88dbb79c9166dbb4"`); | ||||||
|  |         await queryRunner.query(`DROP TABLE "page_like"`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -12,6 +12,11 @@ | ||||||
| 		<small>@{{ page.user.username }}</small> | 		<small>@{{ page.user.username }}</small> | ||||||
| 		<router-link v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId" :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link> | 		<router-link v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId" :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link> | ||||||
| 		<router-link :to="`./${page.name}/view-source`">{{ $t('view-source') }}</router-link> | 		<router-link :to="`./${page.name}/view-source`">{{ $t('view-source') }}</router-link> | ||||||
|  | 		<div class="like"> | ||||||
|  | 			<button @click="unlike()" v-if="page.isLiked" :title="$t('unlike')"><fa :icon="faHeartS"/></button> | ||||||
|  | 			<button @click="like()" v-else :title="$t('like')"><fa :icon="faHeart"/></button> | ||||||
|  | 			<span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span> | ||||||
|  | 		</div> | ||||||
| 	</footer> | 	</footer> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  | @ -19,8 +24,8 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import i18n from '../../../../i18n'; | import i18n from '../../../../i18n'; | ||||||
| import { faICursor, faPlus } from '@fortawesome/free-solid-svg-icons'; | import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faSave, faStickyNote } from '@fortawesome/free-regular-svg-icons'; | import { faHeart } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import XBlock from './page.block.vue'; | import XBlock from './page.block.vue'; | ||||||
| import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator'; | import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator'; | ||||||
| import { collectPageVars } from '../../../scripts/collect-page-vars'; | import { collectPageVars } from '../../../scripts/collect-page-vars'; | ||||||
|  | @ -76,7 +81,7 @@ export default Vue.extend({ | ||||||
| 		return { | 		return { | ||||||
| 			page: null, | 			page: null, | ||||||
| 			script: null, | 			script: null, | ||||||
| 			faPlus, faICursor, faSave, faStickyNote | 			faHeartS, faHeart | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -103,6 +108,24 @@ export default Vue.extend({ | ||||||
| 		getPageVars() { | 		getPageVars() { | ||||||
| 			return collectPageVars(this.page.content); | 			return collectPageVars(this.page.content); | ||||||
| 		}, | 		}, | ||||||
|  | 
 | ||||||
|  | 		like() { | ||||||
|  | 			this.$root.api('pages/like', { | ||||||
|  | 				pageId: this.page.id, | ||||||
|  | 			}).then(() => { | ||||||
|  | 				this.page.isLiked = true; | ||||||
|  | 				this.page.likedCount++; | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		unlike() { | ||||||
|  | 			this.$root.api('pages/unlike', { | ||||||
|  | 				pageId: this.page.id, | ||||||
|  | 			}).then(() => { | ||||||
|  | 				this.page.isLiked = false; | ||||||
|  | 				this.page.likedCount--; | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | @ -161,4 +184,7 @@ export default Vue.extend({ | ||||||
| 		> a + a | 		> a + a | ||||||
| 			margin-left 8px | 			margin-left 8px | ||||||
| 
 | 
 | ||||||
|  | 		> .like | ||||||
|  | 			margin-top 16px | ||||||
|  | 
 | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
							
								
								
									
										138
									
								
								src/client/app/common/views/pages/pages.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/client/app/common/views/pages/pages.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,138 @@ | ||||||
|  | <template> | ||||||
|  | <div> | ||||||
|  | 	<ui-container :body-togglable="true"> | ||||||
|  | 		<template #header><fa :icon="faEdit" fixed-width/>{{ $t('my-pages') }}</template> | ||||||
|  | 		<div class="rknalgpo" v-if="!fetching"> | ||||||
|  | 			<ui-button class="new" @click="create()"><fa :icon="faPlus"/></ui-button> | ||||||
|  | 			<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="pages"> | ||||||
|  | 				<x-page-preview v-for="page in pages" class="page" :page="page" :key="page.id"/> | ||||||
|  | 			</sequential-entrance> | ||||||
|  | 			<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> | ||||||
|  | 		</div> | ||||||
|  | 	</ui-container> | ||||||
|  | 
 | ||||||
|  | 	<ui-container :body-togglable="true"> | ||||||
|  | 		<template #header><fa :icon="faHeart" fixed-width/>{{ $t('liked-pages') }}</template> | ||||||
|  | 		<div class="rknalgpo" v-if="!fetching"> | ||||||
|  | 			<sequential-entrance animation="entranceFromTop" delay="25" tag="div" class="pages"> | ||||||
|  | 				<x-page-preview v-for="like in likes" class="page" :page="like.page" :key="like.page.id"/> | ||||||
|  | 			</sequential-entrance> | ||||||
|  | 			<ui-button v-if="existMoreLikes" @click="fetchMoreLiked()">{{ $t('@.load-more') }}</ui-button> | ||||||
|  | 		</div> | ||||||
|  | 	</ui-container> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons'; | ||||||
|  | import i18n from '../../../i18n'; | ||||||
|  | import Progress from '../../scripts/loading'; | ||||||
|  | import XPagePreview from '../../views/components/page-preview.vue'; | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	i18n: i18n('pages'), | ||||||
|  | 	components: { | ||||||
|  | 		XPagePreview | ||||||
|  | 	}, | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			fetching: true, | ||||||
|  | 			pages: [], | ||||||
|  | 			existMore: false, | ||||||
|  | 			moreFetching: false, | ||||||
|  | 			likes: [], | ||||||
|  | 			existMoreLikes: false, | ||||||
|  | 			moreLikesFetching: false, | ||||||
|  | 			faStickyNote, faPlus, faEdit, faHeart | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 	created() { | ||||||
|  | 		this.fetch(); | ||||||
|  | 	}, | ||||||
|  | 	methods: { | ||||||
|  | 		async fetch() { | ||||||
|  | 			Progress.start(); | ||||||
|  | 			this.fetching = true; | ||||||
|  | 
 | ||||||
|  | 			const pages = await this.$root.api('i/pages', { | ||||||
|  | 				limit: 11 | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			if (pages.length == 11) { | ||||||
|  | 				this.existMore = true; | ||||||
|  | 				pages.pop(); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			const likes = await this.$root.api('i/page-likes', { | ||||||
|  | 				limit: 11 | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			if (likes.length == 11) { | ||||||
|  | 				this.existMoreLikes = true; | ||||||
|  | 				likes.pop(); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			this.pages = pages; | ||||||
|  | 			this.likes = likes; | ||||||
|  | 			this.fetching = false; | ||||||
|  | 
 | ||||||
|  | 			Progress.done(); | ||||||
|  | 		}, | ||||||
|  | 		fetchMore() { | ||||||
|  | 			this.moreFetching = true; | ||||||
|  | 			this.$root.api('i/pages', { | ||||||
|  | 				limit: 11, | ||||||
|  | 				untilId: this.pages[this.pages.length - 1].id | ||||||
|  | 			}).then(pages => { | ||||||
|  | 				if (pages.length == 11) { | ||||||
|  | 					this.existMore = true; | ||||||
|  | 					pages.pop(); | ||||||
|  | 				} else { | ||||||
|  | 					this.existMore = false; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				this.pages = this.pages.concat(pages); | ||||||
|  | 				this.moreFetching = false; | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 		fetchMoreLiked() { | ||||||
|  | 			this.moreLikesFetching = true; | ||||||
|  | 			this.$root.api('i/page-likes', { | ||||||
|  | 				limit: 11, | ||||||
|  | 				untilId: this.likes[this.likes.length - 1].id | ||||||
|  | 			}).then(pages => { | ||||||
|  | 				if (pages.length == 11) { | ||||||
|  | 					this.existMoreLikes = true; | ||||||
|  | 					pages.pop(); | ||||||
|  | 				} else { | ||||||
|  | 					this.existMoreLikes = false; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				this.likes = this.likes.concat(pages); | ||||||
|  | 				this.moreLikesFetching = false; | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 		create() { | ||||||
|  | 			this.$router.push(`/i/pages/new`); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="stylus" scoped> | ||||||
|  | .rknalgpo | ||||||
|  | 	padding 16px | ||||||
|  | 
 | ||||||
|  | 	> .new | ||||||
|  | 		margin-bottom 16px | ||||||
|  | 
 | ||||||
|  | 	> * > .page | ||||||
|  | 		margin-bottom 8px | ||||||
|  | 
 | ||||||
|  | 	@media (min-width 500px) | ||||||
|  | 		> * > .page | ||||||
|  | 			margin-bottom 16px | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
|  | @ -156,7 +156,7 @@ init(async (launch, os) => { | ||||||
| 					{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, | 					{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, | ||||||
| 					{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, | 					{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, | ||||||
| 					{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }, | 					{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }, | ||||||
| 					{ path: '/i/pages', component: () => import('./views/home/pages.vue').then(m => m.default) }, | 					{ path: '/i/pages', component: () => import('../common/views/pages/pages.vue').then(m => m.default) }, | ||||||
| 				]}, | 				]}, | ||||||
| 			{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) }, | 			{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) }, | ||||||
| 			{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, | 			{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, | ||||||
|  |  | ||||||
|  | @ -1,92 +0,0 @@ | ||||||
| <template> |  | ||||||
| <div class="rknalgpo" v-if="!fetching"> |  | ||||||
| 	<ui-button @click="create()"><fa :icon="faPlus"/></ui-button> |  | ||||||
| 	<sequential-entrance animation="entranceFromTop" delay="25"> |  | ||||||
| 		<template v-for="page in pages"> |  | ||||||
| 			<x-page-preview class="page" :page="page" :key="page.id"/> |  | ||||||
| 		</template> |  | ||||||
| 	</sequential-entrance> |  | ||||||
| 	<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> |  | ||||||
| </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import i18n from '../../../i18n'; |  | ||||||
| import Progress from '../../../common/scripts/loading'; |  | ||||||
| import { faPlus } from '@fortawesome/free-solid-svg-icons'; |  | ||||||
| import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; |  | ||||||
| import XPagePreview from '../../../common/views/components/page-preview.vue'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	i18n: i18n(), |  | ||||||
| 	components: { |  | ||||||
| 		XPagePreview |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			fetching: true, |  | ||||||
| 			pages: [], |  | ||||||
| 			existMore: false, |  | ||||||
| 			moreFetching: false, |  | ||||||
| 			faStickyNote, faPlus |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	created() { |  | ||||||
| 		this.fetch(); |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		fetch() { |  | ||||||
| 			Progress.start(); |  | ||||||
| 			this.fetching = true; |  | ||||||
| 
 |  | ||||||
| 			this.$root.api('i/pages', { |  | ||||||
| 				limit: 11 |  | ||||||
| 			}).then(pages => { |  | ||||||
| 				if (pages.length == 11) { |  | ||||||
| 					this.existMore = true; |  | ||||||
| 					pages.pop(); |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				this.pages = pages; |  | ||||||
| 				this.fetching = false; |  | ||||||
| 
 |  | ||||||
| 				Progress.done(); |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 		fetchMore() { |  | ||||||
| 			this.moreFetching = true; |  | ||||||
| 			this.$root.api('i/pages', { |  | ||||||
| 				limit: 11, |  | ||||||
| 				untilId: this.pages[this.pages.length - 1].id |  | ||||||
| 			}).then(pages => { |  | ||||||
| 				if (pages.length == 11) { |  | ||||||
| 					this.existMore = true; |  | ||||||
| 					pages.pop(); |  | ||||||
| 				} else { |  | ||||||
| 					this.existMore = false; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				this.pages = this.pages.concat(pages); |  | ||||||
| 				this.moreFetching = false; |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 		create() { |  | ||||||
| 			this.$router.push(`/i/pages/new`); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="stylus" scoped> |  | ||||||
| .rknalgpo |  | ||||||
| 	margin 0 auto |  | ||||||
| 
 |  | ||||||
| 	> * > .page |  | ||||||
| 		margin-bottom 8px |  | ||||||
| 
 |  | ||||||
| 	@media (min-width 500px) |  | ||||||
| 		> * > .page |  | ||||||
| 			margin-bottom 16px |  | ||||||
| 
 |  | ||||||
| </style> |  | ||||||
|  | @ -3,92 +3,27 @@ | ||||||
| 	<template #header><span style="margin-right:4px;"><fa :icon="faStickyNote"/></span>{{ $t('@.pages') }}</template> | 	<template #header><span style="margin-right:4px;"><fa :icon="faStickyNote"/></span>{{ $t('@.pages') }}</template> | ||||||
| 
 | 
 | ||||||
| 	<main> | 	<main> | ||||||
| 		<ui-button @click="create()"><fa :icon="faPlus"/></ui-button> | 		<x-pages v-bind="$attrs"/> | ||||||
| 		<sequential-entrance animation="entranceFromTop" delay="25"> |  | ||||||
| 			<template v-for="page in pages"> |  | ||||||
| 				<x-page-preview class="page" :page="page" :key="page.id"/> |  | ||||||
| 			</template> |  | ||||||
| 		</sequential-entrance> |  | ||||||
| 		<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> |  | ||||||
| 	</main> | 	</main> | ||||||
| </mk-ui> | </mk-ui> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import i18n from '../../../i18n'; | import i18n from '../../../i18n'; | ||||||
| import Progress from '../../../common/scripts/loading'; | import { faHashtag } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faPlus } from '@fortawesome/free-solid-svg-icons'; | import XPages from '../../../common/views/pages/pages.vue'; | ||||||
| import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; |  | ||||||
| import XPagePreview from '../../../common/views/components/page-preview.vue'; |  | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	i18n: i18n(), | 	i18n: i18n(''), | ||||||
| 	components: { | 	components: { | ||||||
| 		XPagePreview | 		XPages | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			fetching: true, | 			faHashtag | ||||||
| 			pages: [], |  | ||||||
| 			existMore: false, |  | ||||||
| 			moreFetching: false, |  | ||||||
| 			faStickyNote, faPlus |  | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	created() { |  | ||||||
| 		this.fetch(); |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		fetch() { |  | ||||||
| 			Progress.start(); |  | ||||||
| 			this.fetching = true; |  | ||||||
| 
 |  | ||||||
| 			this.$root.api('i/pages', { |  | ||||||
| 				limit: 11 |  | ||||||
| 			}).then(pages => { |  | ||||||
| 				if (pages.length == 11) { |  | ||||||
| 					this.existMore = true; |  | ||||||
| 					pages.pop(); |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				this.pages = pages; |  | ||||||
| 				this.fetching = false; |  | ||||||
| 
 |  | ||||||
| 				Progress.done(); |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 		fetchMore() { |  | ||||||
| 			this.moreFetching = true; |  | ||||||
| 			this.$root.api('i/pages', { |  | ||||||
| 				limit: 11, |  | ||||||
| 				untilId: this.pages[this.pages.length - 1].id |  | ||||||
| 			}).then(pages => { |  | ||||||
| 				if (pages.length == 11) { |  | ||||||
| 					this.existMore = true; |  | ||||||
| 					pages.pop(); |  | ||||||
| 				} else { |  | ||||||
| 					this.existMore = false; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				this.pages = this.pages.concat(pages); |  | ||||||
| 				this.moreFetching = false; |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 		create() { |  | ||||||
| 			this.$router.push(`/i/pages/new`); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 |  | ||||||
| <style lang="stylus" scoped> |  | ||||||
| main |  | ||||||
| 	> * > .page |  | ||||||
| 		margin-bottom 8px |  | ||||||
| 
 |  | ||||||
| 	@media (min-width 500px) |  | ||||||
| 		> * > .page |  | ||||||
| 			margin-bottom 16px |  | ||||||
| 
 |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
|  | @ -41,6 +41,7 @@ import { UserKeypair } from '../models/entities/user-keypair'; | ||||||
| import { UserPublickey } from '../models/entities/user-publickey'; | import { UserPublickey } from '../models/entities/user-publickey'; | ||||||
| import { UserProfile } from '../models/entities/user-profile'; | import { UserProfile } from '../models/entities/user-profile'; | ||||||
| import { Page } from '../models/entities/page'; | import { Page } from '../models/entities/page'; | ||||||
|  | import { PageLike } from '../models/entities/page-like'; | ||||||
| 
 | 
 | ||||||
| const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); | const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); | ||||||
| 
 | 
 | ||||||
|  | @ -116,6 +117,7 @@ export function initDb(justBorrow = false, sync = false, log = false) { | ||||||
| 			NoteWatching, | 			NoteWatching, | ||||||
| 			NoteUnread, | 			NoteUnread, | ||||||
| 			Page, | 			Page, | ||||||
|  | 			PageLike, | ||||||
| 			Log, | 			Log, | ||||||
| 			DriveFile, | 			DriveFile, | ||||||
| 			DriveFolder, | 			DriveFolder, | ||||||
|  |  | ||||||
							
								
								
									
										33
									
								
								src/models/entities/page-like.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/models/entities/page-like.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; | ||||||
|  | import { User } from './user'; | ||||||
|  | import { id } from '../id'; | ||||||
|  | import { Page } from './page'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | @Index(['userId', 'pageId'], { unique: true }) | ||||||
|  | export class PageLike { | ||||||
|  | 	@PrimaryColumn(id()) | ||||||
|  | 	public id: string; | ||||||
|  | 
 | ||||||
|  | 	@Column('timestamp with time zone') | ||||||
|  | 	public createdAt: Date; | ||||||
|  | 
 | ||||||
|  | 	@Index() | ||||||
|  | 	@Column(id()) | ||||||
|  | 	public userId: User['id']; | ||||||
|  | 
 | ||||||
|  | 	@ManyToOne(type => User, { | ||||||
|  | 		onDelete: 'CASCADE' | ||||||
|  | 	}) | ||||||
|  | 	@JoinColumn() | ||||||
|  | 	public user: User | null; | ||||||
|  | 
 | ||||||
|  | 	@Column(id()) | ||||||
|  | 	public pageId: Page['id']; | ||||||
|  | 
 | ||||||
|  | 	@ManyToOne(type => Page, { | ||||||
|  | 		onDelete: 'CASCADE' | ||||||
|  | 	}) | ||||||
|  | 	@JoinColumn() | ||||||
|  | 	public page: Page | null; | ||||||
|  | } | ||||||
|  | @ -95,6 +95,11 @@ export class Page { | ||||||
| 	}) | 	}) | ||||||
| 	public visibleUserIds: User['id'][]; | 	public visibleUserIds: User['id'][]; | ||||||
| 
 | 
 | ||||||
|  | 	@Column('integer', { | ||||||
|  | 		default: 0 | ||||||
|  | 	}) | ||||||
|  | 	public likedCount: number; | ||||||
|  | 
 | ||||||
| 	constructor(data: Partial<Page>) { | 	constructor(data: Partial<Page>) { | ||||||
| 		if (data == null) return; | 		if (data == null) return; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -36,6 +36,7 @@ import { AuthSessionRepository } from './repositories/auth-session'; | ||||||
| import { UserProfile } from './entities/user-profile'; | import { UserProfile } from './entities/user-profile'; | ||||||
| import { HashtagRepository } from './repositories/hashtag'; | import { HashtagRepository } from './repositories/hashtag'; | ||||||
| import { PageRepository } from './repositories/page'; | import { PageRepository } from './repositories/page'; | ||||||
|  | import { PageLikeRepository } from './repositories/page-like'; | ||||||
| 
 | 
 | ||||||
| export const Apps = getCustomRepository(AppRepository); | export const Apps = getCustomRepository(AppRepository); | ||||||
| export const Notes = getCustomRepository(NoteRepository); | export const Notes = getCustomRepository(NoteRepository); | ||||||
|  | @ -74,3 +75,4 @@ export const ReversiGames = getCustomRepository(ReversiGameRepository); | ||||||
| export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository); | export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository); | ||||||
| export const Logs = getRepository(Log); | export const Logs = getRepository(Log); | ||||||
| export const Pages = getCustomRepository(PageRepository); | export const Pages = getCustomRepository(PageRepository); | ||||||
|  | export const PageLikes = getCustomRepository(PageLikeRepository); | ||||||
|  |  | ||||||
							
								
								
									
										26
									
								
								src/models/repositories/page-like.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/models/repositories/page-like.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | ||||||
|  | import { EntityRepository, Repository } from 'typeorm'; | ||||||
|  | import { PageLike } from '../entities/page-like'; | ||||||
|  | import { Pages } from '..'; | ||||||
|  | import { ensure } from '../../prelude/ensure'; | ||||||
|  | 
 | ||||||
|  | @EntityRepository(PageLike) | ||||||
|  | export class PageLikeRepository extends Repository<PageLike> { | ||||||
|  | 	public async pack( | ||||||
|  | 		src: PageLike['id'] | PageLike, | ||||||
|  | 		me?: any | ||||||
|  | 	) { | ||||||
|  | 		const like = typeof src === 'object' ? src : await this.findOne(src).then(ensure); | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			id: like.id, | ||||||
|  | 			page: await Pages.pack(like.page || like.pageId, me), | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public packMany( | ||||||
|  | 		likes: any[], | ||||||
|  | 		me: any | ||||||
|  | 	) { | ||||||
|  | 		return Promise.all(likes.map(x => this.pack(x, me))); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -1,24 +1,30 @@ | ||||||
| import { EntityRepository, Repository } from 'typeorm'; | import { EntityRepository, Repository } from 'typeorm'; | ||||||
| import { Page } from '../entities/page'; | import { Page } from '../entities/page'; | ||||||
| import { SchemaType, types, bool } from '../../misc/schema'; | import { SchemaType, types, bool } from '../../misc/schema'; | ||||||
| import { Users, DriveFiles } from '..'; | import { Users, DriveFiles, PageLikes } from '..'; | ||||||
| import { awaitAll } from '../../prelude/await-all'; | import { awaitAll } from '../../prelude/await-all'; | ||||||
| import { DriveFile } from '../entities/drive-file'; | import { DriveFile } from '../entities/drive-file'; | ||||||
|  | import { User } from '../entities/user'; | ||||||
|  | import { ensure } from '../../prelude/ensure'; | ||||||
| 
 | 
 | ||||||
| export type PackedPage = SchemaType<typeof packedPageSchema>; | export type PackedPage = SchemaType<typeof packedPageSchema>; | ||||||
| 
 | 
 | ||||||
| @EntityRepository(Page) | @EntityRepository(Page) | ||||||
| export class PageRepository extends Repository<Page> { | export class PageRepository extends Repository<Page> { | ||||||
| 	public async pack( | 	public async pack( | ||||||
| 		src: Page, | 		src: Page['id'] | Page, | ||||||
|  | 		me?: User['id'] | User | null | undefined, | ||||||
| 	): Promise<PackedPage> { | 	): Promise<PackedPage> { | ||||||
|  | 		const meId = me ? typeof me === 'string' ? me : me.id : null; | ||||||
|  | 		const page = typeof src === 'object' ? src : await this.findOne(src).then(ensure); | ||||||
|  | 
 | ||||||
| 		const attachedFiles: Promise<DriveFile | undefined>[] = []; | 		const attachedFiles: Promise<DriveFile | undefined>[] = []; | ||||||
| 		const collectFile = (xs: any[]) => { | 		const collectFile = (xs: any[]) => { | ||||||
| 			for (const x of xs) { | 			for (const x of xs) { | ||||||
| 				if (x.type === 'image') { | 				if (x.type === 'image') { | ||||||
| 					attachedFiles.push(DriveFiles.findOne({ | 					attachedFiles.push(DriveFiles.findOne({ | ||||||
| 						id: x.fileId, | 						id: x.fileId, | ||||||
| 						userId: src.userId | 						userId: page.userId | ||||||
| 					})); | 					})); | ||||||
| 				} | 				} | ||||||
| 				if (x.children) { | 				if (x.children) { | ||||||
|  | @ -26,7 +32,7 @@ export class PageRepository extends Repository<Page> { | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		}; | 		}; | ||||||
| 		collectFile(src.content); | 		collectFile(page.content); | ||||||
| 
 | 
 | ||||||
| 		// 後方互換性のため
 | 		// 後方互換性のため
 | ||||||
| 		let migrated = false; | 		let migrated = false; | ||||||
|  | @ -47,29 +53,31 @@ export class PageRepository extends Repository<Page> { | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		}; | 		}; | ||||||
| 		migrate(src.content); | 		migrate(page.content); | ||||||
| 		if (migrated) { | 		if (migrated) { | ||||||
| 			this.update(src.id, { | 			this.update(page.id, { | ||||||
| 				content: src.content | 				content: page.content | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return await awaitAll({ | 		return await awaitAll({ | ||||||
| 			id: src.id, | 			id: page.id, | ||||||
| 			createdAt: src.createdAt.toISOString(), | 			createdAt: page.createdAt.toISOString(), | ||||||
| 			updatedAt: src.updatedAt.toISOString(), | 			updatedAt: page.updatedAt.toISOString(), | ||||||
| 			userId: src.userId, | 			userId: page.userId, | ||||||
| 			user: Users.pack(src.user || src.userId), | 			user: Users.pack(page.user || page.userId), | ||||||
| 			content: src.content, | 			content: page.content, | ||||||
| 			variables: src.variables, | 			variables: page.variables, | ||||||
| 			title: src.title, | 			title: page.title, | ||||||
| 			name: src.name, | 			name: page.name, | ||||||
| 			summary: src.summary, | 			summary: page.summary, | ||||||
| 			alignCenter: src.alignCenter, | 			alignCenter: page.alignCenter, | ||||||
| 			font: src.font, | 			font: page.font, | ||||||
| 			eyeCatchingImageId: src.eyeCatchingImageId, | 			eyeCatchingImageId: page.eyeCatchingImageId, | ||||||
| 			eyeCatchingImage: src.eyeCatchingImageId ? await DriveFiles.pack(src.eyeCatchingImageId) : null, | 			eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null, | ||||||
| 			attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)) | 			attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)), | ||||||
|  | 			likedCount: page.likedCount, | ||||||
|  | 			isLiked: meId ? await PageLikes.findOne({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										45
									
								
								src/server/api/endpoints/i/page-likes.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/server/api/endpoints/i/page-likes.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import { ID } from '../../../../misc/cafy-id'; | ||||||
|  | import define from '../../define'; | ||||||
|  | import { PageLikes } from '../../../../models'; | ||||||
|  | import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '「いいね」したページ一覧を取得します。', | ||||||
|  | 		'en-US': 'Get liked pages' | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	tags: ['account', 'pages'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 
 | ||||||
|  | 	kind: 'read:page-likes', | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		limit: { | ||||||
|  | 			validator: $.optional.num.range(1, 100), | ||||||
|  | 			default: 10 | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		sinceId: { | ||||||
|  | 			validator: $.optional.type(ID), | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		untilId: { | ||||||
|  | 			validator: $.optional.type(ID), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, user) => { | ||||||
|  | 	const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId) | ||||||
|  | 		.andWhere(`like.userId = :meId`, { meId: user.id }) | ||||||
|  | 		.leftJoinAndSelect('like.page', 'page'); | ||||||
|  | 
 | ||||||
|  | 	const likes = await query | ||||||
|  | 		.take(ps.limit!) | ||||||
|  | 		.getMany(); | ||||||
|  | 
 | ||||||
|  | 	return await PageLikes.packMany(likes, user); | ||||||
|  | }); | ||||||
							
								
								
									
										79
									
								
								src/server/api/endpoints/pages/like.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/server/api/endpoints/pages/like.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import { ID } from '../../../../misc/cafy-id'; | ||||||
|  | import define from '../../define'; | ||||||
|  | import { ApiError } from '../../error'; | ||||||
|  | import { Pages, PageLikes } from '../../../../models'; | ||||||
|  | import { genId } from '../../../../misc/gen-id'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '指定したページを「いいね」します。', | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	tags: ['pages'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 
 | ||||||
|  | 	kind: 'write:page-likes', | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		pageId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': '対象のページのID', | ||||||
|  | 				'en-US': 'Target page ID.' | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 		noSuchPage: { | ||||||
|  | 			message: 'No such page.', | ||||||
|  | 			code: 'NO_SUCH_PAGE', | ||||||
|  | 			id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		yourPage: { | ||||||
|  | 			message: 'You cannot like your page.', | ||||||
|  | 			code: 'YOUR_PAGE', | ||||||
|  | 			id: '28800466-e6db-40f2-8fae-bf9e82aa92b8' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		alreadyLiked: { | ||||||
|  | 			message: 'The page has already been liked.', | ||||||
|  | 			code: 'ALREADY_LIKED', | ||||||
|  | 			id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3' | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, user) => { | ||||||
|  | 	const page = await Pages.findOne(ps.pageId); | ||||||
|  | 	if (page == null) { | ||||||
|  | 		throw new ApiError(meta.errors.noSuchPage); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (page.userId === user.id) { | ||||||
|  | 		throw new ApiError(meta.errors.yourPage); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// if already liked
 | ||||||
|  | 	const exist = await PageLikes.findOne({ | ||||||
|  | 		pageId: page.id, | ||||||
|  | 		userId: user.id | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (exist != null) { | ||||||
|  | 		throw new ApiError(meta.errors.alreadyLiked); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Create like
 | ||||||
|  | 	await PageLikes.save({ | ||||||
|  | 		id: genId(), | ||||||
|  | 		createdAt: new Date(), | ||||||
|  | 		pageId: page.id, | ||||||
|  | 		userId: user.id | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	Pages.increment({ id: page.id }, 'likedCount', 1); | ||||||
|  | }); | ||||||
|  | @ -70,5 +70,5 @@ export default define(meta, async (ps, user) => { | ||||||
| 		throw new ApiError(meta.errors.noSuchPage); | 		throw new ApiError(meta.errors.noSuchPage); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return await Pages.pack(page); | 	return await Pages.pack(page, user); | ||||||
| }); | }); | ||||||
|  |  | ||||||
							
								
								
									
										62
									
								
								src/server/api/endpoints/pages/unlike.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/server/api/endpoints/pages/unlike.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import { ID } from '../../../../misc/cafy-id'; | ||||||
|  | import define from '../../define'; | ||||||
|  | import { ApiError } from '../../error'; | ||||||
|  | import { Pages, PageLikes } from '../../../../models'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '指定したページの「いいね」を解除します。', | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	tags: ['pages'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 
 | ||||||
|  | 	kind: 'write:page-likes', | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		pageId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': '対象のページのID', | ||||||
|  | 				'en-US': 'Target page ID.' | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 		noSuchPage: { | ||||||
|  | 			message: 'No such page.', | ||||||
|  | 			code: 'NO_SUCH_PAGE', | ||||||
|  | 			id: 'a0d41e20-1993-40bd-890e-f6e560ae648e' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		notLiked: { | ||||||
|  | 			message: 'You have not liked that page.', | ||||||
|  | 			code: 'NOT_LIKED', | ||||||
|  | 			id: 'f5e586b0-ce93-4050-b0e3-7f31af5259ee' | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, user) => { | ||||||
|  | 	const page = await Pages.findOne(ps.pageId); | ||||||
|  | 	if (page == null) { | ||||||
|  | 		throw new ApiError(meta.errors.noSuchPage); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const exist = await PageLikes.findOne({ | ||||||
|  | 		pageId: page.id, | ||||||
|  | 		userId: user.id | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (exist == null) { | ||||||
|  | 		throw new ApiError(meta.errors.notLiked); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Delete like
 | ||||||
|  | 	await PageLikes.delete(exist.id); | ||||||
|  | 
 | ||||||
|  | 	Pages.decrement({ id: page.id }, 'likedCount', 1); | ||||||
|  | }); | ||||||
|  | @ -21,4 +21,6 @@ export const kinds = [ | ||||||
| 	'write:votes', | 	'write:votes', | ||||||
| 	'read:pages', | 	'read:pages', | ||||||
| 	'write:pages', | 	'write:pages', | ||||||
|  | 	'write:page-likes', | ||||||
|  | 	'read:page-likes', | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue