added PageTop component, markdown styles, white background on logo svg, and documentation comments
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
src-manager/
|
src-manager/
|
||||||
|
docs/
|
||||||
@@ -27,3 +27,4 @@ src-manager
|
|||||||
*.md
|
*.md
|
||||||
eslint.config.js
|
eslint.config.js
|
||||||
nuxt.config.ts
|
nuxt.config.ts
|
||||||
|
docs
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
このレポジトリには2つのプロジェクトが存在する:
|
このレポジトリには2つのプロジェクトが存在する:
|
||||||
|
|
||||||
1. SERA Website: メインプロジェクト、ソースディレクトリ:`assets/ components/ layouts/ pages/ public/ server/`
|
1. SERA Website: メインプロジェクト、ソースディレクトリ:`assets/ components/ composables/ layouts/ pages/ public/ server/ utils/`
|
||||||
2. Content Manager: ニュース等のためのデータベースの管理UI・API、ソースディレクトリ:`src-manager/`
|
2. Content Manager: ニュース等のためのデータベースの管理UI・API、ソースディレクトリ:`src-manager/`
|
||||||
|
|
||||||
## 問題の報告・新仕様の提案
|
## 問題の報告・新仕様の提案
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
* 変数、定数、型名、関数名での連番は極力避ける
|
* 変数、定数、型名、関数名での連番は極力避ける
|
||||||
* 変数、定数、型名、関数名には3文字以上の略称でない意味のある名前をつける
|
* 変数、定数、型名、関数名には3文字以上の略称でない意味のある名前をつける
|
||||||
* 変数、定数、関数名にはcamelCase
|
* 変数、定数、関数名にはcamelCase
|
||||||
* クラス名、型名、Vueコンポーネントファイル名・importにはPascalCase
|
* クラス名、型名、`components/`下のVueコンポーネントファイル名にはPascalCase
|
||||||
* CSSクラス、id、`pages/`下のVueファイル・フォルダにはkebab-case
|
* CSSクラス、id、`pages/`下のVueファイル・フォルダにはkebab-case
|
||||||
* 上記に該当しないものはcamelCaseで命名するものとする
|
* 上記に該当しないものはcamelCaseで命名するものとする
|
||||||
|
|
||||||
@@ -117,8 +117,8 @@
|
|||||||
|
|
||||||
* コメントは英語、日本語どちらでも良い
|
* コメントは英語、日本語どちらでも良い
|
||||||
* 無駄で間違ったコメントは書かない(参照:[Don't Write Comments](https://www.youtube.com/watch?v=Bf7vDBBOBUA&ab_channel=CodeAesthetic))
|
* 無駄で間違ったコメントは書かない(参照:[Don't Write Comments](https://www.youtube.com/watch?v=Bf7vDBBOBUA&ab_channel=CodeAesthetic))
|
||||||
* 仕様、動作についてはコメントではなくドキュメンテーションに記載する
|
* 仕様、使い方についてはコメントではなくドキュメンテーションに記載する
|
||||||
* APIには簡潔なドキュメンテーションを書く([JSdoc](https://www.typescriptlang.org/ja/docs/handbook/jsdoc-supported-types.html))
|
* APIには簡潔な英語のドキュメンテーションを書く([JSdoc](https://www.typescriptlang.org/ja/docs/handbook/jsdoc-supported-types.html))
|
||||||
|
|
||||||
#### その他
|
#### その他
|
||||||
|
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -9,6 +9,10 @@
|
|||||||
* node npm
|
* node npm
|
||||||
* sqlite
|
* sqlite
|
||||||
|
|
||||||
|
## コンテンツの管理
|
||||||
|
|
||||||
|
ニュース等のコンテンツの管理は`src-manager/`内の`README.md`を参照すること
|
||||||
|
|
||||||
## 開発を開始する
|
## 開発を開始する
|
||||||
|
|
||||||
> 初めてこのプロジェクトに参加する際には`CONTRIBUTING.md`を***必ず***読んでおくこと
|
> 初めてこのプロジェクトに参加する際には`CONTRIBUTING.md`を***必ず***読んでおくこと
|
||||||
@@ -17,5 +21,13 @@
|
|||||||
git clone https://git.kenryu.us/kenryuS/sera-new-hp.git # レポジトリをクローン
|
git clone https://git.kenryu.us/kenryuS/sera-new-hp.git # レポジトリをクローン
|
||||||
cd sera-new-hp # 移動して
|
cd sera-new-hp # 移動して
|
||||||
npm install # 依存パッケージのインストール
|
npm install # 依存パッケージのインストール
|
||||||
npm run dev # デベロッパーモードでサイトを構築
|
npm run dev # デベロッパーモードでサーバーを起動
|
||||||
|
npm run generate # 静的サイトを生成
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 便利・重要なファイル/フォルダ
|
||||||
|
|
||||||
|
* `assets/databases/news.db`: ニュースを管理しているsqliteデータベース
|
||||||
|
* `assets/siteinfo.json`: 部長と顧問の名前、コピーライトの年、メンバーの学科・学年ごとの人数、など更新があまりされない情報を集めたファイル、`import`して使う
|
||||||
|
* `docs/`: `typedoc`で生成されたドキュメンテーションが入っている、`python -m http.server`などで`localhost`にホストして読む
|
||||||
|
* `dist/`, `.output/`: `npm run generate`で生成された静的ウェブサイト本体、プロダクションレディーな状態 `dist/`は`.output/`へのリンクである
|
||||||
|
|||||||
Binary file not shown.
@@ -3,21 +3,21 @@
|
|||||||
"clubNameLong": "Space Engineering Research Association",
|
"clubNameLong": "Space Engineering Research Association",
|
||||||
"copyrightYear": 2024,
|
"copyrightYear": 2024,
|
||||||
"memberDepartmentRatio": {
|
"memberDepartmentRatio": {
|
||||||
"date": "2023-3-31",
|
"date": "2024-9-17",
|
||||||
"mechanicalEng": 10,
|
"mechanicalEng": 12,
|
||||||
"elecAndComp": 5,
|
"elecAndComp": 3,
|
||||||
"elecControl": 4,
|
"elecControl": 14,
|
||||||
"civilEng": 1,
|
"civilEng": 2,
|
||||||
"architecture": 2
|
"architecture": 3
|
||||||
},
|
},
|
||||||
"memberGradeRatio": {
|
"memberGradeRatio": {
|
||||||
"date": "2022-3-31",
|
"date": "2024-9-17",
|
||||||
"first": 7,
|
"first": 14,
|
||||||
"second": 4,
|
"second": 11,
|
||||||
"third": 5,
|
"third": 7,
|
||||||
"fourth": 1,
|
"fourth": 1,
|
||||||
"fifth": 5
|
"fifth": 1
|
||||||
},
|
},
|
||||||
"advisorName": "枝本雅史(えだもとまさふみ)",
|
"advisorName": "佐藤敦(さとうあつい)",
|
||||||
"headOfClub": "4年機械工学科 櫻井晴生"
|
"headOfClub": "5年機械工学科 瀨 仁一郎"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "~/assets/styles/color-pallet.css";
|
@import "~/assets/styles/color-pallet.css";
|
||||||
|
@import "~/assets/styles/markdown.css";
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Noto Sans JP";
|
font-family: "Noto Sans JP";
|
||||||
@@ -15,15 +16,20 @@
|
|||||||
|
|
||||||
.page-enter-active,
|
.page-enter-active,
|
||||||
.page-leave-active {
|
.page-leave-active {
|
||||||
transition: all ease-in-out 0.6s;
|
transition: all ease-in-out 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-enter-from,
|
.page-enter-from,
|
||||||
.page-leave-to {
|
.page-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
/* Colors form https://simplicable.com/colors/ */
|
/* Colors form https://simplicable.com/colors/ */
|
||||||
:root {
|
:root {
|
||||||
--deep-space: #000001;
|
|
||||||
--moonlight: #f6eed5;
|
|
||||||
--andromeda: #abcdee;
|
--andromeda: #abcdee;
|
||||||
--meteorite: #4a3b6a;
|
|
||||||
--astronaut: #214559;
|
--astronaut: #214559;
|
||||||
|
--comet2: #6e6970;
|
||||||
|
--deep-space: #000001;
|
||||||
|
--martian-moon: #c3e9d3;
|
||||||
|
--meteorite: #4a3b6a;
|
||||||
|
--moonlight: #f6eed5;
|
||||||
--neptune1: #007dac;
|
--neptune1: #007dac;
|
||||||
--starlight: #efefe8;
|
--neptune2: #7fbb9e;
|
||||||
--sunlight: #fff8df;
|
|
||||||
--ocean-blue: #009dc4;
|
--ocean-blue: #009dc4;
|
||||||
}
|
--starlight1: #dde2e6;
|
||||||
|
--starlight5: #bcc0cc;
|
||||||
|
--starlight: #efefe8;
|
||||||
|
--starship: #e3dd39;
|
||||||
|
--sun2: #ef8e38;
|
||||||
|
--sunlight: #fff8df;
|
||||||
|
--uranus: #ace5ee;
|
||||||
|
--venus1: #eed053;
|
||||||
|
--venus2: #397c80;
|
||||||
|
}
|
||||||
|
|||||||
146
assets/styles/markdown.css
Normal file
146
assets/styles/markdown.css
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
.markdown *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
display: grid;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > h1 {
|
||||||
|
color: var(--neptune1);
|
||||||
|
font-size: 2.5em;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > h2,
|
||||||
|
.markdown > h3,
|
||||||
|
.markdown > h4,
|
||||||
|
.markdown > h5,
|
||||||
|
.markdown > h6 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > h2 {
|
||||||
|
color: var(--neptune2);
|
||||||
|
font-size: 2.125em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > h3 {
|
||||||
|
color: var(--venus2);
|
||||||
|
font-size: 1.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > h4 {
|
||||||
|
color: var(--venus2);
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > h5,
|
||||||
|
.markdown > h6 {
|
||||||
|
color: var(--venus2);
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > small {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > p {
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > p:has(img) {
|
||||||
|
width: fit-content;
|
||||||
|
margin: auto;
|
||||||
|
& > img {
|
||||||
|
width: 32rem;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > p > em,
|
||||||
|
.markdown > p > strong {
|
||||||
|
color: var(--neptune1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > p > em:has(strong) {
|
||||||
|
color: var(--sun2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > blockquote {
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
|
color: var(--deep-space);
|
||||||
|
background-color: var(--starlight5);
|
||||||
|
padding: 1em 2em;
|
||||||
|
border-radius: 1em;
|
||||||
|
margin-left: 0;
|
||||||
|
&::after {
|
||||||
|
--offset-y: 15px;
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
width: 3px;
|
||||||
|
height: calc(100% - (var(--offset-y) * 2));
|
||||||
|
background-color: var(--neptune1);
|
||||||
|
top: var(--offset-y);
|
||||||
|
left: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > figcaption {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > ul li,
|
||||||
|
.markdown > ol li {
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > ul li::marker,
|
||||||
|
.markdown > ol li::marker {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--neptune1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > table {
|
||||||
|
min-width: 128px;
|
||||||
|
border: var(--deep-space) 1px solid;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: auto;
|
||||||
|
& > thead {
|
||||||
|
background: var(--comet2);
|
||||||
|
color: var(--starlight);
|
||||||
|
}
|
||||||
|
& > tbody > tr {
|
||||||
|
background: var(--starlight1);
|
||||||
|
}
|
||||||
|
& > tbody > tr:nth-of-type(even) {
|
||||||
|
background: var(--starlight);
|
||||||
|
}
|
||||||
|
& th,
|
||||||
|
& td {
|
||||||
|
padding: 0 0.25em;
|
||||||
|
border: var(--deep-space) 1px solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.markdown > p:has(img) > img {
|
||||||
|
width: 70vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
.markdown > h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -133,6 +133,7 @@ const handleFocusOutEvent = () => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: auto;
|
top: auto;
|
||||||
|
z-index: 100;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
|
|||||||
149
components/NewsCard.vue
Normal file
149
components/NewsCard.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NewsCardProperty } from "~/utils/newsCard";
|
||||||
|
import { marked } from "marked";
|
||||||
|
|
||||||
|
const property = defineProps<NewsCardProperty>();
|
||||||
|
|
||||||
|
const datePosted = new Date(
|
||||||
|
property.newsEntry.date as number
|
||||||
|
).toLocaleDateString("ja-JP", { dateStyle: "medium" });
|
||||||
|
|
||||||
|
const coverImagePath = ref<string>(property.newsEntry.coverImagePath ? property.newsEntry.coverImagePath : "/sera-logo-text.svg");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="news-card" :style="{ backgroundImage: `url(${coverImagePath})`}">
|
||||||
|
<NuxtLink
|
||||||
|
class="card-content"
|
||||||
|
:to="property.newsEntry.linkPath"
|
||||||
|
v-if="
|
||||||
|
property.newsEntry.linkPath !== null &&
|
||||||
|
property.newsEntry.entryType === EntryType.Article
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p class="news-type article">記事</p>
|
||||||
|
<p class="new" v-if="property.isNew">NEW!</p>
|
||||||
|
<p class="content">
|
||||||
|
<article
|
||||||
|
v-html="marked.parse(property.newsEntry.cardContent as string)"
|
||||||
|
></article>
|
||||||
|
</p>
|
||||||
|
<small>{{ datePosted }}</small>
|
||||||
|
</NuxtLink>
|
||||||
|
<div class="card-content" v-else>
|
||||||
|
<p class="news-type tweet">お知らせ</p>
|
||||||
|
<p class="new" v-if="property.isNew">NEW!</p>
|
||||||
|
<p class="content">
|
||||||
|
<article v-html="marked.parse(property.newsEntry.cardContent as string)"></article>
|
||||||
|
</p>
|
||||||
|
<small>{{ datePosted }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.news-card {
|
||||||
|
width: 15rem;
|
||||||
|
height: 18rem;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 10px 5px 5px var(--starlight1);
|
||||||
|
background: inherit;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: local;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
display: block;
|
||||||
|
border-radius: 1rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255,255,255,0.15);
|
||||||
|
backdrop-filter: blur(5px) brightness(55%);
|
||||||
|
color: var(--starlight);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content .content {
|
||||||
|
font-size: larger;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-top: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content article,
|
||||||
|
.card-content img {
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content img {
|
||||||
|
margin-top: 3rem;
|
||||||
|
width: 10rem;
|
||||||
|
height: auto;
|
||||||
|
padding: 0.25rem;
|
||||||
|
margin-left: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content img[src="/sera-logo-text.svg"] {
|
||||||
|
background-color: var(--starlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content .news-type {
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
width: 6rem;
|
||||||
|
height: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
border-top-left-radius: 1rem;
|
||||||
|
border-bottom-right-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content .tweet {
|
||||||
|
background-color: var(--venus2);
|
||||||
|
color: var(--venus1);;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content .article {
|
||||||
|
background-color: var(--venus1);
|
||||||
|
color: var(--astronaut);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content .new {
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
right: 0;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
width: 4rem;
|
||||||
|
height: 2rem;
|
||||||
|
background-color: var(--sun2);
|
||||||
|
color: var(--astronaut);
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
border-top-right-radius: 1rem;
|
||||||
|
border-bottom-left-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content small {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 6rem;
|
||||||
|
height: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--starship);
|
||||||
|
color: var(--astronaut);
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-top-left-radius: 1rem;
|
||||||
|
border-bottom-right-radius: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>Component: newslist</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
29
components/PageTop.vue
Normal file
29
components/PageTop.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PageTopProperty } from "~/utils/pageTop";
|
||||||
|
|
||||||
|
const property = defineProps<PageTopProperty>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :style="{ backgroundImage: `url(${property.imagePath})` }">
|
||||||
|
{{ property.text }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
width: 100vw;
|
||||||
|
height: 25vh;
|
||||||
|
margin: 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: local;
|
||||||
|
background-size: cover;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--starlight1);
|
||||||
|
font-size: 36pt;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -30,7 +30,9 @@ const showThePast = (event: Event) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="top-column">
|
<div class="top-column">
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<h3>{{ SiteInfo.clubNameLong }}</h3>
|
<h3>
|
||||||
|
岐阜高専宇宙工学研究会 - {{ SiteInfo.clubNameLong }}
|
||||||
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
宇宙分野に興味ある学生が<wbr />集い、<wbr />宇宙理工学に<wbr />関する知識を<wbr />身に付けると共に、<wbr />
|
宇宙分野に興味ある学生が<wbr />集い、<wbr />宇宙理工学に<wbr />関する知識を<wbr />身に付けると共に、<wbr />
|
||||||
宇宙分野に関連する<wbr />各種競技会へ<wbr />参加して<wbr />人間力と実践力を<wbr />養うことを目的に<wbr />活動しています。
|
宇宙分野に関連する<wbr />各種競技会へ<wbr />参加して<wbr />人間力と実践力を<wbr />養うことを目的に<wbr />活動しています。
|
||||||
@@ -181,6 +183,7 @@ footer {
|
|||||||
& svg {
|
& svg {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,12 +49,17 @@ const mediaDropDownEntries: Array<DropDownEntry> = [
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
header {
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
.navigation-menu {
|
.navigation-menu {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
padding: 1.25rem 3rem;
|
padding: 1.25rem 3rem;
|
||||||
|
width: calc(100% - 6rem);
|
||||||
height: 64px;
|
height: 64px;
|
||||||
background: var(--deep-space);
|
background: var(--deep-space);
|
||||||
}
|
}
|
||||||
@@ -97,5 +102,6 @@ const mediaDropDownEntries: Array<DropDownEntry> = [
|
|||||||
#logo-img {
|
#logo-img {
|
||||||
width: 128px;
|
width: 128px;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enums for viewport types
|
* Enums for viewport types
|
||||||
|
* @readonly
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const enum ViewPortType {
|
export const enum ViewPortType {
|
||||||
DESKTOP,
|
DESKTOP,
|
||||||
TABLET,
|
TABLET,
|
||||||
MOBILE
|
MOBILE,
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vue Composable for getting window dimensions and viewport type based on width
|
* Vue Composable for getting window dimensions and viewport type based on width
|
||||||
@@ -27,11 +28,9 @@ export function useWindowDimensions() {
|
|||||||
height.value = window.innerHeight;
|
height.value = window.innerHeight;
|
||||||
if (width.value >= 1024) {
|
if (width.value >= 1024) {
|
||||||
viewPortType.value = ViewPortType.DESKTOP;
|
viewPortType.value = ViewPortType.DESKTOP;
|
||||||
}
|
} else if (width.value < 640) {
|
||||||
else if (width.value < 640) {
|
|
||||||
viewPortType.value = ViewPortType.MOBILE;
|
viewPortType.value = ViewPortType.MOBILE;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
viewPortType.value = ViewPortType.TABLET;
|
viewPortType.value = ViewPortType.TABLET;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
error.vue
Normal file
21
error.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NuxtError } from "#app";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
error: Object as () => NuxtError,
|
||||||
|
});
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: "default",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<div>
|
||||||
|
<h1>{{ error?.statusCode }}</h1>
|
||||||
|
<p>{{ error?.message }}</p>
|
||||||
|
<NuxtLink to="/">戻る</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<TheHeader />
|
<TheHeader />
|
||||||
Layout: default
|
|
||||||
<div class="website-content">
|
<div class="website-content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
@@ -9,7 +8,14 @@
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.website-content {
|
.website-content {
|
||||||
width: 90%;
|
display: grid;
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.website-content {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
place-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
4346
package-lock.json
generated
4346
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,16 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt generate",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"documentation": "typedoc utils/*.ts server/api/*.ts composables/*.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@unhead/schema": "^1.11.6",
|
||||||
|
"marked": "^14.1.2",
|
||||||
"nuxt": "^3.12.4",
|
"nuxt": "^3.12.4",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"vue": "latest"
|
"vue": "latest"
|
||||||
@@ -17,7 +20,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/icon": "^1.5.1",
|
"@nuxt/icon": "^1.5.1",
|
||||||
"@nuxt/image": "^1.7.0",
|
"@nuxt/image": "^1.7.0",
|
||||||
"@nuxtjs/eslint-module": "^4.1.0",
|
"@nuxtjs/eslint-module": "^3.1.0",
|
||||||
"@nuxtjs/sitemap": "^6.0.1",
|
"@nuxtjs/sitemap": "^6.0.1",
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.9.1",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
|||||||
167
pages/index.vue
167
pages/index.vue
@@ -1,10 +1,165 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { marked } from "marked";
|
||||||
|
import type { EntryType } from "#imports";
|
||||||
|
|
||||||
|
const { data } = await useFetch("/api/getNewsList");
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const twitterScript = document.createElement("script");
|
||||||
|
const twitterDivision = document.getElementById("twitter");
|
||||||
|
twitterScript.type = "text/javascript";
|
||||||
|
twitterScript.src = "https://platform.twitter.com/widgets.js";
|
||||||
|
twitterScript.async = true;
|
||||||
|
twitterDivision?.appendChild(twitterScript);
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta(
|
||||||
|
generateSeoMeta(
|
||||||
|
"ホーム",
|
||||||
|
"岐阜高専宇宙工学研究会のホームページ",
|
||||||
|
"/sera-logo-text.svg"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<main>
|
||||||
Page: index
|
<div id="news-board">
|
||||||
<NewsList />
|
<h3>News</h3>
|
||||||
</div>
|
<div></div>
|
||||||
|
<ul id="news-list">
|
||||||
|
<li v-for="entry in data" :key="entry.date as number">
|
||||||
|
<small>
|
||||||
|
{{
|
||||||
|
new Date(entry.date as number).toLocaleDateString(
|
||||||
|
"ja-JP",
|
||||||
|
{ dateStyle: "medium" }
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</small>
|
||||||
|
<div class="new-label" v-if="data?.indexOf(entry) < 2">
|
||||||
|
NEW!
|
||||||
|
</div>
|
||||||
|
<article
|
||||||
|
v-html="marked.parse(entry.cardContent as string)"
|
||||||
|
></article>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="entry.entryType === EntryType.Article"
|
||||||
|
:to="entry.linkPath as string"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:keyboard-double-arrow-right-rounded"
|
||||||
|
/>
|
||||||
|
Read More
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="twitter">
|
||||||
|
<a
|
||||||
|
class="twitter-timeline"
|
||||||
|
data-lang="ja"
|
||||||
|
data-dnt="true"
|
||||||
|
data-align="center"
|
||||||
|
data-theme="dark"
|
||||||
|
:data-height="16 * 70"
|
||||||
|
:data-width="16 * 33"
|
||||||
|
href="https://twitter.com/SERA_NITGC?ref_src=twsrc%5Etfw"
|
||||||
|
>Tweets by SERA_NITGC</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
grid: auto-flow / 4fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#news-board {
|
||||||
|
max-width: 60rem;
|
||||||
|
max-height: 50rem;
|
||||||
|
width: fit-content;
|
||||||
|
place-self: center;
|
||||||
|
overflow-y: scroll;
|
||||||
|
border: var(--neptune1) solid 3px;
|
||||||
|
box-shadow: 10px 5px 5px var(--starlight1);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto;
|
||||||
|
grid-template-rows: repeat(2, auto);
|
||||||
|
& > h3 {
|
||||||
|
place-self: center;
|
||||||
|
}
|
||||||
|
& > div {
|
||||||
|
width: 90%;
|
||||||
|
place-self: center;
|
||||||
|
border: var(--neptune1) solid 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#news-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
& li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: var(--starlight1) solid 2px;
|
||||||
|
}
|
||||||
|
& li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
& li > * {
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
& li > *:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
& li > *:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
& li div {
|
||||||
|
background-color: var(--sun2);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
& li a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--neptune1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
& li a span {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
margin-right: 0.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#twitter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
& > .twitter-timeline-rendered {
|
||||||
|
display: unset;
|
||||||
|
width: unset;
|
||||||
|
max-width: unset;
|
||||||
|
margin: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
main {
|
||||||
|
width: calc(100vw - 2rem);
|
||||||
|
place-self: center;
|
||||||
|
place-items: center;
|
||||||
|
grid: auto-flow / 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#news-board {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#twitter {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,80 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { marked } from "marked";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const { data } = await useFetch("/api/getArticle", {
|
||||||
|
query: { name: route.params.article },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.value === undefined || data.value === null) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: "Article Not Found :(",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const article = document.getElementById("article");
|
||||||
|
const articleTitle = article?.getElementsByTagName("h1")[0];
|
||||||
|
const postedDate = new Date(data.value?.date).toLocaleDateString(
|
||||||
|
"ja-JP-u-ca-japanese",
|
||||||
|
{ dateStyle: "medium" }
|
||||||
|
);
|
||||||
|
const postedDateElement = document.createElement("small");
|
||||||
|
postedDateElement.textContent = "掲載日: " + postedDate;
|
||||||
|
articleTitle?.insertAdjacentElement("afterend", postedDateElement);
|
||||||
|
const cardContentConversion = document.createElement("div");
|
||||||
|
cardContentConversion.innerHTML = marked.parse(
|
||||||
|
data.value?.cardContent as string
|
||||||
|
) as string;
|
||||||
|
useSeoMeta(
|
||||||
|
generateSeoMeta(
|
||||||
|
articleTitle?.innerText,
|
||||||
|
cardContentConversion.innerText,
|
||||||
|
data.value?.coverImagePath || "/sera-logo-text.svg",
|
||||||
|
"article"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>Page: news/[article]</div>
|
<main>
|
||||||
|
<img :src="(data?.coverImagePath as string) || '/sera-logo-text.svg'" />
|
||||||
|
<div
|
||||||
|
id="article"
|
||||||
|
class="markdown"
|
||||||
|
v-html="marked.parse(data?.article as string)"
|
||||||
|
></div>
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
width: 50%;
|
||||||
|
height: fit-content;
|
||||||
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > img {
|
||||||
|
width: 42rem;
|
||||||
|
place-self: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
main {
|
||||||
|
width: calc(100% - 6rem);
|
||||||
|
padding: 0 3rem;
|
||||||
|
}
|
||||||
|
main > img {
|
||||||
|
width: 70vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,46 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
const { data } = await useFetch("/api/getNewsList");
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>Page: news/index</div>
|
<PageTop
|
||||||
|
:text="'News'"
|
||||||
|
:image-path="'https://www.gifu-nct.ac.jp/gakuseikai/club/sera/img/subtop/sub_top_sample.jpg'"
|
||||||
|
/>
|
||||||
|
<main>
|
||||||
|
<div class="news-list">
|
||||||
|
<NewsCard
|
||||||
|
v-for="article in data"
|
||||||
|
:key="article.date as number"
|
||||||
|
:news-entry="article"
|
||||||
|
:is-new="data?.indexOf(article) < 2"
|
||||||
|
></NewsCard>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.news-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, 15rem);
|
||||||
|
gap: 20px;
|
||||||
|
justify-self: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-list > div:has(a) {
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
scale: 105%;
|
||||||
|
filter: grayscale(25%);
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
.news-list {
|
||||||
|
grid-auto-flow: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
viewBox="0 0 149.7076 64.060745"
|
viewBox="0 0 149.7076 64.060745"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
|
style="background: white"
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -7,6 +7,7 @@
|
|||||||
viewBox="0 0 197.77673 72.193382"
|
viewBox="0 0 197.77673 72.193382"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
|
style="background: white"
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
65
server/api/getArticle.ts
Normal file
65
server/api/getArticle.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Getting article from database.
|
||||||
|
* @module api/getArticle
|
||||||
|
*/
|
||||||
|
|
||||||
|
import sqlite3 from "sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { asyncDatabaseRead } from "~/utils/asyncDatabase";
|
||||||
|
import type { ArticleInfo } from "~/utils/news";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler of getArticle event. Entry is selected by linkPath
|
||||||
|
* @name getArticleEventHandler
|
||||||
|
* @param {H3Event<EventHandlerRequest>} event
|
||||||
|
* @returns {ArticleInfo} - object that contains information and content of article
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const getArticleEventHandler = defineEventHandler(async (event: any) => {
|
||||||
|
const database = new sqlite3.Database(
|
||||||
|
path.join(__dirname, "../../assets/databases/news.db")
|
||||||
|
);
|
||||||
|
|
||||||
|
let res: ArticleInfo = {
|
||||||
|
date: 0,
|
||||||
|
cardContent: "",
|
||||||
|
article: "",
|
||||||
|
linkPath: "",
|
||||||
|
coverImagePath: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const target = "/news/" + getQuery(event).name;
|
||||||
|
const sql = `SELECT date, cardContent, article, linkPath, coverImagePath FROM news WHERE linkPath = "${target}" AND entryType = 0;`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entry: ArticleInfo = await asyncDatabaseRead<ArticleInfo>(
|
||||||
|
database,
|
||||||
|
sql,
|
||||||
|
(rows) => {
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
res = {
|
||||||
|
date: entry.date,
|
||||||
|
cardContent: entry.cardContent,
|
||||||
|
article: entry.article,
|
||||||
|
linkPath: entry.linkPath,
|
||||||
|
coverImagePath: entry.coverImagePath,
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
database.close();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getArticleEventHandler;
|
||||||
50
server/api/getNewsList.ts
Normal file
50
server/api/getNewsList.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Getting list of articles from database.
|
||||||
|
* @module api/getArticleList
|
||||||
|
*/
|
||||||
|
|
||||||
|
import sqlite3 from "sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { asyncDatabaseRead } from "~/utils/asyncDatabase";
|
||||||
|
import type { NewsEntry } from "~/utils/news";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler of getArticleList event
|
||||||
|
* @name getArticleListEventHandler
|
||||||
|
* @param {H3Event<EventHandlerRequest>} event
|
||||||
|
* @returns {Array<NewsEntry>} list of news
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const getArticleListEventHandler = defineEventHandler(async (event: any) => {
|
||||||
|
const database = new sqlite3.Database(
|
||||||
|
path.join(__dirname, "../../assets/databases/news.db")
|
||||||
|
);
|
||||||
|
const sql = `SELECT date, entryType, cardContent, linkPath, coverImagePath FROM news ORDER BY date DESC;`;
|
||||||
|
|
||||||
|
let res: Array<NewsEntry> = new Array<NewsEntry>(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
res = await asyncDatabaseRead<Array<NewsEntry>>(
|
||||||
|
database,
|
||||||
|
sql,
|
||||||
|
(rows) => {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
database.close();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getArticleListEventHandler;
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Getting Unix time when the API was called.
|
* Getting Unix time when the API was called.
|
||||||
* @module api/getTime
|
* @module api/getTime
|
||||||
* @exports defineEventHandler(getTimeEventHandler) */
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler of getTime event
|
* Handler of getTime event
|
||||||
* @name getTimeEventHandler
|
* @name getTimeEventHandler
|
||||||
* @param {H3Event<EventHandlerRequest>} event
|
* @param {H3Event<EventHandlerRequest>} event
|
||||||
* @returns {number} - Unix time
|
* @returns {number} Unix time
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const getTimeEventHandler = (event: any) => {
|
const getTimeEventHandler = defineEventHandler((event: any) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return now.valueOf();
|
return now.valueOf();
|
||||||
};
|
});
|
||||||
|
|
||||||
export default defineEventHandler(getTimeEventHandler);
|
export default getTimeEventHandler;
|
||||||
|
|||||||
65
utils/asyncDatabase.ts
Normal file
65
utils/asyncDatabase.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Wrapper functions for asynchronous database operations
|
||||||
|
* @module utils/asyncDatabase
|
||||||
|
*/
|
||||||
|
|
||||||
|
import sqlite3 from "sqlite3";
|
||||||
|
|
||||||
|
/** Callback function type for {@link asyncDatabaseRead} */
|
||||||
|
type asyncDatabaseRowsCallbackFunction = (rows: any[]) => any;
|
||||||
|
/** Callback function type for {@link asyncDatabaseWrite} */
|
||||||
|
type asyncDatabaseVoidCallbackFunction = () => any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper functions that perform read operations to database asynchronously
|
||||||
|
* @template Type
|
||||||
|
* @param {sqlite3.Database} database sqlite3 database object
|
||||||
|
* @param {string} sqlQuery SQL query to execute
|
||||||
|
* @param {asyncDatabaseRowsCallbackFunction} callback callback to perform further operations on each row
|
||||||
|
* @returns {Promise<Type>} Promise for database operation
|
||||||
|
*/
|
||||||
|
const asyncDatabaseRead = <Type>(
|
||||||
|
database: sqlite3.Database,
|
||||||
|
sqlQuery: string,
|
||||||
|
callback: asyncDatabaseRowsCallbackFunction
|
||||||
|
): Promise<Type> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
database.all(sqlQuery, (err: any, rows) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(callback(rows));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper functions that perform write operations to database asynchronously
|
||||||
|
* @template Type
|
||||||
|
* @param {sqlite3.Database} database sqlite3 database object
|
||||||
|
* @param {string} sqlQuery SQL query to execute
|
||||||
|
* @param {asyncDatabaseVoidCallbackFunction} callback callback to perform after the operation
|
||||||
|
* @returns {Promise<Type>} Promise for database operation
|
||||||
|
*/
|
||||||
|
const asyncDatabaseWrite = <Type>(
|
||||||
|
database: sqlite3.Database,
|
||||||
|
sqlQuery: string,
|
||||||
|
callback: asyncDatabaseVoidCallbackFunction
|
||||||
|
): Promise<Type> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
database.run(sqlQuery, (err: any) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(callback());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { asyncDatabaseRead, asyncDatabaseWrite };
|
||||||
|
export type {
|
||||||
|
asyncDatabaseRowsCallbackFunction,
|
||||||
|
asyncDatabaseVoidCallbackFunction,
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum for interaction mode of DropDown component
|
* Enum for interaction mode of DropDown component
|
||||||
|
* @readonly
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
export const enum DropDownMode {
|
export const enum DropDownMode {
|
||||||
@@ -14,6 +15,7 @@ export const enum DropDownMode {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum for alignment of DropDown component
|
* Enum for alignment of DropDown component
|
||||||
|
* @readonly
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
export const enum DropDownAlignment {
|
export const enum DropDownAlignment {
|
||||||
@@ -24,8 +26,8 @@ export const enum DropDownAlignment {
|
|||||||
/**
|
/**
|
||||||
* Interface for the entry of DropDown menu
|
* Interface for the entry of DropDown menu
|
||||||
* @typedef {object} DropDownEntry
|
* @typedef {object} DropDownEntry
|
||||||
* @property {string} text - Text to be displayed on the menu
|
* @property {string} text Text to be displayed on the menu
|
||||||
* @property {string} link - Hyperlink to the page
|
* @property {string} link Hyperlink to the page
|
||||||
*/
|
*/
|
||||||
interface DropDownEntry {
|
interface DropDownEntry {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -35,11 +37,11 @@ interface DropDownEntry {
|
|||||||
/**
|
/**
|
||||||
* Interface for the property of DropDown component
|
* Interface for the property of DropDown component
|
||||||
* @typedef {object} DropDownProperty
|
* @typedef {object} DropDownProperty
|
||||||
* @property {string} label - Label of the component
|
* @property {string} label Label of the component
|
||||||
* @property {(DropDownMode | string)} mode - Interaction mode of the component
|
* @property {(DropDownMode | string)} mode Interaction mode of the component
|
||||||
* @property {Array<DropDownEntry>} entries - Entries of DropDown menu
|
* @property {Array<DropDownEntry>} entries Entries of DropDown menu
|
||||||
* @property {boolean} showInMobile - Whether to show the component in mobile(<640px) environemnt
|
* @property {boolean} showInMobile Whether to show the component in mobile(<640px) environemnt
|
||||||
* @property {(DropDownAlignment | number)=} alignment - Explicitly assign the alignment of the component
|
* @property {(DropDownAlignment | number)=} alignment Explicitly assign the alignment of the component
|
||||||
*/
|
*/
|
||||||
interface DropDownProperty {
|
interface DropDownProperty {
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
48
utils/generateSeoMeta.ts
Normal file
48
utils/generateSeoMeta.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Helper function to generate object for useSeoMeta composable
|
||||||
|
* @module utils/generateSeoMeta
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { UseSeoMetaInput } from "@unhead/schema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate object for useSeoMeta composable
|
||||||
|
* @typedef {"website" | "article"} ContentType
|
||||||
|
* @param {string} title Title of the page
|
||||||
|
* @param {string} description description of the page
|
||||||
|
* @param {string} imagePath path to image for SNS card, root is at public/
|
||||||
|
* @param {ContentType} [type] Type of website, either website or article, defaults to website if not passed
|
||||||
|
* @returns {UseSeoMetaInput} object that can be passed to useSeoMeta composable
|
||||||
|
* @example
|
||||||
|
* useSeoMeta(
|
||||||
|
* generateSeoMeta(
|
||||||
|
* "Home",
|
||||||
|
* "Home page for my website",
|
||||||
|
* "/default_card_image.png"
|
||||||
|
* )
|
||||||
|
* );
|
||||||
|
* @example
|
||||||
|
* useSeoMeta(generateSeoMeta(data.articleName, data.articleDescription, data.articleCoverImage, "article"));
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export function generateSeoMeta(
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
imagePath: string,
|
||||||
|
type?: "website" | "article"
|
||||||
|
): UseSeoMetaInput {
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
ogTitle: title + " - 岐阜高専宇宙工学研究会",
|
||||||
|
twitterTitle: title + " - 岐阜高専宇宙工学研究会",
|
||||||
|
description: description,
|
||||||
|
ogDescription: description,
|
||||||
|
twitterDescription: description,
|
||||||
|
ogImage: imagePath,
|
||||||
|
twitterImage: imagePath,
|
||||||
|
twitterCard: "summary",
|
||||||
|
charset: "utf-8",
|
||||||
|
ogLocale: "ja_JP",
|
||||||
|
ogType: type || "website",
|
||||||
|
};
|
||||||
|
}
|
||||||
49
utils/news.ts
Normal file
49
utils/news.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Types for news data
|
||||||
|
* @module utils/news
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumeration for news entry type of either article or tweet style
|
||||||
|
* @readonly
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const enum EntryType {
|
||||||
|
Article,
|
||||||
|
Tweet,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for article information
|
||||||
|
* @typedef {object} ArticleInfo
|
||||||
|
* @property {number | null} date Unix time of article creation
|
||||||
|
* @property {string | null} article content of article itself
|
||||||
|
* @property {string | null} linkPath path to the article
|
||||||
|
* @property {string | null} coverImagePath Path to the cover image
|
||||||
|
*/
|
||||||
|
interface ArticleInfo {
|
||||||
|
date: number | null;
|
||||||
|
cardContent: string | null;
|
||||||
|
article: string | null;
|
||||||
|
linkPath: string | null;
|
||||||
|
coverImagePath: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for news
|
||||||
|
* @typedef {object} NewsEntry
|
||||||
|
* @property {number | null} date Unix time of creation
|
||||||
|
* @property {EntryType | null} entryType Type of news
|
||||||
|
* @property {string | null} cardContent Content displayed on card
|
||||||
|
* @property {string | null} linkPath Link path to the article
|
||||||
|
* @property {string | null} coverImagePath Path to the cover image
|
||||||
|
*/
|
||||||
|
interface NewsEntry {
|
||||||
|
date: number | null;
|
||||||
|
entryType: EntryType | null;
|
||||||
|
cardContent: string | null;
|
||||||
|
linkPath: string | null;
|
||||||
|
coverImagePath: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ArticleInfo, NewsEntry };
|
||||||
18
utils/newsCard.ts
Normal file
18
utils/newsCard.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Type for NewsCard component
|
||||||
|
* @module utils/newsCard
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NewsEntry } from "./news";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that defines property for NewsCard component
|
||||||
|
* @property {NewsEntry} newsEntry Data of news
|
||||||
|
* @property {boolean} isNew Mark the entry new
|
||||||
|
*/
|
||||||
|
interface NewsCardProperty {
|
||||||
|
newsEntry: NewsEntry;
|
||||||
|
isNew: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { NewsCardProperty };
|
||||||
16
utils/pageTop.ts
Normal file
16
utils/pageTop.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Types for PageTop component
|
||||||
|
* @module utils/pageTop
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that defines property for PageTop component
|
||||||
|
* @property {string} text Text to show in top
|
||||||
|
* @property {string} imagePath Path to image used in background
|
||||||
|
*/
|
||||||
|
interface PageTopProperty {
|
||||||
|
text: string;
|
||||||
|
imagePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { PageTopProperty };
|
||||||
Reference in New Issue
Block a user