a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
77 | className
78 | )}
79 | {...props}
80 | />
81 | )
82 | }
83 |
84 | function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
85 | return (
86 |
94 | )
95 | }
96 |
97 | export {
98 | Empty,
99 | EmptyHeader,
100 | EmptyTitle,
101 | EmptyDescription,
102 | EmptyContent,
103 | EmptyMedia,
104 | }
105 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { ChevronRight, MoreHorizontal } from "lucide-react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8 | return
;
9 | }
10 |
11 | function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12 | return (
13 |
21 | );
22 | }
23 |
24 | function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25 | return (
26 |
31 | );
32 | }
33 |
34 | function BreadcrumbLink({
35 | asChild,
36 | className,
37 | ...props
38 | }: React.ComponentProps<"a"> & {
39 | asChild?: boolean;
40 | }) {
41 | const Comp = asChild ? Slot : "a";
42 |
43 | return (
44 |
49 | );
50 | }
51 |
52 | function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53 | return (
54 |
62 | );
63 | }
64 |
65 | function BreadcrumbSeparator({
66 | children,
67 | className,
68 | ...props
69 | }: React.ComponentProps<"li">) {
70 | return (
71 |
svg]:size-3.5", className)}
76 | {...props}
77 | >
78 | {children ?? }
79 |
80 | );
81 | }
82 |
83 | function BreadcrumbEllipsis({
84 | className,
85 | ...props
86 | }: React.ComponentProps<"span">) {
87 | return (
88 |
95 |
96 | More
97 |
98 | );
99 | }
100 |
101 | export {
102 | Breadcrumb,
103 | BreadcrumbList,
104 | BreadcrumbItem,
105 | BreadcrumbLink,
106 | BreadcrumbPage,
107 | BreadcrumbSeparator,
108 | BreadcrumbEllipsis,
109 | };
110 |
--------------------------------------------------------------------------------
/src/app/(main)/poems/detail/[slug]/_components/sidebar-right/author-card.tsx:
--------------------------------------------------------------------------------
1 | import { CalendarIcon, UserIcon } from "lucide-react";
2 | import Link from "next/link";
3 | import { Avatar, AvatarFallback } from "@/components/ui/avatar";
4 | import { Badge } from "@/components/ui/badge";
5 | import { Button } from "@/components/ui/button";
6 | import {
7 | Card,
8 | CardContent,
9 | CardFooter,
10 | CardHeader,
11 | } from "@/components/ui/card";
12 | import { Separator } from "@/components/ui/separator";
13 | import type { ApiPoemFindDetail } from "@/server/api/router/poem";
14 |
15 | export function AuthorCard({ poem }: { poem: ApiPoemFindDetail }) {
16 | const { author, dynasty } = poem;
17 |
18 | const lifespan = `${author.birthDate || "?"}年—${author.deathDate || "?"}年`;
19 |
20 | return (
21 |
22 |
23 |
24 | {/* 作者头像 */}
25 |
26 |
27 | {author.name.slice(0, 2)}
28 |
29 |
30 |
31 |
32 | {/* 作者姓名 */}
33 |
34 |
{author.name}
35 |
{author.pinyin}
36 |
37 |
38 | {/* 生卒年/朝代 */}
39 |
40 |
41 | {lifespan}
42 |
43 | {dynasty!.name}
44 |
45 |
46 |
47 |
48 |
49 |
50 | {/* 文学地位/称号 */}
51 | {author.epithets && (
52 |
53 | {author.epithets.map((item) => (
54 |
55 | {item}
56 |
57 | ))}
58 |
59 | )}
60 |
61 | {/* 核心风格/创作特色 */}
62 |
63 |
创作特色
64 |
{author.style || "待完善"}
65 |
66 |
67 | {/* 作者简介 */}
68 |
69 |
简介
70 |
{author.introduce || "待完善"}
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | 进入主页
79 |
80 |
81 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/src/server/api/router/poem/index.ts:
--------------------------------------------------------------------------------
1 | import type { TRPCRouterRecord } from "@trpc/server";
2 | import z from "zod";
3 | import { publicProcedure } from "../../trpc";
4 |
5 | export * from "./discover";
6 |
7 | export type ApiPoemFindDetail = Awaited<
8 | ReturnType
9 | >;
10 |
11 | export const poemRouter = {
12 | findDetail: publicProcedure
13 | .input(
14 | z
15 | .object({
16 | id: z.string().optional(),
17 | slug: z.string().optional(),
18 | })
19 | .refine((data) => (data.id && !data.slug) || (!data.id && data.slug), {
20 | message: "Provide either id or slug, not both",
21 | path: ["id", "slug"],
22 | }),
23 | )
24 | .query(async ({ ctx, input }) => {
25 | const { id, slug } = input;
26 | const poem = await ctx.db.poem.findUnique({
27 | where: id ? { id } : { slug: slug! },
28 | select: {
29 | id: true,
30 | slug: true,
31 | title: true,
32 | titleSlug: true,
33 | titlePinyin: true,
34 | paragraphs: true,
35 | paragraphsPinyin: true,
36 | visits: true,
37 | createdAt: true,
38 | annotation: true,
39 | dynasty: true,
40 | author: true,
41 | appreciation: true,
42 | translation: true,
43 | isOrderliness: true,
44 | updatedAt: true,
45 | tags: {
46 | select: {
47 | name: true,
48 | slug: true,
49 | },
50 | },
51 | },
52 | });
53 |
54 | if (!poem) {
55 | throw new Error("Poem not found");
56 | }
57 |
58 | ctx.db.poem
59 | .update({
60 | where: { id: poem.id },
61 | data: {
62 | visits: {
63 | increment: 1,
64 | },
65 | },
66 | })
67 | .then(() => {});
68 |
69 | return poem;
70 | }),
71 |
72 | search: publicProcedure
73 | .input(
74 | z.object({
75 | keyword: z.string(),
76 | }),
77 | )
78 | .query(async ({ ctx, input }) => {
79 | const { keyword } = input;
80 |
81 | const poems = await ctx.db.poem.findMany({
82 | where: {
83 | searchText: { contains: keyword },
84 | },
85 | take: 20,
86 | select: {
87 | id: true,
88 | slug: true,
89 | title: true,
90 | author: {
91 | select: {
92 | name: true,
93 | slug: true,
94 | dynasty: {
95 | select: {
96 | name: true,
97 | slug: true,
98 | },
99 | },
100 | },
101 | },
102 | },
103 | });
104 |
105 | return poems;
106 | }),
107 | } satisfies TRPCRouterRecord;
108 |
--------------------------------------------------------------------------------
/src/app/(main)/authors/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 | import { api } from "@/trpc/server";
3 | import { AuthorTable } from "./_components/author-table";
4 |
5 | interface PageProps {
6 | searchParams: Promise<{
7 | page?: string;
8 | pageSize?: string;
9 | dynasty?: string;
10 | }>;
11 | }
12 |
13 | export default async function Page({ searchParams }: PageProps) {
14 | const params = await searchParams;
15 | const page = Number(params.page) || 1;
16 | const pageSize = Number(params.pageSize) || 100;
17 | const dynastySlug = params.dynasty;
18 |
19 | // 验证页码,如果小于1则重定向到第1页
20 | if (page < 1) {
21 | redirect("/authors?page=1");
22 | }
23 |
24 | const result = await api.author.getPagedList({
25 | page,
26 | pageSize,
27 | dynastySlug,
28 | });
29 |
30 | // 如果页码超出范围,重定向到最后一页
31 | if (page > result.totalPages && result.totalPages > 0) {
32 | const searchParams = new URLSearchParams();
33 | searchParams.set("page", result.totalPages.toString());
34 | if (pageSize !== 20) searchParams.set("pageSize", pageSize.toString());
35 | if (dynastySlug) searchParams.set("dynasty", dynastySlug);
36 | redirect(`/authors?${searchParams.toString()}`);
37 | }
38 |
39 | return (
40 | <>
41 |
42 |
43 | {dynastySlug
44 | ? `${result.items[0]?.dynasty.name}朝代诗人`
45 | : "诗人作者"}
46 |
47 |
48 | {dynastySlug
49 | ? `${result.items[0]?.dynasty.name}朝代共有 ${result.total} 位诗人作者,探索他们的文学成就`
50 | : `共收录 ${result.total} 位历代文人墨客,品味千年诗词文化`}
51 |
52 |
53 |
54 |
63 | >
64 | );
65 | }
66 |
67 | // 生成页面元数据
68 | export async function generateMetadata({ searchParams }: PageProps) {
69 | const params = await searchParams;
70 | const dynastySlug = params.dynasty;
71 | const page = Number(params.page) || 1;
72 |
73 | if (dynastySlug) {
74 | try {
75 | const result = await api.author.getPagedList({
76 | page: 1,
77 | pageSize: 1,
78 | dynastySlug,
79 | });
80 | const dynastyName = result.items[0]?.dynasty.name;
81 |
82 | return {
83 | title: `${dynastyName}朝代诗人列表${page > 1 ? ` - 第${page}页` : ""}`,
84 | description: `探索${dynastyName}朝代的诗人作者,品味千年诗词文化`,
85 | };
86 | } catch {
87 | return {
88 | title: "朝代不存在",
89 | };
90 | }
91 | }
92 |
93 | return {
94 | title: `诗人作者${page > 1 ? ` - 第${page}页` : ""}`,
95 | description: "探索历代文人墨客,品味千年诗词文化",
96 | };
97 | }
98 |
--------------------------------------------------------------------------------
/src/config/data-table.ts:
--------------------------------------------------------------------------------
1 | export type DataTableConfig = typeof dataTableConfig;
2 |
3 | export const dataTableConfig = {
4 | textOperators: [
5 | { label: "Contains", value: "iLike" as const },
6 | { label: "Does not contain", value: "notILike" as const },
7 | { label: "Is", value: "eq" as const },
8 | { label: "Is not", value: "ne" as const },
9 | { label: "Is empty", value: "isEmpty" as const },
10 | { label: "Is not empty", value: "isNotEmpty" as const },
11 | ],
12 | numericOperators: [
13 | { label: "Is", value: "eq" as const },
14 | { label: "Is not", value: "ne" as const },
15 | { label: "Is less than", value: "lt" as const },
16 | { label: "Is less than or equal to", value: "lte" as const },
17 | { label: "Is greater than", value: "gt" as const },
18 | { label: "Is greater than or equal to", value: "gte" as const },
19 | { label: "Is between", value: "isBetween" as const },
20 | { label: "Is empty", value: "isEmpty" as const },
21 | { label: "Is not empty", value: "isNotEmpty" as const },
22 | ],
23 | dateOperators: [
24 | { label: "Is", value: "eq" as const },
25 | { label: "Is not", value: "ne" as const },
26 | { label: "Is before", value: "lt" as const },
27 | { label: "Is after", value: "gt" as const },
28 | { label: "Is on or before", value: "lte" as const },
29 | { label: "Is on or after", value: "gte" as const },
30 | { label: "Is between", value: "isBetween" as const },
31 | { label: "Is relative to today", value: "isRelativeToToday" as const },
32 | { label: "Is empty", value: "isEmpty" as const },
33 | { label: "Is not empty", value: "isNotEmpty" as const },
34 | ],
35 | selectOperators: [
36 | { label: "Is", value: "eq" as const },
37 | { label: "Is not", value: "ne" as const },
38 | { label: "Is empty", value: "isEmpty" as const },
39 | { label: "Is not empty", value: "isNotEmpty" as const },
40 | ],
41 | multiSelectOperators: [
42 | { label: "Has any of", value: "inArray" as const },
43 | { label: "Has none of", value: "notInArray" as const },
44 | { label: "Is empty", value: "isEmpty" as const },
45 | { label: "Is not empty", value: "isNotEmpty" as const },
46 | ],
47 | booleanOperators: [
48 | { label: "Is", value: "eq" as const },
49 | { label: "Is not", value: "ne" as const },
50 | ],
51 | sortOrders: [
52 | { label: "Asc", value: "asc" as const },
53 | { label: "Desc", value: "desc" as const },
54 | ],
55 | filterVariants: [
56 | "text",
57 | "number",
58 | "range",
59 | "date",
60 | "dateRange",
61 | "boolean",
62 | "select",
63 | "multiSelect",
64 | ] as const,
65 | operators: [
66 | "iLike",
67 | "notILike",
68 | "eq",
69 | "ne",
70 | "inArray",
71 | "notInArray",
72 | "isEmpty",
73 | "isNotEmpty",
74 | "lt",
75 | "lte",
76 | "gt",
77 | "gte",
78 | "isBetween",
79 | "isRelativeToToday",
80 | ] as const,
81 | joinOperators: ["and", "or"] as const,
82 | };
83 |
--------------------------------------------------------------------------------
/src/app/(main)/authors/detail/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { BookOpenIcon, CalendarIcon } from "lucide-react";
2 | import Link from "next/link";
3 | import { notFound } from "next/navigation";
4 | import { Badge } from "@/components/ui/badge";
5 | import { api } from "@/trpc/server";
6 |
7 | interface PageProps {
8 | params: Promise<{ slug: string }>;
9 | }
10 |
11 | export default async function Page({ params }: PageProps) {
12 | const { slug } = await params;
13 | const author = await api.author.findBySlug({ slug });
14 |
15 | if (!author) {
16 | notFound();
17 | }
18 |
19 | const lifespan =
20 | author.birthDate && author.deathDate
21 | ? `${author.birthDate}年 - ${author.deathDate}年`
22 | : author.birthDate
23 | ? `${author.birthDate}年 -`
24 | : author.deathDate
25 | ? `- ${author.deathDate}年`
26 | : "生卒年不详";
27 |
28 | return (
29 |
30 |
31 |
32 | {author.name}
33 |
34 |
35 |
36 |
{author.dynasty.name}
37 |
38 |
39 |
40 | {lifespan}
41 |
42 |
43 |
44 |
45 | 共有 {author._count.poems} 首作品
46 |
47 |
48 |
49 |
50 |
51 | {author.introduce || "暂无简介"}
52 |
53 |
54 | {/* 作品列表 - 可以后续添加 */}
55 |
56 | 作品列表
57 |
58 |
59 | {author.poems.map((poem) => (
60 |
65 | {poem.title}
66 |
67 | ))}
68 |
69 |
70 | );
71 | }
72 |
73 | // 生成页面元数据
74 | export async function generateMetadata({ params }: PageProps) {
75 | const { slug } = await params;
76 | const author = await api.author.findBySlug({ slug });
77 |
78 | if (!author) {
79 | notFound();
80 | }
81 |
82 | return {
83 | title: `${author.name} - 诗人详情`,
84 | description: author.introduce
85 | ? `${author.introduce.slice(0, 100)}...`
86 | : `${author.name},${author.dynasty.name}诗人,共有${author._count.poems}首作品`,
87 | };
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function Table({ className, ...props }: React.ComponentProps<"table">) {
8 | return (
9 |
19 | )
20 | }
21 |
22 | function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
23 | return (
24 |
29 | )
30 | }
31 |
32 | function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
33 | return (
34 |
39 | )
40 | }
41 |
42 | function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
43 | return (
44 | tr]:last:border-b-0",
48 | className
49 | )}
50 | {...props}
51 | />
52 | )
53 | }
54 |
55 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56 | return (
57 |
65 | )
66 | }
67 |
68 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69 | return (
70 | [role=checkbox]]:translate-y-[2px]",
74 | className
75 | )}
76 | {...props}
77 | />
78 | )
79 | }
80 |
81 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82 | return (
83 | [role=checkbox]]:translate-y-[2px]",
87 | className
88 | )}
89 | {...props}
90 | />
91 | )
92 | }
93 |
94 | function TableCaption({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"caption">) {
98 | return (
99 |
104 | )
105 | }
106 |
107 | export {
108 | Table,
109 | TableHeader,
110 | TableBody,
111 | TableFooter,
112 | TableHead,
113 | TableRow,
114 | TableCell,
115 | TableCaption,
116 | }
117 |
--------------------------------------------------------------------------------
/scripts/update-db-make-search-index.ts:
--------------------------------------------------------------------------------
1 | // tsx scripts/update-db-make-search-index.ts
2 |
3 | import { convert } from "pinyin-pro";
4 | import { db } from "@/server/db";
5 |
6 | async function main() {
7 | console.log("开始更新搜索索引...");
8 |
9 | // 1. 查询 searchText 不存在的数据
10 | const poems = await db.poem.findMany({
11 | // where: {
12 | // OR: [{ searchText: null }, { searchText: "" }],
13 | // },
14 | skip: 100000,
15 | select: {
16 | id: true,
17 | title: true,
18 | titlePinyin: true,
19 | // paragraphs: true,
20 | // paragraphsPinyin: true,
21 | author: {
22 | select: {
23 | name: true,
24 | pinyin: true,
25 | dynasty: {
26 | select: {
27 | name: true,
28 | pinyin: true,
29 | },
30 | },
31 | },
32 | },
33 | },
34 | });
35 |
36 | console.log(`找到 ${poems.length} 条需要更新的数据`);
37 |
38 | // 分批处理,避免一次性更新太多
39 | const BATCH_SIZE = 100;
40 | const totalBatches = Math.ceil(poems.length / BATCH_SIZE);
41 |
42 | for (let i = 0; i < totalBatches; i++) {
43 | const start = i * BATCH_SIZE;
44 | const end = Math.min(start + BATCH_SIZE, poems.length);
45 | const batch = poems.slice(start, end);
46 |
47 | // 批量更新
48 | await Promise.all(
49 | batch.map(async (poem) => {
50 | // 处理标题拼音
51 | const titlePinyinWithoutTone = convert(poem.titlePinyin, {
52 | format: "toneNone",
53 | });
54 |
55 | // 处理正文
56 | // const paragraphsText = poem.paragraphs.join("");
57 | // const paragraphsPinyinWithoutTone = convert(poem.paragraphsPinyin, {
58 | // format: "toneNone",
59 | // });
60 |
61 | // 处理作者拼音
62 | const authorPinyinWithoutTone = convert(poem.author.pinyin, {
63 | format: "toneNone",
64 | });
65 |
66 | // 处理朝代拼音
67 | const dynastyPinyinWithoutTone = convert(poem.author.dynasty?.pinyin, {
68 | format: "toneNone",
69 | });
70 |
71 | // 组合成搜索文本
72 | const searchText = [
73 | poem.author.name, // 作者名
74 | authorPinyinWithoutTone, // 作者拼音(无声调)
75 | poem.author.dynasty?.name || "", // 朝代名
76 | dynastyPinyinWithoutTone, // 朝代拼音(无声调)
77 | poem.title, // 标题
78 | titlePinyinWithoutTone, // 标题拼音(无声调)
79 | // paragraphsText, // 正文
80 | // paragraphsPinyinWithoutTone, // 正文拼音(无声调)
81 | ]
82 | .filter(Boolean) // 过滤空值
83 | .join(" ");
84 |
85 | // 3. 更新数据库
86 | await db.poem.update({
87 | where: { id: poem.id },
88 | data: { searchText: searchText.replace(/\s+/g, " ").trim() },
89 | });
90 | }),
91 | );
92 |
93 | console.log(
94 | `已处理 ${end}/${poems.length} (${((end / poems.length) * 100).toFixed(1)}%)`,
95 | );
96 | }
97 |
98 | console.log("搜索索引更新完成!");
99 | }
100 |
101 | main()
102 | .catch((e) => {
103 | console.error("更新失败:", e);
104 | process.exit(1);
105 | })
106 | .finally(async () => {
107 | await db.$disconnect();
108 | });
109 |
--------------------------------------------------------------------------------
/start-database.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Use this script to start a docker container for a local development database
3 |
4 | # TO RUN ON WINDOWS:
5 | # 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install
6 | # 2. Install Docker Desktop or Podman Deskop
7 | # - Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/
8 | # - Podman Desktop - https://podman.io/getting-started/installation
9 | # 3. Open WSL - `wsl`
10 | # 4. Run this script - `./start-database.sh`
11 |
12 | # On Linux and macOS you can run this script directly - `./start-database.sh`
13 |
14 | # import env variables from .env
15 | set -a
16 | source .env
17 |
18 | DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}')
19 | DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}')
20 | DB_NAME=$(echo "$DATABASE_URL" | awk -F'/' '{print $4}')
21 | DB_CONTAINER_NAME="$DB_NAME-postgres"
22 |
23 | if ! [ -x "$(command -v docker)" ] && ! [ -x "$(command -v podman)" ]; then
24 | echo -e "Docker or Podman is not installed. Please install docker or podman and try again.\nDocker install guide: https://docs.docker.com/engine/install/\nPodman install guide: https://podman.io/getting-started/installation"
25 | exit 1
26 | fi
27 |
28 | # determine which docker command to use
29 | if [ -x "$(command -v docker)" ]; then
30 | DOCKER_CMD="docker"
31 | elif [ -x "$(command -v podman)" ]; then
32 | DOCKER_CMD="podman"
33 | fi
34 |
35 | if ! $DOCKER_CMD info > /dev/null 2>&1; then
36 | echo "$DOCKER_CMD daemon is not running. Please start $DOCKER_CMD and try again."
37 | exit 1
38 | fi
39 |
40 | if command -v nc >/dev/null 2>&1; then
41 | if nc -z localhost "$DB_PORT" 2>/dev/null; then
42 | echo "Port $DB_PORT is already in use."
43 | exit 1
44 | fi
45 | else
46 | echo "Warning: Unable to check if port $DB_PORT is already in use (netcat not installed)"
47 | read -p "Do you want to continue anyway? [y/N]: " -r REPLY
48 | if ! [[ $REPLY =~ ^[Yy]$ ]]; then
49 | echo "Aborting."
50 | exit 1
51 | fi
52 | fi
53 |
54 | if [ "$($DOCKER_CMD ps -q -f name=$DB_CONTAINER_NAME)" ]; then
55 | echo "Database container '$DB_CONTAINER_NAME' already running"
56 | exit 0
57 | fi
58 |
59 | if [ "$($DOCKER_CMD ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then
60 | $DOCKER_CMD start "$DB_CONTAINER_NAME"
61 | echo "Existing database container '$DB_CONTAINER_NAME' started"
62 | exit 0
63 | fi
64 |
65 | if [ "$DB_PASSWORD" = "password" ]; then
66 | echo "You are using the default database password"
67 | read -p "Should we generate a random password for you? [y/N]: " -r REPLY
68 | if ! [[ $REPLY =~ ^[Yy]$ ]]; then
69 | echo "Please change the default password in the .env file and try again"
70 | exit 1
71 | fi
72 | # Generate a random URL-safe password
73 | DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_')
74 | sed -i '' "s#:password@#:$DB_PASSWORD@#" .env
75 | fi
76 |
77 | $DOCKER_CMD run -d \
78 | --name $DB_CONTAINER_NAME \
79 | -e POSTGRES_USER="postgres" \
80 | -e POSTGRES_PASSWORD="$DB_PASSWORD" \
81 | -e POSTGRES_DB="$DB_NAME" \
82 | -p "$DB_PORT":5432 \
83 | docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created"
84 |
--------------------------------------------------------------------------------