init
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Refuse to install package versions published less than 7 days (10080 minutes)
|
||||
# ago, reducing exposure to freshly compromised or malicious releases.
|
||||
# Honoured by pnpm and bun; npm currently has no native support and ignores it,
|
||||
# so this is harmless today and forward-compatible.
|
||||
minimum-release-age=10080
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
@@ -0,0 +1,22 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function Donate(name: string, amount: number, message: string): $CancellablePromise<$models.CampaignStats> {
|
||||
return $Call.ByID(3212591404, name, amount, message);
|
||||
}
|
||||
|
||||
export function GetRecentDonations(): $CancellablePromise<$models.Donation[] | null> {
|
||||
return $Call.ByID(2710704353);
|
||||
}
|
||||
|
||||
export function GetStats(): $CancellablePromise<$models.CampaignStats> {
|
||||
return $Call.ByID(1236111792);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
export function Greet(name: string): $CancellablePromise<string> {
|
||||
return $Call.ByID(1411160069, name);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as CampaignService from "./campaignservice.js";
|
||||
import * as GreetService from "./greetservice.js";
|
||||
export {
|
||||
CampaignService,
|
||||
GreetService
|
||||
};
|
||||
|
||||
export type {
|
||||
CampaignStats,
|
||||
Donation
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,33 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as time$0 from "../time/models.js";
|
||||
|
||||
export interface CampaignStats {
|
||||
"totalDonors": number;
|
||||
"totalAmount": number;
|
||||
|
||||
/**
|
||||
* 마리
|
||||
*/
|
||||
"releasedFish": number;
|
||||
|
||||
/**
|
||||
* kg
|
||||
*/
|
||||
"collectedWaste": number;
|
||||
|
||||
/**
|
||||
* m²
|
||||
*/
|
||||
"purifiedArea": number;
|
||||
}
|
||||
|
||||
export interface Donation {
|
||||
"name": string;
|
||||
"amount": number;
|
||||
"message": string;
|
||||
"timestamp": time$0.Time;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//@ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
Object.freeze($Create.Events);
|
||||
@@ -0,0 +1,14 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import type { Events } from "@wailsio/runtime";
|
||||
|
||||
declare module "@wailsio/runtime" {
|
||||
namespace Events {
|
||||
interface CustomEvents {
|
||||
"time": string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export type {
|
||||
Time
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,47 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* A Time represents an instant in time with nanosecond precision.
|
||||
*
|
||||
* Programs using times should typically store and pass them as values,
|
||||
* not pointers. That is, time variables and struct fields should be of
|
||||
* type [time.Time], not *time.Time.
|
||||
*
|
||||
* A Time value can be used by multiple goroutines simultaneously except
|
||||
* that the methods [Time.GobDecode], [Time.UnmarshalBinary], [Time.UnmarshalJSON] and
|
||||
* [Time.UnmarshalText] are not concurrency-safe.
|
||||
*
|
||||
* Time instants can be compared using the [Time.Before], [Time.After], and [Time.Equal] methods.
|
||||
* The [Time.Sub] method subtracts two instants, producing a [Duration].
|
||||
* The [Time.Add] method adds a Time and a Duration, producing a Time.
|
||||
*
|
||||
* The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC.
|
||||
* As this time is unlikely to come up in practice, the [Time.IsZero] method gives
|
||||
* a simple way of detecting a time that has not been initialized explicitly.
|
||||
*
|
||||
* Each time has an associated [Location]. The methods [Time.Local], [Time.UTC], and Time.In return a
|
||||
* Time with a specific Location. Changing the Location of a Time value with
|
||||
* these methods does not change the actual instant it represents, only the time
|
||||
* zone in which to interpret it.
|
||||
*
|
||||
* Representations of a Time value saved by the [Time.GobEncode], [Time.MarshalBinary], [Time.AppendBinary],
|
||||
* [Time.MarshalJSON], [Time.MarshalText] and [Time.AppendText] methods store the [Time.Location]'s offset,
|
||||
* but not the location name. They therefore lose information about Daylight Saving Time.
|
||||
*
|
||||
* In addition to the required “wall clock” reading, a Time may contain an optional
|
||||
* reading of the current process's monotonic clock, to provide additional precision
|
||||
* for comparison or subtraction.
|
||||
* See the “Monotonic Clocks” section in the package documentation for details.
|
||||
*
|
||||
* Note that the Go == operator compares not just the time instant but also the
|
||||
* Location and the monotonic clock reading. Therefore, Time values should not
|
||||
* be used as map or database keys without first guaranteeing that the
|
||||
* identical Location has been set for all values, which can be achieved
|
||||
* through use of the UTC or Local method, and that the monotonic clock reading
|
||||
* has been stripped by setting t = t.Round(0). In general, prefer t.Equal(u)
|
||||
* to t == u, since t.Equal uses the most accurate comparison available and
|
||||
* correctly handles the case when only one of its arguments has a monotonic
|
||||
* clock reading.
|
||||
*/
|
||||
export type Time = any;
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/wails.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<title>Wails + Vue + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg" aria-hidden="true"></div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1351
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:dev": "vue-tsc && vite build --minify false --mode development",
|
||||
"build": "vue-tsc && vite build --mode production",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wailsio/runtime": "latest",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^8.0.5",
|
||||
"vue-tsc": "^1.0.11"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
@@ -0,0 +1,368 @@
|
||||
:root {
|
||||
/* Neon-night design tokens. */
|
||||
--ink: #06070f; /* darkest base behind the image */
|
||||
--text: #f4f6fb;
|
||||
--muted: #9aa6c0;
|
||||
--accent: #ff4d4d; /* Wails red */
|
||||
--accent-2: #ff2d72; /* pink, used in gradients */
|
||||
--glass: rgba(255, 255, 255, 0.05);
|
||||
--glass-strong: rgba(255, 255, 255, 0.08);
|
||||
--glass-border: rgba(255, 255, 255, 0.12);
|
||||
--radius: 11px;
|
||||
--grad: linear-gradient(135deg, #ff5a4d 0%, #ff2d72 100%);
|
||||
|
||||
/* Golden-ratio spacing scale (φ ≈ 1.618), used for the vertical rhythm. */
|
||||
--s-1: 0.618rem; /* φ⁻¹ */
|
||||
--s-2: 1rem;
|
||||
--s-3: 1.618rem; /* φ */
|
||||
--s-4: 2.618rem; /* φ² */
|
||||
--s-5: 4.236rem; /* φ³ */
|
||||
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
color-scheme: dark;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html {
|
||||
background: var(--ink);
|
||||
}
|
||||
html, body {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
/* Nothing in the chrome is selectable (it's an app, not a document); the
|
||||
text input opts back in below. */
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
/* Drag the window by its background. Interactive controls opt out with
|
||||
--wails-draggable: no-drag (the property inherits). */
|
||||
--wails-draggable: drag;
|
||||
}
|
||||
|
||||
/* Framework mount nodes (#app / #root) shouldn't affect layout, so the
|
||||
body flex centering applies directly to .container. */
|
||||
#app, #root { display: contents; }
|
||||
|
||||
/* Interactive elements: clickable (not draggable) and, for the input, selectable. */
|
||||
.brand-mark, .brand-badge, .input-box, .btn, .footer-docs, .toast {
|
||||
--wails-draggable: no-drag;
|
||||
}
|
||||
.input, input[type="text"] {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* ----- Background layer ----------------------------------------------------
|
||||
A fixed, full-viewport layer rather than `background-attachment: fixed`
|
||||
(which iOS WKWebView ignores / renders janky). A dark gradient is layered
|
||||
over the photo so foreground text stays legible. On desktop the gradient is
|
||||
weighted to the left, where the content sits. */
|
||||
.bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(6, 7, 15, 0.94) 0%, rgba(6, 7, 15, 0.78) 32%,
|
||||
rgba(6, 7, 15, 0.42) 64%, rgba(6, 7, 15, 0.30) 100%),
|
||||
linear-gradient(0deg, rgba(6, 7, 15, 0.70) 0%, rgba(6, 7, 15, 0.10) 45%),
|
||||
url("/bg-desktop.jpg") center / cover no-repeat;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column; /* content area + footer pinned to the bottom */
|
||||
/* Respect mobile safe areas (notch / home indicator). No bottom padding:
|
||||
the footer sits flush at the bottom edge. */
|
||||
/* Wider outer margins on the golden scale (φ² top/right, φ³ left). */
|
||||
padding: max(var(--s-4), env(safe-area-inset-top)) max(var(--s-4), env(safe-area-inset-right))
|
||||
0 max(var(--s-5), env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1 0 auto; /* fill the space above the footer */
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center; /* vertically centre the content */
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* ----- Brand row ----------------------------------------------------------- */
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
/* No top margin — the content block is vertically centred, so an extra top
|
||||
margin would push everything (incl. the input) down. φ² gap below. */
|
||||
margin-bottom: var(--s-4);
|
||||
}
|
||||
.brand-mark, .brand-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
.brand-mark:hover, .brand-badge:hover {
|
||||
transform: translateY(-2px);
|
||||
filter: drop-shadow(0 0 1em rgba(255, 77, 77, 0.45));
|
||||
}
|
||||
/* Logo heights sit on the golden scale (φ³ = 4.236rem). The Wails mark carries
|
||||
its wordmark, so the badge is one golden step smaller (φ³ ÷ √φ) to keep the
|
||||
pair visually balanced. */
|
||||
.brand-logo { height: var(--s-5); width: auto; display: block; object-fit: contain; }
|
||||
.brand-badge img { height: calc(var(--s-5) / 1.272); width: auto; display: block; object-fit: contain; }
|
||||
|
||||
/* ----- Version badge ------------------------------------------------------- */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
margin-bottom: 1.1rem;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: #ff7a8f;
|
||||
background: rgba(255, 45, 114, 0.14);
|
||||
border: 1px solid rgba(255, 45, 114, 0.32);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* ----- Heading ------------------------------------------------------------- */
|
||||
.title {
|
||||
margin: 0 0 var(--s-2);
|
||||
max-width: 100%;
|
||||
font-size: clamp(2.5rem, 7.5vw, 3.7rem);
|
||||
line-height: 1.05;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.title-accent {
|
||||
background: var(--grad);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* The framework word ("JavaScript") crossfades to the entered name on Greet:
|
||||
the outgoing word is taken out of flow and fades out while the incoming word
|
||||
fades in over the same spot. */
|
||||
.title-name { position: relative; display: inline-block; }
|
||||
.title-name-text {
|
||||
display: inline-block;
|
||||
transition: opacity 0.6s ease, transform 0.6s cubic-bezier(0.22, 0.68, 0.2, 1);
|
||||
}
|
||||
.title-name-text.is-outgoing { position: absolute; left: 0; top: 0; }
|
||||
.title-name-text.is-entering { opacity: 0; transform: translateY(0.3em); } /* rises in from below */
|
||||
.title-name-text.is-leaving { opacity: 0; transform: translateY(-0.3em); } /* lifts out upward */
|
||||
|
||||
.subtitle {
|
||||
margin: 0 0 var(--s-5);
|
||||
max-width: 32rem;
|
||||
color: var(--muted);
|
||||
font-size: clamp(1.05rem, 2.9vw, 1.2rem);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* ----- Greet input + button ------------------------------------------------ */
|
||||
/* Input row and the card below it are sized down so they sit more compact
|
||||
under the heading. */
|
||||
.greet { width: 62%; }
|
||||
|
||||
/* Input font is pinned at 16px: iOS Safari/WKWebView zooms the page when an
|
||||
input under 16px gains focus, so the whole control is sized around that.
|
||||
Tight vertical padding keeps it ~36px tall, icon : control ≈ 1 : φ. */
|
||||
.input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
width: 100%;
|
||||
padding: 4px 5px 4px 11px;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
.input-box:focus-within { border-color: rgba(255, 77, 77, 0.55); }
|
||||
|
||||
.input-icon {
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
flex: 0 0 auto;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.input,
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 16px;
|
||||
padding: 5px 2px;
|
||||
outline: none;
|
||||
}
|
||||
.input::placeholder { color: rgba(154, 166, 192, 0.7); }
|
||||
|
||||
.btn {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border: none;
|
||||
background: var(--grad);
|
||||
color: #fff;
|
||||
padding: 6px 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 18px rgba(255, 45, 114, 0.35);
|
||||
transition: transform 0.1s ease, box-shadow 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
.btn svg { width: 17px; height: 17px; }
|
||||
.btn:hover { box-shadow: 0 8px 24px rgba(255, 45, 114, 0.5); }
|
||||
.btn:active { transform: scale(0.97); }
|
||||
|
||||
/* ----- Toast (the reply from Go) ------------------------------------------- */
|
||||
/* Fixed overlay near the bottom, so showing the greeting never reflows the page.
|
||||
The Go-blue badge makes it obvious the string was produced by the Go backend. */
|
||||
.toast {
|
||||
position: fixed;
|
||||
/* Top-right with an equal golden-ratio inset (φ = 1.618rem) on both edges,
|
||||
clear of the macOS traffic lights (which are on the left). Falls back to
|
||||
the notch / safe area when larger. Drops in from just above. */
|
||||
top: max(var(--s-3), env(safe-area-inset-top));
|
||||
right: max(var(--s-3), env(safe-area-inset-right));
|
||||
z-index: 20;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
max-width: min(86vw, 420px);
|
||||
padding: 11px 16px;
|
||||
background: rgba(13, 17, 28, 0.72);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 14px;
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
backdrop-filter: blur(16px);
|
||||
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.45);
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
line-height: 1.45;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(-16px);
|
||||
transition: opacity 0.3s ease, transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.toast.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
/* Muted, on-palette label so it's clear the string came from the Go backend. */
|
||||
.toast-label {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.toast-msg { min-width: 0; }
|
||||
|
||||
/* ----- Footer -------------------------------------------------------------- */
|
||||
/* Thin bar pinned to the bottom: version far-left, live clock centered. */
|
||||
.footer-divider {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin: 0;
|
||||
}
|
||||
.footer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr; /* version | clock | spacer */
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
/* keep the bar off the very edge and clear of the home indicator */
|
||||
padding: 11px max(0.5rem, env(safe-area-inset-right))
|
||||
max(11px, env(safe-area-inset-bottom)) 0;
|
||||
color: var(--muted);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.footer-version {
|
||||
justify-self: start;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.footer-time {
|
||||
justify-self: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
.footer-time svg { width: 19px; height: 19px; opacity: 0.85; }
|
||||
.footer-docs {
|
||||
justify-self: end;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.footer-docs:hover { color: var(--text); }
|
||||
.footer-docs svg { width: 17px; height: 17px; }
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 8px var(--accent);
|
||||
}
|
||||
|
||||
/* ----- Mobile -------------------------------------------------------------- */
|
||||
@media (max-width: 640px) {
|
||||
.bg {
|
||||
background:
|
||||
linear-gradient(0deg, rgba(6, 7, 15, 0.92) 0%, rgba(6, 7, 15, 0.55) 40%,
|
||||
rgba(6, 7, 15, 0.45) 100%),
|
||||
url("/bg-mobile.jpg") center / cover no-repeat;
|
||||
}
|
||||
.container { align-items: center; text-align: center; margin: 0 auto; }
|
||||
/* Stack the title onto two lines ("Wails +" / "Javascript") like the target. */
|
||||
.title-accent { display: block; }
|
||||
.subtitle { margin-left: auto; margin-right: auto; }
|
||||
/* A touch more width back on small screens so the input stays usable. */
|
||||
.greet { width: 86%; }
|
||||
/* Trim the surround on a phone, but keep the input at 16px (iOS focus-zoom
|
||||
guard) — only the padding/button/icon shrink, not the field's text. */
|
||||
.input-box { padding: 3px 4px 3px 10px; gap: 6px; }
|
||||
.input-icon { width: 21px; height: 21px; }
|
||||
.input { font-size: 16px; padding: 4px 2px; }
|
||||
.btn { padding: 5px 13px; font-size: 14px; gap: 5px; }
|
||||
.btn svg { width: 16px; height: 16px; }
|
||||
/* Three items (version / clock / docs) share a narrow row — shrink to fit. */
|
||||
.footer { font-size: 11px; gap: 4px; }
|
||||
.footer-time { gap: 5px; }
|
||||
.footer-time svg { width: 13px; height: 13px; }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,328 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { RouterView, useRouter, useRoute } from 'vue-router'
|
||||
import { Anchor, ShieldAlert, HeartHandshake, Menu, X } from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const mobileMenuOpen = ref(false)
|
||||
|
||||
const navItems = [
|
||||
{ name: '메인 홈', path: '/', icon: Anchor },
|
||||
{ name: '캠페인 소개', path: '/about', icon: ShieldAlert },
|
||||
{ name: '희망 기부', path: '/donation', icon: HeartHandshake }
|
||||
]
|
||||
|
||||
const navigateTo = (path: string) => {
|
||||
router.push(path)
|
||||
mobileMenuOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<!-- Header -->
|
||||
<header class="app-header">
|
||||
<div class="header-logo" @click="navigateTo('/')">
|
||||
<div class="logo-icon-wrapper">
|
||||
<Anchor class="logo-icon" />
|
||||
</div>
|
||||
<div class="logo-text">
|
||||
<span class="logo-title">희망의 바다</span>
|
||||
<span class="logo-subtitle">수협중앙회</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Nav -->
|
||||
<nav class="desktop-nav">
|
||||
<button
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:class="['nav-link', { active: route.path === item.path }]"
|
||||
@click="navigateTo(item.path)"
|
||||
>
|
||||
<component :is="item.icon" class="nav-icon" />
|
||||
<span>{{ item.name }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile Nav Toggle -->
|
||||
<button class="mobile-nav-toggle" @click="mobileMenuOpen = !mobileMenuOpen">
|
||||
<Menu v-if="!mobileMenuOpen" />
|
||||
<X v-else />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Mobile Nav Menu -->
|
||||
<transition name="slide-fade">
|
||||
<nav v-if="mobileMenuOpen" class="mobile-nav">
|
||||
<button
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:class="['mobile-nav-link', { active: route.path === item.path }]"
|
||||
@click="navigateTo(item.path)"
|
||||
>
|
||||
<component :is="item.icon" class="nav-icon" />
|
||||
<span>{{ item.name }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</transition>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="main-content">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</RouterView>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
<p class="footer-title">수협중앙회 희망의 바다 만들기 운동본부</p>
|
||||
<p class="footer-text">© 2026 수협중앙회. All rights reserved. 깨끗하고 풍요로운 바다, 함께 만들어갑니다.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 전역 스코프 및 공통 레이아웃 스타일 정의 */
|
||||
:root {
|
||||
--primary-color: #00d2ff;
|
||||
--primary-rgb: 0, 210, 255;
|
||||
--secondary-color: #0066ff;
|
||||
--accent-color: #ff6b6b;
|
||||
--dark-bg: #030814;
|
||||
--glass-bg: rgba(6, 17, 38, 0.65);
|
||||
--glass-border: rgba(0, 210, 255, 0.15);
|
||||
--text-color: #e0f2fe;
|
||||
}
|
||||
|
||||
body {
|
||||
background: radial-gradient(circle at center, #061530 0%, #030814 100%) !important;
|
||||
color: var(--text-color);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0 !important; /* Wails 기본 padding 덮어쓰기 */
|
||||
}
|
||||
|
||||
/* Wails default background override */
|
||||
.bg {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 70px;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 40px;
|
||||
z-index: 100;
|
||||
--wails-draggable: drag;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
--wails-draggable: no-drag;
|
||||
}
|
||||
|
||||
.logo-icon-wrapper {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 15px rgba(0, 210, 255, 0.4);
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
color: white;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-3px); }
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.5px;
|
||||
background: linear-gradient(to right, #ffffff, var(--primary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.logo-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.desktop-nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
--wails-draggable: no-drag;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--primary-color);
|
||||
background: rgba(0, 210, 255, 0.08);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: white;
|
||||
background: linear-gradient(135deg, rgba(0, 210, 255, 0.2), rgba(0, 102, 255, 0.2));
|
||||
border: 1px solid var(--primary-color);
|
||||
box-shadow: 0 0 15px rgba(0, 210, 255, 0.25);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.mobile-nav-toggle {
|
||||
display: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
--wails-draggable: no-drag;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-top: 70px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
background: #020611;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding: 30px 40px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.footer-title {
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-enter-active,
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.desktop-nav {
|
||||
display: none;
|
||||
}
|
||||
.mobile-nav-toggle {
|
||||
display: block;
|
||||
}
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
}
|
||||
.mobile-nav {
|
||||
position: fixed;
|
||||
top: 70px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
gap: 8px;
|
||||
z-index: 99;
|
||||
}
|
||||
.mobile-nav-link {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.mobile-nav-link.active {
|
||||
color: white;
|
||||
background: rgba(0, 210, 255, 0.15);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, useTemplateRef} from 'vue'
|
||||
import {Events, WML} from "@wailsio/runtime";
|
||||
import {GreetService} from "../../bindings/changeme";
|
||||
|
||||
// Show the actual Wails version this project was generated against.
|
||||
const wailsVersion = "v3.0.0-alpha2.106";
|
||||
|
||||
const name = ref('');
|
||||
const time = ref('Listening for Time event...');
|
||||
|
||||
const titleNameRef = useTemplateRef<HTMLElement>('titleNameRef');
|
||||
const toastRef = useTemplateRef<HTMLElement>('toastRef');
|
||||
const resultRef = useTemplateRef<HTMLElement>('resultRef');
|
||||
let toastTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
// Crossfade the framework word in the heading ("Wails + Vue") to the name the
|
||||
// user entered ("Wails + <name>"): the old word fades out while the new one
|
||||
// fades in over the same spot.
|
||||
function swapTitleName(newName: string) {
|
||||
const titleNameElement = titleNameRef.value;
|
||||
if (!titleNameElement) {
|
||||
return;
|
||||
}
|
||||
const current = titleNameElement.querySelector('.title-name-text:not(.is-outgoing)');
|
||||
if (!current || current.textContent === newName) {
|
||||
return;
|
||||
}
|
||||
const incoming = document.createElement('span');
|
||||
incoming.className = 'title-name-text is-entering';
|
||||
incoming.textContent = newName;
|
||||
current.classList.add('is-outgoing');
|
||||
titleNameElement.appendChild(incoming);
|
||||
// Force a reflow so the transitions run from the starting state.
|
||||
void incoming.offsetWidth;
|
||||
incoming.classList.remove('is-entering');
|
||||
current.classList.add('is-leaving');
|
||||
current.addEventListener('transitionend', () => current.remove(), {once: true});
|
||||
}
|
||||
|
||||
// Pop the toast with the message Go returned, then auto-dismiss it.
|
||||
function showToast(message: string) {
|
||||
if (!resultRef.value || !toastRef.value) {
|
||||
return;
|
||||
}
|
||||
resultRef.value.innerText = message;
|
||||
toastRef.value.classList.add('is-visible');
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => toastRef.value?.classList.remove('is-visible'), 4000);
|
||||
}
|
||||
|
||||
const doGreet = () => {
|
||||
let n = name.value || 'anonymous';
|
||||
swapTitleName(n);
|
||||
GreetService.Greet(n).then(showToast).catch(console.error);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Events.On('time', (timeValue: { data: string }) => {
|
||||
// On a narrow screen the full RFC1123 stamp is too wide for the footer, so
|
||||
// show just the clock time there (matching the CSS breakpoint).
|
||||
const full = timeValue.data;
|
||||
const compact = (full.match(/\d{1,2}:\d{2}:\d{2}/) || [full])[0];
|
||||
time.value = window.matchMedia('(max-width: 640px)').matches ? compact : full;
|
||||
});
|
||||
// Wire up data-wml-openURL links (logos + footer "Docs" link).
|
||||
WML.Reload();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="container">
|
||||
<header class="brand">
|
||||
<a class="brand-mark" data-wml-openURL="https://v3.wails.io" aria-label="Wails website">
|
||||
<img src="/wails.png" class="brand-logo" alt="Wails logo"/>
|
||||
</a>
|
||||
<a class="brand-badge" data-wml-openURL="https://vuejs.org/" aria-label="Vue">
|
||||
<img src="/vue.svg" alt="Vue logo"/>
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<h1 class="title"><span class="title-accent">Wails +</span> <span class="title-name" ref="titleNameRef"><span class="title-name-text">Vue</span></span></h1>
|
||||
<p class="subtitle">Build beautiful cross-platform apps with Go and Vue.</p>
|
||||
|
||||
<div class="greet">
|
||||
<div class="input-box" id="input">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<input aria-label="input" class="input" id="name" v-model="name" type="text" placeholder="Your name" autocomplete="off"/>
|
||||
<button aria-label="greet-btn" class="btn" @click="doGreet">Greet
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<hr class="footer-divider"/>
|
||||
<footer class="footer">
|
||||
<span class="footer-version">{{ wailsVersion }}</span>
|
||||
<span class="footer-time">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<span id="time">{{ time }}</span>
|
||||
</span>
|
||||
<a class="footer-docs" data-wml-openURL="https://v3.wails.io" aria-label="Wails documentation">Docs
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></svg>
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<!-- Toast: shows the greeting returned by the Go backend (overlay, so it never
|
||||
reflows the layout). -->
|
||||
<div class="toast" id="toast" ref="toastRef" role="status" aria-live="polite">
|
||||
<span class="toast-label">From Go</span>
|
||||
<span aria-label="result" class="toast-msg" id="result" ref="resultRef"></span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import Home from '../views/Home.vue';
|
||||
import About from '../views/About.vue';
|
||||
import Donation from '../views/Donation.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: About
|
||||
},
|
||||
{
|
||||
path: '/donation',
|
||||
name: 'Donation',
|
||||
component: Donation
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,563 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowRight, Compass, Shield, Award, Users } from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const activeTab = ref('seed')
|
||||
|
||||
const activities = {
|
||||
seed: {
|
||||
title: '수산종자 방류 사업',
|
||||
subtitle: '수산자원의 회복과 복원',
|
||||
desc: '고갈되어 가는 우리 바다의 어족 자원을 늘리고 어업인의 소득을 보장하기 위해, 지역 해역 특성에 맞는 우수한 품종의 치어를 방류합니다.',
|
||||
points: [
|
||||
'방류 효과 분석을 바탕으로 한 경제성 높은 어종 선정',
|
||||
'전국 연안 지자체 및 회원조합과 공동으로 매년 추진',
|
||||
'감성돔, 조피볼락, 꽃게 등 대표 고부가가치 품종 방류',
|
||||
'어업인의 수산 소득 증대 및 미래 생태 식량 확보'
|
||||
],
|
||||
infographic: [
|
||||
{ step: '01', title: '우량 종자 확보', desc: '질병 검사를 마친 건강한 수산 치어 선별' },
|
||||
{ step: '02', title: '해역 적합도 평가', desc: '지역별 염도 및 온도에 알맞은 어종 연구' },
|
||||
{ step: '03', title: '체계적 방류', desc: '수산 자원 고갈 지점에서 집중 방류 실시' },
|
||||
{ step: '04', title: '추적 및 관리', desc: '방류된 치어의 생존율 및 자원조성 효과 모니터링' }
|
||||
]
|
||||
},
|
||||
cleanup: {
|
||||
title: '해양 쓰레기 수거 사업',
|
||||
subtitle: '조업 중 쓰레기 수매 및 수거 지원',
|
||||
desc: '바다 밑에 침적된 폐어구, 플라스틱 등 어업 활동을 방해하고 해양 생태계를 오염시키는 해양 쓰레기를 신속히 수거하여 깨끗한 환경을 만듭니다.',
|
||||
points: [
|
||||
'어업인 참여 유도를 위한 조업 중 인양 쓰레기 수매 사업 진행',
|
||||
'시민단체 및 연안 해안가 집중 정화 운동 전개',
|
||||
'침적 폐그물 수거를 통해 해양 동물의 유령어업(Ghost Fishing) 방지',
|
||||
'해양 플라스틱 오염 방지를 통한 미세 플라스틱 먹이사슬 차단'
|
||||
],
|
||||
infographic: [
|
||||
{ step: '01', title: '인양 및 수집', desc: '조업 중 수거한 쓰레기를 항구로 반입' },
|
||||
{ step: '02', title: '수협 수매 시스템', desc: '수협이 매입하여 쓰레기 회수율 극대화' },
|
||||
{ step: '03', title: '친환경 처리', desc: '재활용 가능 플라스틱과 폐어구 분류 처리' },
|
||||
{ step: '04', title: '바다 청정화', desc: '어장 환경의 개선과 선박 안전 운항 확보' }
|
||||
]
|
||||
},
|
||||
restoration: {
|
||||
title: '바닷속 환경 정화 사업',
|
||||
subtitle: '갯녹음 치유 및 생태계 복원',
|
||||
desc: '지구 온난화와 환경 오염으로 바다가 하얗게 사막화되는 갯녹음 현상(백화현상)을 방지하고, 해조류가 자라는 풍요로운 바다 숲을 만듭니다.',
|
||||
points: [
|
||||
'갯녹음 해역의 해적생물(성게 등) 집중 구제 및 유해 유기물 제거',
|
||||
'어장 환경 개선을 위한 바닥갈이(경운) 및 물갈이 사업 지원',
|
||||
'미생물 및 친환경 공법을 활용한 바다 해저 토양 정화',
|
||||
'바다숲 조성을 통해 수산 생물의 산란장 및 서식지 제공'
|
||||
],
|
||||
infographic: [
|
||||
{ step: '01', title: '현황 정밀 조사', desc: '갯녹음 및 바다 바닥 황폐화 상태 과학적 진단' },
|
||||
{ step: '02', title: '어장 바닥갈이', desc: '경운 작업을 통해 암반의 퇴적물과 유해물질 세척' },
|
||||
{ step: '03', title: '해조류 이식 및 관리', desc: '다시마, 감태 등 해조류를 안착시켜 바다숲 형성' },
|
||||
{ step: '04', title: '풍요로운 어장 복원', desc: '어패류가 산란하고 성장할 수 있는 보금자리 완성' }
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="about-container">
|
||||
<!-- Intro Hero -->
|
||||
<section class="about-hero">
|
||||
<div class="glow-bg"></div>
|
||||
<div class="hero-content">
|
||||
<h1 class="about-title">바다를 살리는 3가지 약속</h1>
|
||||
<p class="about-subtitle-text">
|
||||
수협중앙회는 2007년부터 전국의 어업인들과 힘을 합쳐,<br />
|
||||
바다의 수산 자원을 회복하고 해양 환경을 맑게 가꾸는 공익적 실천을 이어가고 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabbed Activities Grid -->
|
||||
<section class="activities-section">
|
||||
<div class="tab-buttons">
|
||||
<button
|
||||
:class="['tab-btn', { active: activeTab === 'seed' }]"
|
||||
@click="activeTab = 'seed'"
|
||||
>
|
||||
<Compass class="tab-icon" />
|
||||
<span>수산종자 방류</span>
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-btn', { active: activeTab === 'cleanup' }]"
|
||||
@click="activeTab = 'cleanup'"
|
||||
>
|
||||
<Shield class="tab-icon" />
|
||||
<span>쓰레기 수거</span>
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-btn', { active: activeTab === 'restoration' }]"
|
||||
@click="activeTab = 'restoration'"
|
||||
>
|
||||
<Award class="tab-icon" />
|
||||
<span>바닷속 환경 정화</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content Area -->
|
||||
<transition name="fade" mode="out-in">
|
||||
<div :key="activeTab" class="tab-content">
|
||||
<div class="content-left">
|
||||
<h2 class="content-title">{{ activities[activeTab].title }}</h2>
|
||||
<h3 class="content-subtitle">{{ activities[activeTab].subtitle }}</h3>
|
||||
<p class="content-desc">{{ activities[activeTab].desc }}</p>
|
||||
<ul class="point-list">
|
||||
<li v-for="(point, idx) in activities[activeTab].points" :key="idx" class="point-item">
|
||||
<span class="bullet"></span>
|
||||
<span>{{ point }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="content-right">
|
||||
<h4 class="right-title">추진 프로세스</h4>
|
||||
<div class="infographic-flow">
|
||||
<div
|
||||
v-for="step in activities[activeTab].infographic"
|
||||
:key="step.step"
|
||||
class="flow-step"
|
||||
>
|
||||
<div class="step-num">{{ step.step }}</div>
|
||||
<div class="step-info">
|
||||
<h5 class="step-title">{{ step.title }}</h5>
|
||||
<p class="step-desc">{{ step.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</section>
|
||||
|
||||
<!-- Citizen Campaign Info -->
|
||||
<section class="citizen-section">
|
||||
<div class="citizen-card">
|
||||
<div class="citizen-icon-wrapper">
|
||||
<Users class="citizen-icon" />
|
||||
</div>
|
||||
<div class="citizen-info">
|
||||
<span class="citizen-badge">국민 참여 프로그램</span>
|
||||
<h2 class="citizen-title">모두의 바다, 함께海 캠페인</h2>
|
||||
<p class="citizen-desc">
|
||||
해양 환경 정화에 관심이 있는 학생, 일반 시민 모임, 동호회가 바닷가 정화 활동을 실천할 수 있도록 수협중앙회가 활동 경비(최대 100만 원)를 지원합니다.
|
||||
</p>
|
||||
<div class="citizen-benefits">
|
||||
<div class="benefit-item">
|
||||
<div class="benefit-dot"></div>
|
||||
<span>전국 항·포구 및 해안가 정화 단체 지원</span>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<div class="benefit-dot"></div>
|
||||
<span>청소년 교육 및 환경 인식 제고</span>
|
||||
</div>
|
||||
<div class="benefit-item">
|
||||
<div class="benefit-dot"></div>
|
||||
<span>기금 기부를 통한 가상의 바다 정화 동참</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="router.push('/donation')">
|
||||
<span>참여하고 바다 가꾸기</span>
|
||||
<ArrowRight class="btn-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.about-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 80px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* About Hero */
|
||||
.about-hero {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
border-radius: 24px;
|
||||
background: rgba(6, 17, 38, 0.4);
|
||||
border: 1px solid rgba(0, 210, 255, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glow-bg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(0, 210, 255, 0.15) 0%, transparent 70%);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.about-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 16px 0;
|
||||
background: linear-gradient(to right, #ffffff, var(--primary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.about-subtitle-text {
|
||||
font-size: 1.1rem;
|
||||
color: #94a3b8;
|
||||
line-height: 1.6;
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Activities Section */
|
||||
.activities-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.tab-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: rgba(6, 17, 38, 0.6);
|
||||
border: 1px solid rgba(0, 210, 255, 0.1);
|
||||
padding: 16px 30px;
|
||||
border-radius: 16px;
|
||||
color: #94a3b8;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
border-color: rgba(0, 210, 255, 0.3);
|
||||
color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: linear-gradient(135deg, rgba(0, 210, 255, 0.15), rgba(0, 102, 255, 0.15));
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
box-shadow: 0 5px 20px rgba(0, 210, 255, 0.15);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
gap: 40px;
|
||||
background: rgba(6, 17, 38, 0.4);
|
||||
border: 1px solid rgba(0, 210, 255, 0.08);
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.content-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.content-subtitle {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.content-desc {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.point-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.point-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
color: #cbd5e1;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bullet {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
margin-top: 8px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 8px var(--primary-color);
|
||||
}
|
||||
|
||||
/* Right Column Infographic Flow */
|
||||
.content-right {
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding-left: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.right-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
margin: 0 0 20px 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.infographic-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flow-step:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 17px;
|
||||
width: 2px;
|
||||
height: calc(100% - 10px);
|
||||
background: linear-gradient(to bottom, var(--primary-color), rgba(0, 102, 255, 0.1));
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 10px rgba(0, 210, 255, 0.3);
|
||||
}
|
||||
|
||||
.step-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Citizen Campaign Info */
|
||||
.citizen-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.citizen-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 40px;
|
||||
background: radial-gradient(circle at bottom right, rgba(0, 102, 255, 0.1) 0%, rgba(3, 8, 20, 0.8) 80%);
|
||||
border: 1px solid rgba(0, 210, 255, 0.15);
|
||||
border-radius: 24px;
|
||||
padding: 50px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.citizen-icon-wrapper {
|
||||
background: linear-gradient(135deg, rgba(0, 210, 255, 0.1), rgba(0, 102, 255, 0.1));
|
||||
border: 1px solid rgba(0, 210, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.citizen-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: var(--primary-color);
|
||||
animation: pulse 2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); filter: drop-shadow(0 0 10px rgba(0, 210, 255, 0.2)); }
|
||||
100% { transform: scale(1.05); filter: drop-shadow(0 0 20px rgba(0, 210, 255, 0.5)); }
|
||||
}
|
||||
|
||||
.citizen-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.citizen-badge {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #ff6b6b;
|
||||
background: rgba(255, 107, 107, 0.12);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.citizen-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.citizen-desc {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.citizen-benefits {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #cbd5e1;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.benefit-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #ff6b6b;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.tab-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
.content-right {
|
||||
border-left: none;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding-left: 0;
|
||||
padding-top: 30px;
|
||||
}
|
||||
.citizen-card {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
padding: 30px;
|
||||
}
|
||||
.citizen-icon-wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
.citizen-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tab-buttons {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.tab-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,784 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { Heart, Send, CheckCircle2, MessageSquare, Shield, Fish, Trash2, HelpCircle } from 'lucide-vue-next'
|
||||
|
||||
const stats = ref({
|
||||
totalDonors: 12543,
|
||||
totalAmount: 627150000,
|
||||
releasedFish: 4200000,
|
||||
collectedWaste: 185000,
|
||||
purifiedArea: 95000
|
||||
})
|
||||
|
||||
const recentDonations = ref<any[]>([])
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
amount: 30000,
|
||||
message: ''
|
||||
})
|
||||
|
||||
const showSuccess = ref(false)
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const predefinedAmounts = [10000, 30000, 50000, 100000, 300000]
|
||||
|
||||
// Real-time impact simulation computed values
|
||||
const simulatedImpact = computed(() => {
|
||||
const amount = form.value.amount || 0
|
||||
const multiplier = amount / 10000
|
||||
return {
|
||||
fish: Math.floor(multiplier * 2),
|
||||
waste: (multiplier * 0.5).toFixed(1),
|
||||
area: (multiplier * 0.2).toFixed(2)
|
||||
}
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const CampaignService = await import('../../bindings/changeme/campaignservice')
|
||||
const currentStats = await CampaignService.GetStats()
|
||||
if (currentStats) stats.value = currentStats
|
||||
const list = await CampaignService.GetRecentDonations()
|
||||
if (list) {
|
||||
// Reverse list to show newest first
|
||||
recentDonations.value = [...list].reverse()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Wails bindings not available, using mock local data.', e)
|
||||
// Local mock data fallback
|
||||
recentDonations.value = [
|
||||
{ name: '김어부', amount: 50000, message: '바다가 다시 풍요로워지길 바랍니다.', timestamp: new Date(Date.now() - 10 * 60000).toISOString() },
|
||||
{ name: '이바다', amount: 30000, message: '깨끗한 바다를 우리 아이들에게!', timestamp: new Date(Date.now() - 60 * 60000).toISOString() },
|
||||
{ name: '박수협', amount: 100000, message: '어업인 여러분 힘내세요!', timestamp: new Date(Date.now() - 180 * 60000).toISOString() }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
const handleDonate = async () => {
|
||||
if (form.value.amount <= 0) return
|
||||
isSubmitting.value = true
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
const CampaignService = await import('../../bindings/changeme/campaignservice')
|
||||
const newStats = await CampaignService.Donate(form.value.name, form.value.amount, form.value.message)
|
||||
if (newStats) stats.value = newStats
|
||||
|
||||
// Refresh donation list
|
||||
const list = await CampaignService.GetRecentDonations()
|
||||
if (list) recentDonations.value = [...list].reverse()
|
||||
|
||||
showSuccessModal()
|
||||
} catch (e) {
|
||||
console.warn('Wails backend Donate call skipped. Simulating successfully on frontend.', e)
|
||||
// Simulating locally if not running in Wails
|
||||
stats.value.totalDonors += 1
|
||||
stats.value.totalAmount += form.value.amount
|
||||
|
||||
const multiplier = form.value.amount / 10000
|
||||
stats.value.releasedFish += Math.floor(multiplier * 2)
|
||||
stats.value.collectedWaste += Math.floor(multiplier * 0.5)
|
||||
stats.value.purifiedArea += Math.floor(multiplier * 0.2)
|
||||
|
||||
recentDonations.value.unshift({
|
||||
name: form.value.name || '익명의 기부자',
|
||||
amount: form.value.amount,
|
||||
message: form.value.message || '푸른 바다를 함께 응원합니다!',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
showSuccessModal()
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showSuccessModal = () => {
|
||||
showSuccess.value = true
|
||||
// Reset form
|
||||
form.value.name = ''
|
||||
form.value.message = ''
|
||||
}
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(num)
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const d = new Date(dateStr)
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="donation-container">
|
||||
<!-- Hero Header -->
|
||||
<div class="donation-hero">
|
||||
<h1 class="page-title">희망의 바다 기금 기부</h1>
|
||||
<p class="page-subtitle">수협중앙회와 함께 더 맑은 바다 생태계를 가꾸는 가장 가치 있는 첫걸음입니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Grid layout -->
|
||||
<div class="donation-grid">
|
||||
<!-- Left Column: Form and Impact simulation -->
|
||||
<div class="column-left">
|
||||
<!-- Interactive Impact Simulator Card -->
|
||||
<div class="card simulator-card">
|
||||
<h3 class="card-title"><Fish class="card-icon" /> 실시간 기부 효과 시뮬레이션</h3>
|
||||
<p class="card-desc">기부하시는 금액에 따라 우리 바다에 생겨날 놀라운 변화를 확인해보세요.</p>
|
||||
|
||||
<div class="simulator-graphic">
|
||||
<!-- Ocean environment visual feedback -->
|
||||
<div class="mini-ocean">
|
||||
<div class="sim-bubbles">
|
||||
<div v-for="n in 6" :key="n" class="sim-bubble" :style="`--i:${n}`"></div>
|
||||
</div>
|
||||
|
||||
<!-- Floating simulated fish -->
|
||||
<div class="fish-pond">
|
||||
<div
|
||||
v-for="n in Math.min(simulatedImpact.fish, 10)"
|
||||
:key="n"
|
||||
class="sim-fish"
|
||||
:style="`--speed:${1.5 + Math.random() * 2}s; --delay:${Math.random() * 2}s; --y:${15 + (n * 8)}%`"
|
||||
>
|
||||
🐟
|
||||
</div>
|
||||
<div v-if="simulatedImpact.fish === 0" class="no-fish-text">치어가 방류 대기 중입니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Impact Metrics Grid -->
|
||||
<div class="impact-metrics">
|
||||
<div class="metric-box">
|
||||
<span class="m-icon"><Fish /></span>
|
||||
<span class="m-val">{{ simulatedImpact.fish }} 마리</span>
|
||||
<span class="m-lbl">수산 치어 방류</span>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span class="m-icon"><Trash2 /></span>
|
||||
<span class="m-val">{{ simulatedImpact.waste }} kg</span>
|
||||
<span class="m-lbl">해양 쓰레기 수거</span>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span class="m-icon"><Shield /></span>
|
||||
<span class="m-val">{{ simulatedImpact.area }} m²</span>
|
||||
<span class="m-lbl">바닷속 갯녹음 정화</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Donation Form Card -->
|
||||
<div class="card form-card">
|
||||
<h3 class="card-title"><Heart class="card-icon form-icon" /> 가상 후원 참여 폼</h3>
|
||||
<p class="card-desc">후원 금액과 응원 메시지는 실시간 캠페인 대시보드에 반영됩니다.</p>
|
||||
|
||||
<form @submit.prevent="handleDonate" class="donation-form">
|
||||
<!-- Name Input -->
|
||||
<div class="form-group">
|
||||
<label for="donor-name" class="form-label">후원자명 (또는 단체명)</label>
|
||||
<input
|
||||
id="donor-name"
|
||||
type="text"
|
||||
v-model="form.name"
|
||||
placeholder="익명의 기부자"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Amount Input & Preset Buttons -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">후원 금액 선택</label>
|
||||
<div class="preset-buttons">
|
||||
<button
|
||||
type="button"
|
||||
v-for="amt in predefinedAmounts"
|
||||
:key="amt"
|
||||
:class="['preset-btn', { active: form.amount === amt }]"
|
||||
@click="form.amount = amt"
|
||||
>
|
||||
{{ formatNumber(amt / 10000) }}만 원
|
||||
</button>
|
||||
</div>
|
||||
<div class="custom-amount-input">
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="form.amount"
|
||||
placeholder="직접 입력 (원)"
|
||||
min="1000"
|
||||
step="1000"
|
||||
class="form-input custom-input"
|
||||
/>
|
||||
<span class="currency-label">원</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Input -->
|
||||
<div class="form-group">
|
||||
<label for="donor-msg" class="form-label">바다를 위한 응원 메시지</label>
|
||||
<textarea
|
||||
id="donor-msg"
|
||||
v-model="form.message"
|
||||
placeholder="깨끗한 바다를 향한 어업인과 수협에 전할 격려의 한마디를 작성해주세요."
|
||||
class="form-textarea"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-submit" :disabled="isSubmitting || form.amount < 1000">
|
||||
<Heart class="submit-icon" />
|
||||
<span>{{ isSubmitting ? '전송 중...' : '기금 후원하기' }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Live dashboard stats and recent list -->
|
||||
<div class="column-right">
|
||||
<!-- Campaign Stats Box -->
|
||||
<div class="card stats-box-card">
|
||||
<h3 class="card-title">실시간 희망 바다 현황</h3>
|
||||
<div class="simple-stat-row">
|
||||
<span class="simple-lbl">누적 기금 조성액</span>
|
||||
<span class="simple-val accent-text">{{ formatNumber(stats.totalAmount) }} 원</span>
|
||||
</div>
|
||||
<div class="simple-stat-row">
|
||||
<span class="simple-lbl">누적 동참자</span>
|
||||
<span class="simple-val">{{ formatNumber(stats.totalDonors) }} 명</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Donations Feed -->
|
||||
<div class="card feed-card">
|
||||
<h3 class="card-title"><MessageSquare class="card-icon" /> 실시간 참여 피드</h3>
|
||||
<div class="donation-feed">
|
||||
<transition-group name="list">
|
||||
<div v-for="(item, idx) in recentDonations" :key="idx" class="feed-item">
|
||||
<div class="feed-header">
|
||||
<span class="feed-name">{{ item.name }} 님</span>
|
||||
<span class="feed-amt">{{ formatNumber(item.amount) }}원 후원</span>
|
||||
</div>
|
||||
<p class="feed-msg">"{{ item.message || '풍요로운 바다의 미래를 힘차게 응원합니다!' }}"</p>
|
||||
<span class="feed-time">{{ formatDate(item.timestamp) }}</span>
|
||||
</div>
|
||||
</transition-group>
|
||||
|
||||
<div v-if="recentDonations.length === 0" class="empty-feed">
|
||||
첫 번째 기부 참여를 기다리고 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Modal popup -->
|
||||
<div v-if="showSuccess" class="modal-overlay" @click.self="showSuccess = false">
|
||||
<div class="modal-content">
|
||||
<CheckCircle2 class="success-big-icon" />
|
||||
<h2 class="modal-title">후원이 완료되었습니다!</h2>
|
||||
<p class="modal-desc">
|
||||
보내주신 따뜻한 희망의 씨앗이 건강한 바다 생태계 복원과 우리 어업인 지원 기금에 소중하게 사용됩니다. 감사합니다!
|
||||
</p>
|
||||
<button class="btn btn-primary btn-modal-close" @click="showSuccess = false">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.donation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.donation-hero {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 10px 0;
|
||||
background: linear-gradient(to right, #ffffff, var(--primary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.donation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.column-left, .column-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
/* Card Common Base */
|
||||
.card {
|
||||
background: rgba(6, 17, 38, 0.4);
|
||||
border: 1px solid rgba(0, 210, 255, 0.08);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0 0 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
/* Live Simulator Visual Graphic */
|
||||
.simulator-card {
|
||||
border-color: rgba(0, 210, 255, 0.15);
|
||||
}
|
||||
|
||||
.simulator-graphic {
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mini-ocean {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
background: linear-gradient(to bottom, #072a56 0%, #03152d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 210, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sim-bubbles {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.sim-bubble {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
background: rgba(0, 210, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
left: calc(var(--i) * 15%);
|
||||
animation: sim-bubble-up 3s linear infinite;
|
||||
animation-delay: calc(var(--i) * 0.4s);
|
||||
}
|
||||
|
||||
@keyframes sim-bubble-up {
|
||||
0% { transform: translateY(0) scale(1); opacity: 0; }
|
||||
20% { opacity: 0.6; }
|
||||
90% { opacity: 0.6; }
|
||||
100% { transform: translateY(-200px) scale(1.3); opacity: 0; }
|
||||
}
|
||||
|
||||
.fish-pond {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.sim-fish {
|
||||
position: absolute;
|
||||
font-size: 1.3rem;
|
||||
animation: swim var(--speed) linear infinite alternate;
|
||||
animation-delay: var(--delay);
|
||||
left: -20px;
|
||||
top: var(--y);
|
||||
}
|
||||
|
||||
@keyframes swim {
|
||||
0% { left: -20px; transform: scaleX(1); }
|
||||
45% { transform: scaleX(1); }
|
||||
50% { transform: scaleX(-1); }
|
||||
95% { transform: scaleX(-1); }
|
||||
100% { left: calc(100% - 10px); transform: scaleX(1); }
|
||||
}
|
||||
|
||||
.no-fish-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Impact Metrics */
|
||||
.impact-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.metric-box {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(0, 210, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 15px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.m-icon {
|
||||
color: var(--primary-color);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.m-val {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.m-lbl {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Donation Form */
|
||||
.donation-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.form-input, .form-textarea {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
color: white;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 10px rgba(0, 210, 255, 0.2);
|
||||
}
|
||||
|
||||
.preset-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
color: #94a3b8;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.preset-btn:hover {
|
||||
background: rgba(0, 210, 255, 0.08);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.preset-btn.active {
|
||||
background: rgba(0, 210, 255, 0.15);
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-amount-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
width: 100%;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.currency-label {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
color: #64748b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff4747);
|
||||
color: white;
|
||||
justify-content: center;
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 5px 15px rgba(255, 107, 107, 0.3);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-submit:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(255, 107, 107, 0.5);
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
background: #1e293b;
|
||||
color: #475569;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.submit-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Campaign Stats Box */
|
||||
.stats-box-card {
|
||||
background: linear-gradient(135deg, rgba(6, 17, 38, 0.7), rgba(3, 8, 20, 0.7));
|
||||
border-color: rgba(0, 210, 255, 0.15);
|
||||
}
|
||||
|
||||
.simple-stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.simple-stat-row:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.simple-lbl {
|
||||
font-size: 0.9rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.simple-val {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.accent-text {
|
||||
color: var(--primary-color);
|
||||
text-shadow: 0 0 10px rgba(0, 210, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Feed Card */
|
||||
.feed-card {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.donation-feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
max-height: 380px;
|
||||
overflow-y: auto;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.donation-feed::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.donation-feed::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 210, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.feed-item {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.03);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feed-item:hover {
|
||||
border-color: rgba(0, 210, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.feed-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.feed-name {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.feed-amt {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.feed-msg {
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.feed-time {
|
||||
font-size: 0.75rem;
|
||||
color: #475569;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.empty-feed {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #475569;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Success Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(2, 6, 17, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 110;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: radial-gradient(circle at center, #061530 0%, #030814 100%);
|
||||
border: 1px solid rgba(0, 210, 255, 0.25);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 20px 50px rgba(0, 210, 255, 0.3);
|
||||
}
|
||||
|
||||
.success-big-icon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 20px;
|
||||
animation: scale-up-bounce 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
@keyframes scale-up-bounce {
|
||||
0% { transform: scale(0.3); opacity: 0; }
|
||||
70% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.modal-desc {
|
||||
font-size: 0.95rem;
|
||||
color: #94a3b8;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 30px 0;
|
||||
}
|
||||
|
||||
.btn-modal-close {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.donation-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,585 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Anchor, Users, Fish, Trash2, HelpCircle, ChevronRight, Waves } from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const stats = ref({
|
||||
totalDonors: 12543,
|
||||
totalAmount: 627150000,
|
||||
releasedFish: 4200000,
|
||||
collectedWaste: 185000,
|
||||
purifiedArea: 95000
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Dynamic import to handle non-Wails preview mode gracefully
|
||||
// @ts-ignore
|
||||
const CampaignService = await import('../../bindings/changeme/campaignservice')
|
||||
const res = await CampaignService.GetStats()
|
||||
if (res) {
|
||||
stats.value = res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Wails bindings not available, using mock statistics.', e)
|
||||
}
|
||||
})
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(num)
|
||||
}
|
||||
|
||||
const formatKoreanAmount = (num: number) => {
|
||||
if (num >= 100000000) {
|
||||
const eok = Math.floor(num / 100000000)
|
||||
const man = Math.floor((num % 100000000) / 10000)
|
||||
return `${eok}억 ${man > 0 ? man + '만' : ''}원`
|
||||
}
|
||||
return formatNumber(num) + '원'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section">
|
||||
<div class="ocean-bg">
|
||||
<div class="wave wave1"></div>
|
||||
<div class="wave wave2"></div>
|
||||
<div class="wave wave3"></div>
|
||||
<div class="light-ray"></div>
|
||||
<div class="bubbles">
|
||||
<div v-for="n in 15" :key="n" class="bubble" :style="`--i:${n}; --s:${Math.random() * 1.5 + 0.5}`"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-content">
|
||||
<span class="hero-badge"><Waves class="badge-icon" /> 수협중앙회 공익 캠페인</span>
|
||||
<h1 class="hero-title">
|
||||
생명의 바다,<br />
|
||||
<span class="highlight">희망을 품다</span>
|
||||
</h1>
|
||||
<p class="hero-desc">
|
||||
바다는 어업인의 일터이자 인류 생명의 젖줄입니다.<br />
|
||||
수협중앙회 '희망의 바다 만들기 운동'과 함께 깨끗하고 풍요로운 바다 생태계를 미래 세대에게 물려주세요.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<button class="btn btn-primary" @click="router.push('/donation')">
|
||||
<span>캠페인 동참하기</span>
|
||||
<HeartHandshake class="btn-icon" />
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="router.push('/about')">
|
||||
<span>자세히 알아보기</span>
|
||||
<ChevronRight class="btn-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Dashboard Section -->
|
||||
<section class="stats-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">희망의 바다 누적 성과</h2>
|
||||
<p class="section-subtitle">2007년부터 수협중앙회와 전국의 어업인이 땀 흘려 가꿔온 생명의 기록입니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<!-- Card 1: Donors -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper donor-icon">
|
||||
<Users class="stat-icon" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">누적 참여 후원자</span>
|
||||
<span class="stat-number">{{ formatNumber(stats.totalDonors) }} <span class="stat-unit">명</span></span>
|
||||
</div>
|
||||
<div class="stat-progress">
|
||||
<div class="progress-bar" style="width: 82%"></div>
|
||||
</div>
|
||||
<p class="stat-desc">많은 시민과 어업인이 한마음으로 동참하고 있습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Released Fish -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper fish-icon">
|
||||
<Fish class="stat-icon" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">방류된 수산종자</span>
|
||||
<span class="stat-number">{{ formatNumber(stats.releasedFish) }} <span class="stat-unit">마리</span></span>
|
||||
</div>
|
||||
<div class="stat-progress">
|
||||
<div class="progress-bar" style="width: 75%"></div>
|
||||
</div>
|
||||
<p class="stat-desc">조피볼락, 감성돔 등 우수 수산종자를 바다로 돌려보냈습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Waste -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper waste-icon">
|
||||
<Trash2 class="stat-icon" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">수거된 해양 쓰레기</span>
|
||||
<span class="stat-number">{{ formatNumber(stats.collectedWaste) }} <span class="stat-unit">kg</span></span>
|
||||
</div>
|
||||
<div class="stat-progress">
|
||||
<div class="progress-bar" style="width: 90%"></div>
|
||||
</div>
|
||||
<p class="stat-desc">조업 중 인양 폐기물과 침적 폐어구를 집중 정화했습니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Purified Area -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon-wrapper area-icon">
|
||||
<Anchor class="stat-icon" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">정화된 해역 면적</span>
|
||||
<span class="stat-number">{{ formatNumber(stats.purifiedArea) }} <span class="stat-unit">m²</span></span>
|
||||
</div>
|
||||
<div class="stat-progress">
|
||||
<div class="progress-bar" style="width: 68%"></div>
|
||||
</div>
|
||||
<p class="stat-desc">갯녹음 해역 복원 및 바닥갈이 사업을 진행했습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- General Info Alert -->
|
||||
<div class="total-donation-badge">
|
||||
<span class="badge-label">희망의 바다 조성 기금 누적액</span>
|
||||
<span class="badge-amount">{{ formatKoreanAmount(stats.totalAmount) }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Call to action Section -->
|
||||
<section class="cta-section">
|
||||
<div class="cta-card">
|
||||
<h3 class="cta-title">어업인과 국민이 함께 해양 생태계를 살립니다</h3>
|
||||
<p class="cta-desc">
|
||||
풍요로운 어장 복원과 맑은 바다 환경 조성은 우리 모두의 의무입니다. <br />
|
||||
당신의 작은 실천이 푸른 바다의 희망찬 미래를 만들어 냅니다.
|
||||
</p>
|
||||
<button class="btn btn-primary" @click="router.push('/donation')">지금 바로 후원하기</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 80px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 550px;
|
||||
padding: 40px;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.ocean-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, #021a3a 0%, #030814 100%);
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Waves animation */
|
||||
.wave {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 200%;
|
||||
height: 100px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath d='M321.39,56.44c58-10.79,114.16-30.13,172-41.86,82.39-16.72,168.19-17.73,250.45-.39C823.78,31,906.67,72,985.66,92.83c70.05,18.48,146.53,26.09,214.34,3V120H0V0C26.9,8.75,55.05,18.06,83.4,26.8,145.42,46,212.41,60.11,321.39,56.44Z' fill='%2300d2ff' fill-opacity='0.08'/%3E%3C/svg%3E") repeat-x;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.wave1 {
|
||||
animation: wave 20s linear infinite;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.wave2 {
|
||||
animation: wave 12s linear infinite reverse;
|
||||
opacity: 0.5;
|
||||
z-index: 3;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.wave3 {
|
||||
animation: wave 8s linear infinite;
|
||||
opacity: 0.2;
|
||||
z-index: 4;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
/* Underwater Light Ray */
|
||||
.light-ray {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 30%;
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, rgba(0, 210, 255, 0.15) 0%, transparent 70%);
|
||||
transform: skewX(-25deg);
|
||||
filter: blur(40px);
|
||||
pointer-events: none;
|
||||
animation: shimmer 6s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { opacity: 0.5; transform: skewX(-20deg) translateX(-10px); }
|
||||
100% { opacity: 1; transform: skewX(-30deg) translateX(10px); }
|
||||
}
|
||||
|
||||
/* Bubbles */
|
||||
.bubble {
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
background: rgba(0, 210, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 50%;
|
||||
width: calc(var(--s) * 15px);
|
||||
height: calc(var(--s) * 15px);
|
||||
left: calc(var(--i) * 6.5%);
|
||||
animation: bubble-up calc(10s / var(--s)) linear infinite;
|
||||
animation-delay: calc(var(--i) * 0.4s);
|
||||
}
|
||||
|
||||
@keyframes bubble-up {
|
||||
0% {
|
||||
transform: translateY(0) scale(1) translateX(0);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
90% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-600px) scale(1.2) translateX(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
max-width: 600px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
background: rgba(0, 210, 255, 0.12);
|
||||
border: 1px solid rgba(0, 210, 255, 0.3);
|
||||
border-radius: 99px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(2.2rem, 5vw, 3.4rem);
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.hero-title .highlight {
|
||||
background: linear-gradient(to right, #00d2ff, #0066ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.6;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 28px;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
box-shadow: 0 5px 15px rgba(0, 210, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 210, 255, 0.5);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Stats Section */
|
||||
.stats-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 10px 0;
|
||||
background: linear-gradient(to right, #ffffff, #94a3b8);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: rgba(6, 17, 38, 0.4);
|
||||
border: 1px solid rgba(0, 210, 255, 0.08);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: rgba(0, 210, 255, 0.25);
|
||||
box-shadow: 0 10px 30px rgba(0, 210, 255, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon-wrapper {
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.donor-icon {
|
||||
background: rgba(0, 210, 255, 0.15);
|
||||
}
|
||||
.fish-icon {
|
||||
background: rgba(0, 102, 255, 0.15);
|
||||
}
|
||||
.waste-icon {
|
||||
background: rgba(255, 107, 107, 0.15);
|
||||
}
|
||||
.area-icon {
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 1rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-progress {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.stat-desc {
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.total-donation-badge {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 16px 30px;
|
||||
background: linear-gradient(135deg, rgba(6, 17, 38, 0.8), rgba(3, 8, 20, 0.8));
|
||||
border: 1px solid rgba(0, 210, 255, 0.2);
|
||||
border-radius: 99px;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.badge-label {
|
||||
font-size: 0.95rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-amount {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 800;
|
||||
color: var(--primary-color);
|
||||
text-shadow: 0 0 10px rgba(0, 210, 255, 0.4);
|
||||
}
|
||||
|
||||
/* CTA Section */
|
||||
.cta-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cta-card {
|
||||
background: radial-gradient(circle at top right, rgba(0, 210, 255, 0.15) 0%, rgba(3, 8, 20, 0.8) 70%);
|
||||
border: 1px solid rgba(0, 210, 255, 0.15);
|
||||
border-radius: 24px;
|
||||
padding: 50px 40px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cta-desc {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.6;
|
||||
color: #94a3b8;
|
||||
max-width: 700px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
padding: 30px 20px;
|
||||
min-height: auto;
|
||||
}
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.total-donation-badge {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
border-radius: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.cta-card {
|
||||
padding: 35px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "bindings"],
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import wails from "@wailsio/runtime/plugins/vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: Number(process.env.WAILS_VITE_PORT) || 9245,
|
||||
strictPort: true,
|
||||
},
|
||||
plugins: [vue(), wails("./bindings")],
|
||||
});
|
||||
Reference in New Issue
Block a user