init commit

This commit is contained in:
2025-05-06 20:44:33 +09:00
commit 91f0d54563
5567 changed files with 948185 additions and 0 deletions

41
frontend/linktree-frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,107 @@
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-729349ddf41de5398f47cbb58672c50d.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-434465ffb43ebea7d68f6a5b59081080.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-d5293ead5395b673d6471aba21ead960.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-e2805e3f432a29638a666eae127e485c.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-75eb2bf6fa8af82d62c61bb4cac79e1d.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-c889da661b7c58b0deecfbe8e821babb.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-ff54c608e77aa45d6d7fe812d5603c24.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-56d4f452a028c443cc47b3fadf4ce597.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-86b6ba3051c727f86b5bebe10ec3d60e.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-94b6e1f3e395174bc9a2ac26a293a78d.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-55e3fc6a5a6463f26fe1663dd67c13dc.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-51893472f68d786b7cec684fd0a2af6d.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@@ -0,0 +1,40 @@
*, *:before, *:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.card {
font-weight: 400;
border: 0;
-webkit-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}
.card {
position: relative;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.25rem;
}
.card-body {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
min-height: 1px;
padding: 1.25rem;
}
input, input:focus, input:active {
outline: none !important;
-moz-outline-style: none !important;
box-shadow: none !important;
}

View File

@@ -0,0 +1,80 @@
body {
font-family: 'Varela Round', sans-serif;
}
.modal-login {
color: #636363;
width: 350px;
}
.modal-login .modal-content {
padding: 20px;
border-radius: 5px;
border: none;
}
.modal-login .modal-header {
border-bottom: none;
position: relative;
justify-content: center;
}
.modal-login h4 {
text-align: center;
font-size: 26px;
}
.modal-login .form-group {
position: relative;
}
.modal-login i {
position: absolute;
left: 13px;
top: 11px;
font-size: 18px;
}
.modal-login .form-control {
padding-left: 40px;
}
.modal-login .form-control:focus {
border-color: #00ce81;
}
.modal-login .form-control, .modal-login .btn {
min-height: 40px;
border-radius: 3px;
}
.modal-login .close {
position: absolute;
top: -5px;
right: -5px;
}
.modal-login .btn, .modal-login .btn:active {
border: none;
background: #00ce81 !important;
line-height: normal;
}
.modal-login .btn:hover, .modal-login .btn:focus {
background: #00bf78 !important;
}
.modal-login .modal-footer {
background: #ecf0f1;
border-color: #dee4e7;
text-align: center;
margin: 0 -20px -20px;
border-radius: 5px;
font-size: 13px;
justify-content: center;
}
.modal-login .modal-footer a {
color: #999;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1,19 @@
if (window.innerWidth < 768) {
[].slice.call(document.querySelectorAll('[data-bss-disabled-mobile]')).forEach(function (elem) {
elem.classList.remove('animated');
elem.removeAttribute('data-bss-hover-animate');
elem.removeAttribute('data-aos');
elem.removeAttribute('data-bss-parallax-bg');
elem.removeAttribute('data-bss-scroll-zoom');
});
}
document.addEventListener('DOMContentLoaded', function() {
var hoverAnimationTriggerList = [].slice.call(document.querySelectorAll('[data-bss-hover-animate]'));
var hoverAnimationList = hoverAnimationTriggerList.forEach(function (hoverAnimationEl) {
hoverAnimationEl.addEventListener('mouseenter', function(e){ e.target.classList.add('animated', e.target.dataset.bssHoverAnimate) });
hoverAnimationEl.addEventListener('mouseleave', function(e){ e.target.classList.remove('animated', e.target.dataset.bssHoverAnimate) });
});
}, false);

View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Home - Brand</title>
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/apple-touch-icon.png?h=0f5e29c1169e75a7003e818478b67caa">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/apple-touch-icon.png?h=0f5e29c1169e75a7003e818478b67caa">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/web-app-manifest-192x192.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/web-app-manifest-512x512.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css?h=608a9825a1f76f674715160908e57785">
<link rel="manifest" href="/manifest.json?h=457941ffad3c027c946331c09a4d7d2f" crossorigin="use-credentials">
<link rel="stylesheet" href="/assets/css/Lato.css?h=46b2e143124c2518691befa61c089d66">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="/assets/css/Ludens-basic-login.css?h=de6e6f3af82d797c9eae818187ec86d4">
<link rel="stylesheet" href="/assets/css/Modal-Login-form.css?h=8b10c467dd7a5107e2952123e8397d0b">
</head>
<body>
<section class="showcase">
<!-- Start: 1 Row 2 Columns -->
<div class="container">
<div class="row">
<div class="col-md-6 col-xl-2"><img src="/assets/img/bg-showcase-1.jpg?h=717dfd74ae2c9ffe2373428a05a3f602" width="143" height="94"></div>
<div class="col-md-6 col-xl-9"><label class="col-form-label font-monospace fs-6 text-reset text-center text-primary text-opacity-75 border rounded-circle">Iphone</label></div>
</div>
</div><!-- End: 1 Row 2 Columns -->
</section>
<footer class="bg-light footer">
<div class="container">
<div class="row">
<div class="col-lg-6 text-center text-lg-start my-auto h-100">
<ul class="list-inline mb-2">
<li class="list-inline-item"><a href="#">About</a></li>
<li class="list-inline-item"><span></span></li>
<li class="list-inline-item"><a href="#">Contact</a></li>
<li class="list-inline-item"><span></span></li>
<li class="list-inline-item"><a href="#">Terms of &nbsp;Use</a></li>
<li class="list-inline-item"><span></span></li>
<li class="list-inline-item"><a href="#">Privacy Policy</a></li>
</ul>
<p class="text-muted small mb-4 mb-lg-0">© Brand 2025. All Rights Reserved.</p>
</div>
<div class="col-lg-6 text-center text-lg-end my-auto h-100">
<ul class="list-inline mb-0">
<li class="list-inline-item"><a href="#"><i class="fa fa-facebook fa-2x fa-fw"></i></a></li>
<li class="list-inline-item"><a href="#"><i class="fa fa-twitter fa-2x fa-fw"></i></a></li>
<li class="list-inline-item"><a href="#"><i class="fa fa-instagram fa-2x fa-fw"></i></a></li>
</ul>
</div>
</div>
</div>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/bs-init.js?h=ec5d4df3c798a2943b2ecbac76ebfde0"></script>
</body>
</html>

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Home - Brand</title>
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/apple-touch-icon.png?h=0f5e29c1169e75a7003e818478b67caa">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/apple-touch-icon.png?h=0f5e29c1169e75a7003e818478b67caa">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/web-app-manifest-192x192.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/web-app-manifest-512x512.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css?h=608a9825a1f76f674715160908e57785">
<link rel="manifest" href="/manifest.json?h=457941ffad3c027c946331c09a4d7d2f" crossorigin="use-credentials">
<link rel="stylesheet" href="/assets/css/Lato.css?h=46b2e143124c2518691befa61c089d66">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/simple-line-icons/2.5.5/css/simple-line-icons.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css">
<link rel="stylesheet" href="/assets/css/Ludens-basic-login.css?h=de6e6f3af82d797c9eae818187ec86d4">
<link rel="stylesheet" href="/assets/css/Modal-Login-form.css?h=8b10c467dd7a5107e2952123e8397d0b">
</head>
<body>
<nav class="navbar navbar-expand bg-light">
<div class="container"><a class="navbar-brand" href="#"><img src="/assets/img/CAT.png?h=c38a6c0cbff3db2cee57966787d8189b" width="89" height="89">CatLink</a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-1"></button>
<div class="collapse navbar-collapse" id="navcol-1"><a class="btn btn-primary ms-auto" role="button" href="#">Вход</a></div>
</div>
</nav>
<header class="text-center text-white masthead" style="background:url('/assets/img/bg-masthead.jpg?h=3d56ee9570bd6ab1d22f0827b18a0a99')no-repeat center center;background-size:cover;">
<div class="overlay"></div>
<div class="container">
<div class="row">
<div class="col-xl-9 mx-auto position-relative">
<h1 class="mb-5">Ваши ссылки. Ваш стиль. Ваш CatLink.</h1>
</div>
<div class="col-md-10 col-lg-8 col-xl-7 mx-auto position-relative">
<form>
<div class="row">
<div class="col-12 col-md-9 mb-2 mb-md-0"><input class="form-control form-control-lg" type="email" placeholder="Введите электронную почту"></div>
<div class="col-12 col-md-3 col-xl-1"><button class="btn btn-primary btn-lg" type="submit">Начать</button></div>
</div>
</form>
</div>
</div>
</div>
</header>
<section class="text-center bg-light features-icons">
<div class="container">
<div class="row">
<div class="col-lg-4">
<div class="mx-auto features-icons-item mb-5 mb-lg-0 mb-lg-3">
<div class="d-flex features-icons-icon"><i class="icon-link text-primary m-auto" data-bss-hover-animate="pulse"></i></div>
<h3>Публикация</h3>
<p class="lead mb-0">Делитесь единой ссылкой catlinks.kr/ваше-имя в био, мессенджерах и письмах.</p>
</div>
</div>
<div class="col-lg-4">
<div class="mx-auto features-icons-item mb-5 mb-lg-0 mb-lg-3">
<div class="d-flex features-icons-icon"><i class="icon-question m-auto text-primary" data-bss-hover-animate="pulse"></i></div>
<h3>Почему CatLink?</h3>
<p class="lead mb-0">повяжите свои миры одной «хвостовой» ссылкой.</p>
</div>
</div>
<div class="col-lg-4">
<div class="mx-auto features-icons-item mb-5 mb-lg-0 mb-lg-3">
<div class="d-flex features-icons-icon"><i class="icon-social-instagram m-auto text-primary" data-bss-hover-animate="pulse"></i></div>
<h3>Разместите всё важное на одной ссылке</h3>
<p class="lead mb-0">и идите дальше, как кошка: легко и грациозно.</p>
</div>
</div>
</div>
</div>
</section>
<section class="showcase">
<div class="container-fluid p-0">
<div class="row g-0">
<div class="col-lg-6 text-white order-lg-2 showcase-img" style="background-image:url(&quot;/assets/img/bg-showcase-1.jpg?h=717dfd74ae2c9ffe2373428a05a3f602&quot;);"><span></span></div>
<div class="col-lg-6 my-auto order-lg-1 showcase-text">
<h2>Fully Responsive Design</h2>
<p class="lead mb-0">When you use a theme created with Bootstrap, you know that the theme will look great on any device, whether it's a phone, tablet, or desktop the page will behave responsively!</p>
</div>
</div>
<div class="row g-0">
<div class="col-lg-6 text-white showcase-img" style="background-image:url(&quot;/assets/img/bg-showcase-2.jpg?h=82f59ff9dc7ce5bb277d6dfa737a6e45&quot;);"><span></span></div>
<div class="col-lg-6 my-auto order-lg-1 showcase-text">
<h2>Updated For Bootstrap 5</h2>
<p class="lead mb-0">Newly improved, and full of great utility classes, Bootstrap 5 is leading the way in mobile responsive web development! All of the themes are now using Bootstrap 5!</p>
</div>
</div>
<div class="row g-0">
<div class="col-lg-6 text-white order-lg-2 showcase-img" style="background-image:url(&quot;/assets/img/bg-showcase-3.jpg?h=c7ec0329b8412e48f1b91e5c6a8cc7cf&quot;);"><span></span></div>
<div class="col-lg-6 my-auto order-lg-1 showcase-text">
<h2>Easy to Use &amp;&nbsp;Customize</h2>
<p class="lead mb-0">Landing Page is just HTML and CSS with a splash of SCSS for users who demand some deeper customization options. Out of the box, just add your content and images, and your new landing page will be ready to go!</p>
</div>
</div>
</div>
</section>
<section class="text-center bg-light testimonials">
<div class="container">
<h2 class="mb-5">What people are saying...</h2>
<div class="row">
<div class="col-lg-4">
<div class="mx-auto testimonial-item mb-5 mb-lg-0"><img class="rounded-circle img-fluid mb-3" src="/assets/img/testimonials-1.jpg?h=c9a15635305654b24ce5a3055e22f73e">
<h5>Margaret E.</h5>
<p class="fw-light mb-0">"This is fantastic! Thanks so much guys!"</p>
</div>
</div>
<div class="col-lg-4">
<div class="mx-auto testimonial-item mb-5 mb-lg-0"><img class="rounded-circle img-fluid mb-3" src="/assets/img/testimonials-2.jpg?h=2f7c16e307b7da2bdf38d580d9a3fed9">
<h5>Fred S.</h5>
<p class="fw-light mb-0">"Bootstrap is amazing. I've been using it to create lots of super nice landing pages."</p>
</div>
</div>
<div class="col-lg-4">
<div class="mx-auto testimonial-item mb-5 mb-lg-0"><img class="rounded-circle img-fluid mb-3" src="/assets/img/testimonials-3.jpg?h=39503ac082e01a410b496ed9ce0df8e6">
<h5>Sarah W.</h5>
<p class="fw-light mb-0">"Thanks so much for making these free resources available to us!"</p>
</div>
</div>
</div>
</div>
</section>
<section class="text-center text-white call-to-action" style="background:url(&quot;/assets/img/bg-masthead.jpg?h=3d56ee9570bd6ab1d22f0827b18a0a99&quot;) no-repeat center center;background-size:cover;">
<div class="overlay"></div>
<div class="container">
<div class="row">
<div class="col-xl-9 mx-auto position-relative">
<h2 class="mb-4">Ready to get started? Sign up now!</h2>
</div>
<div class="col-md-10 col-lg-8 col-xl-7 mx-auto position-relative">
<form>
<div class="row">
<div class="col-12 col-md-9 mb-2 mb-md-0"><input class="form-control form-control-lg" type="email" placeholder="Enter your email..."></div>
<div class="col-12 col-md-3"><button class="btn btn-primary btn-lg" type="submit">Sign up!</button></div>
</div>
</form>
</div>
</div>
</div>
</section>
<footer class="bg-light footer">
<div class="container">
<div class="row">
<div class="col-lg-6 text-center text-lg-start my-auto h-100">
<ul class="list-inline mb-2">
<li class="list-inline-item"><a href="#">About</a></li>
<li class="list-inline-item"><span></span></li>
<li class="list-inline-item"><a href="#">Contact</a></li>
<li class="list-inline-item"><span></span></li>
<li class="list-inline-item"><a href="#">Terms of &nbsp;Use</a></li>
<li class="list-inline-item"><span></span></li>
<li class="list-inline-item"><a href="#">Privacy Policy</a></li>
</ul>
<p class="text-muted small mb-4 mb-lg-0">© Brand 2025. All Rights Reserved.</p>
</div>
<div class="col-lg-6 text-center text-lg-end my-auto h-100">
<ul class="list-inline mb-0">
<li class="list-inline-item"><a href="#"><i class="fa fa-facebook fa-2x fa-fw"></i></a></li>
<li class="list-inline-item"><a href="#"><i class="fa fa-twitter fa-2x fa-fw"></i></a></li>
<li class="list-inline-item"><a href="#"><i class="fa fa-instagram fa-2x fa-fw"></i></a></li>
</ul>
</div>
</div>
</div>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/bs-init.js?h=ec5d4df3c798a2943b2ecbac76ebfde0"></script>
</body>
</html>

View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>links</title>
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/apple-touch-icon.png?h=0f5e29c1169e75a7003e818478b67caa">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="icon" type="image/png" sizes="96x96" href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/apple-touch-icon.png?h=0f5e29c1169e75a7003e818478b67caa">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/web-app-manifest-192x192.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/web-app-manifest-512x512.png?h=c8792ce927e01a494c4af48bed5dc5e3">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css?h=608a9825a1f76f674715160908e57785">
<link rel="manifest" href="/manifest.json?h=457941ffad3c027c946331c09a4d7d2f" crossorigin="use-credentials">
<link rel="stylesheet" href="/assets/css/Lato.css?h=46b2e143124c2518691befa61c089d66">
<link rel="stylesheet" href="/assets/css/Ludens-basic-login.css?h=de6e6f3af82d797c9eae818187ec86d4">
<link rel="stylesheet" href="/assets/css/Modal-Login-form.css?h=8b10c467dd7a5107e2952123e8397d0b">
</head>
<body>
<!-- Start: Ludens basic login -->
<div class="d-flex d-xl-flex align-items-center align-items-xl-center" style="width: 100%;height: 100%;">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-9 col-lg-12 col-xl-10">
<div class="card shadow-lg o-hidden border-0 my-5">
<div class="card-body p-0">
<div class="row">
<div class="col-lg-6 d-none d-lg-flex">
<div class="flex-grow-1 bg-login-image" style="background-image: url(&quot;/assets/img/durvill_logo.jpg?h=8f459951278c6136008a828c282ceb97&quot;);"></div>
</div>
<div class="col-lg-6">
<div class="p-5">
<div class="text-center">
<h4 class="text-dark mb-4">Welcome back!</h4>
</div>
<form class="user">
<div class="mb-3"><input class="form-control form-control-user" type="email" id="exampleInputEmail" aria-describedby="emailHelp" placeholder="Enter Email Address..." name="email"></div>
<div class="mb-3"><input class="form-control form-control-user" type="password" id="exampleInputPassword" placeholder="Password" name="password"></div>
<div class="mb-3">
<div class="custom-control custom-checkbox small"></div>
</div><button class="btn btn-primary d-block btn-user w-100" type="submit" style="background: #01703E;">Login</button>
<hr>
<hr>
</form>
<div class="text-center"><a class="small" href="forgot-password.html" style="color: #01703E;">Forgot Password?</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- End: Ludens basic login -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/bs-init.js?h=ec5d4df3c798a2943b2ecbac76ebfde0"></script>
</body>
</html>

View File

@@ -0,0 +1 @@
{"short_name":"CatLink","name":"CatLink","icons":[{"src":"/assets/img/web-app-manifest-192x192.png?h=c8792ce927e01a494c4af48bed5dc5e3","type":"image/png","sizes":"192x192"},{"src":"/assets/img/web-app-manifest-512x512.png?h=c8792ce927e01a494c4af48bed5dc5e3","type":"image/png","sizes":"512x512"}],"start_url":"/","display":"fullscreen"}

View File

@@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View File

@@ -0,0 +1,34 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'http',
hostname: '127.0.0.1',
port: '8000', // <-- обязательно 8000, где Django отдаёт медиа
pathname: '/storage/**', // <-- подпапкиstorage/avatars, images/link_groups и т.д.
},
],
},
// Разрешаем в деве обращения с вашего адреса
allowedDevOrigins: [
'http://localhost:3001',
'http://192.168.219.114:3001',
'http://0.0.0.0:3001',
'http://localhost:3000',
'http://192.168.219.114:3000',
],
// Проксируем все запросы /api/* на Django
async rewrites() {
return [
{
source: '/api/:path*', // локальный путь на фронте
destination: 'http://127.0.0.1:8000/api/:path*/' // куда реально уходит запрос
}
]
},
};
module.exports = nextConfig;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
{
"name": "linktree-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"axios": "^1.9.0",
"next": "15.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.1.5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.21",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.5",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,7 @@
// postcss.config.js
module.exports = {
plugins: {
'@tailwindcss/postcss': {}, // новый пакет-плагин для Tailwind v4+
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,107 @@
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-729349ddf41de5398f47cbb58672c50d.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-434465ffb43ebea7d68f6a5b59081080.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-d5293ead5395b673d6471aba21ead960.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-e2805e3f432a29638a666eae127e485c.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-75eb2bf6fa8af82d62c61bb4cac79e1d.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-c889da661b7c58b0deecfbe8e821babb.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-ff54c608e77aa45d6d7fe812d5603c24.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-56d4f452a028c443cc47b3fadf4ce597.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-86b6ba3051c727f86b5bebe10ec3d60e.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-94b6e1f3e395174bc9a2ac26a293a78d.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-55e3fc6a5a6463f26fe1663dd67c13dc.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'Lato';
src: url(/assets/fonts/Lato-51893472f68d786b7cec684fd0a2af6d.woff2?h=46b2e143124c2518691befa61c089d66) format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@@ -0,0 +1,40 @@
*, *:before, *:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.card {
font-weight: 400;
border: 0;
-webkit-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}
.card {
position: relative;
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.25rem;
}
.card-body {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
min-height: 1px;
padding: 1.25rem;
}
input, input:focus, input:active {
outline: none !important;
-moz-outline-style: none !important;
box-shadow: none !important;
}

View File

@@ -0,0 +1,80 @@
body {
font-family: 'Varela Round', sans-serif;
}
.modal-login {
color: #636363;
width: 350px;
}
.modal-login .modal-content {
padding: 20px;
border-radius: 5px;
border: none;
}
.modal-login .modal-header {
border-bottom: none;
position: relative;
justify-content: center;
}
.modal-login h4 {
text-align: center;
font-size: 26px;
}
.modal-login .form-group {
position: relative;
}
.modal-login i {
position: absolute;
left: 13px;
top: 11px;
font-size: 18px;
}
.modal-login .form-control {
padding-left: 40px;
}
.modal-login .form-control:focus {
border-color: #00ce81;
}
.modal-login .form-control, .modal-login .btn {
min-height: 40px;
border-radius: 3px;
}
.modal-login .close {
position: absolute;
top: -5px;
right: -5px;
}
.modal-login .btn, .modal-login .btn:active {
border: none;
background: #00ce81 !important;
line-height: normal;
}
.modal-login .btn:hover, .modal-login .btn:focus {
background: #00bf78 !important;
}
.modal-login .modal-footer {
background: #ecf0f1;
border-color: #dee4e7;
text-align: center;
margin: 0 -20px -20px;
border-radius: 5px;
font-size: 13px;
justify-content: center;
}
.modal-login .modal-footer a {
color: #999;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1,19 @@
if (window.innerWidth < 768) {
[].slice.call(document.querySelectorAll('[data-bss-disabled-mobile]')).forEach(function (elem) {
elem.classList.remove('animated');
elem.removeAttribute('data-bss-hover-animate');
elem.removeAttribute('data-aos');
elem.removeAttribute('data-bss-parallax-bg');
elem.removeAttribute('data-bss-scroll-zoom');
});
}
document.addEventListener('DOMContentLoaded', function() {
var hoverAnimationTriggerList = [].slice.call(document.querySelectorAll('[data-bss-hover-animate]'));
var hoverAnimationList = hoverAnimationTriggerList.forEach(function (hoverAnimationEl) {
hoverAnimationEl.addEventListener('mouseenter', function(e){ e.target.classList.add('animated', e.target.dataset.bssHoverAnimate) });
hoverAnimationEl.addEventListener('mouseleave', function(e){ e.target.classList.remove('animated', e.target.dataset.bssHoverAnimate) });
});
}, false);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,21 @@
{
"name": "CatLink",
"short_name": "CatLink",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,95 @@
// src/app/auth/login/page.tsx
'use client'
import { useState, useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { useRouter } from 'next/navigation'
type FormData = { username: string; password: string }
export default function LoginPage() {
const router = useRouter()
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>()
const [apiError, setApiError] = useState<string | null>(null)
useEffect(() => {
if (typeof window !== 'undefined' && localStorage.getItem('token')) {
router.push('/dashboard')
}
}, [router])
async function onSubmit(data: FormData) {
setApiError(null)
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/auth/login/`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}
)
if (!res.ok) {
const json = await res.json()
setApiError(json.detail || 'Ошибка входа')
return
}
const { access } = await res.json()
localStorage.setItem('token', access)
router.push('/dashboard')
} catch {
setApiError('Сетевая ошибка')
}
}
return (
<div className="d-flex align-items-center justify-content-center" style={{ height: '100vh' }}>
<div className="card shadow-lg border-0" style={{ maxWidth: 800, width: '100%' }}>
<div className="row g-0">
<div className="col-lg-6 d-none d-lg-flex" style={{
backgroundImage: "url('/assets/img/durvill_logo.jpg')",
backgroundSize: 'cover',
backgroundPosition: 'center'
}} />
<div className="col-lg-6">
<div className="p-5">
<h4 className="text-center mb-4">Welcome back!</h4>
{apiError && <p className="text-danger text-center">{apiError}</p>}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-3">
<input
type="text"
placeholder="Enter Username..."
className={`form-control ${errors.username ? 'is-invalid' : ''}`}
{...register('username', { required: 'Введите имя пользователя' })}
/>
{errors.username && <div className="invalid-feedback">{errors.username.message}</div>}
</div>
<div className="mb-3">
<input
type="password"
placeholder="Password"
className={`form-control ${errors.password ? 'is-invalid' : ''}`}
{...register('password', { required: 'Введите пароль' })}
/>
{errors.password && <div className="invalid-feedback">{errors.password.message}</div>}
</div>
<button
type="submit"
disabled={isSubmitting}
className="btn btn-primary w-100"
style={{ background: '#01703E' }}
>
{isSubmitting ? 'Вхожу...' : 'Login'}
</button>
</form>
<div className="text-center mt-3">
<a href="#" className="small text-decoration-none">Forgot Password?</a>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,80 @@
// src/components/ProfileCard.tsx
'use client'
import Image from 'next/image'
import { format } from 'date-fns'
import ru from 'date-fns/locale/ru'
interface ProfileCardProps {
avatar: string // API теперь отдаёт что-то вроде "frontend/assets/img/avatars/3.png"
full_name: string
email: string
bio?: string
last_login: string
date_joined: string
}
export function ProfileCard({
avatar,
full_name,
email,
bio,
last_login,
date_joined,
}: ProfileCardProps) {
// Если API отдаёт относительный путь без /media/, добавляем префикс:
const avatarSrc = avatar.startsWith('http')
? avatar
: `${process.env.NEXT_PUBLIC_API_URL}/media/${avatar}`
const fmt = (iso: string) => {
try {
return format(new Date(iso), 'dd.MM.yyyy HH:mm', { locale: ru })
} catch {
return iso
}
}
return (
<div
className="card shadow rounded mx-auto my-4"
style={{ maxWidth: 600 }}
>
<div className="card-body text-center">
{/* Avatar */}
<div className="mb-3">
<Image
className="rounded-circle border border-white"
src={avatarSrc}
alt="Avatar"
width={150}
height={150}
/>
</div>
{/* Full Name */}
<h3 className="mb-1">{full_name || '—'}</h3>
{/* Email */}
<p className="text-muted mb-3">{email}</p>
{/* Bio */}
<p className="mb-4">
{bio && bio.trim() ? bio : 'Описание профиля отсутствует.'}
</p>
{/* Даты регистрации и последнего входа */}
<div className="d-flex justify-content-around">
<div className="text-start">
<p className="mb-1 small text-uppercase">Зарегистрирован</p>
<p className="mb-0">{fmt(date_joined)}</p>
</div>
<div className="text-end">
<p className="mb-1 small text-uppercase">Последний вход</p>
<p className="mb-0">{fmt(last_login)}</p>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import Link from 'next/link'
export function Footer() {
return (
<footer className="bg-light footer py-5 border-top">
<div className="container">
<div className="row">
<div className="col-lg-6 text-center text-lg-start mb-3 mb-lg-0">
<ul className="list-inline mb-2">
<li className="list-inline-item"><Link href="#">About</Link></li>
<li className="list-inline-item"><span></span></li>
<li className="list-inline-item"><Link href="#">Contact</Link></li>
<li className="list-inline-item"><span></span></li>
<li className="list-inline-item"><Link href="#">Terms of Use</Link></li>
<li className="list-inline-item"><span></span></li>
<li className="list-inline-item"><Link href="#">Privacy Policy</Link></li>
</ul>
<p className="text-muted small mb-0">© CatLink 2025. Все права защищены.</p>
</div>
<div className="col-lg-6 text-center text-lg-end">
<ul className="list-inline mb-0">
<li className="list-inline-item"><Link href="#"><i className="fa fa-facebook fa-2x fa-fw" /></Link></li>
<li className="list-inline-item"><Link href="#"><i className="fa fa-twitter fa-2x fa-fw" /></Link></li>
<li className="list-inline-item"><Link href="#"><i className="fa fa-instagram fa-2x fa-fw" /></Link></li>
</ul>
</div>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,72 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import Image from 'next/image'
interface UserProfile {
username: string
first_name?: string
avatarUrl?: string
}
export function Header() {
const [user, setUser] = useState<UserProfile | null>(null)
const router = useRouter()
useEffect(() => {
const token = localStorage.getItem('token')
if (!token) {
return
}
fetch('/api/auth/user/', {
headers: { Authorization: `Bearer ${token}` },
})
.then(res => {
if (!res.ok) throw new Error('unauth')
return res.json()
})
.then(setUser)
.catch(() => {
localStorage.removeItem('token')
router.push('/auth/login')
})
}, [router])
const logout = () => {
localStorage.removeItem('token')
router.push('/')
}
return (
<nav className="navbar navbar-expand bg-light fixed-top">
<div className="container">
<Link href="/" className="navbar-brand">
<Image src="/assets/img/CAT.png" width={89} height={89} alt="CatLink"/>
</Link>
<div className="collapse navbar-collapse">
{user ? (
<div className="ms-auto d-flex align-items-center">
<Link href="/dashboard" className="me-3 d-flex align-items-center">
<Image
src={user.avatarUrl || '/assets/img/avatar-dhg.png'}
width={32}
height={32}
className="rounded-circle"
alt="avatar"
/>
<span className="ms-2">{user.first_name || user.username}</span>
</Link>
<button className="btn btn-outline-danger" onClick={logout}>
Выход
</button>
</div>
) : (
<Link href="/auth/login" className="btn btn-primary ms-auto">
Вход
</Link>
)}
</div>
</div>
</nav>
)
}

View File

@@ -0,0 +1,199 @@
// src/app/dashboard/page.tsx
'use client'
import React, { useEffect, useState, Fragment } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { ProfileCard } from '../components/ProfileCard'
interface UserProfile {
avatar: string
full_name: string
email: string
bio?: string
last_login: string
date_joined: string
}
interface LinkItem {
id: number
title: string
url: string
}
interface Group {
id: number
name: string
links?: LinkItem[]
}
export default function DashboardPage() {
const router = useRouter()
const [user, setUser] = useState<UserProfile | null>(null)
const [groups, setGroups] = useState<Group[]>([])
const [expandedGroup, setExpandedGroup] = useState<number | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const token = localStorage.getItem('token')
if (!token) {
router.push('/auth/login')
return
}
const fetchData = async () => {
try {
// 1) Профиль
const uRes = await fetch(`/api/auth/user/`, {
headers: { Authorization: `Bearer ${token}` },
})
if (uRes.status === 401) {
localStorage.removeItem('token')
router.push('/auth/login')
return
}
if (!uRes.ok) throw new Error('Не удалось получить профиль')
const userData: UserProfile = await uRes.json()
// 2) Группы
const gRes = await fetch(`/api/groups/`, {
headers: { Authorization: `Bearer ${token}` },
})
if (gRes.status === 401) {
localStorage.removeItem('token')
router.push('/auth/login')
return
}
if (!gRes.ok) throw new Error('Не удалось загрузить группы')
const groupsData: Group[] = await gRes.json()
setUser(userData)
setGroups(groupsData)
} catch (err) {
// на любую ошибку — редирект на логин
console.error(err)
localStorage.removeItem('token')
router.push('/auth/login')
} finally {
setLoading(false)
}
}
fetchData()
}, [router])
if (loading) {
return <div className="flex items-center justify-center h-screen">Загрузка...</div>
}
return (
<div className="pb-8">
<div className="container">
<div className="row justify-content-center">
<div className="col-xl-10 col-xxl-9">
{/* Профиль пользователя */}
{user && (
<ProfileCard
avatar={user.avatar}
full_name={user.full_name}
email={user.email}
bio={user.bio}
last_login={user.last_login}
date_joined={user.date_joined}
/>
)}
{/* Секция групп ссылок */}
<div className="card shadow mt-5">
<div className="card-header d-flex justify-content-between align-items-center">
<h5 className="mb-0">Группы ссылок</h5>
<button
className="btn btn-sm btn-success"
onClick={() => {
/* TODO: открыть модальное окно добавления группы */
}}
>
<i className="bi bi-plus-lg"></i> Добавить группу
</button>
</div>
<div className="list-group list-group-flush">
{groups.map((group) => (
<Fragment key={group.id}>
<div className="list-group-item d-flex justify-content-between align-items-center">
<span
style={{ cursor: 'pointer' }}
onClick={() =>
setExpandedGroup(
expandedGroup === group.id ? null : group.id
)
}
>
{group.name}{' '}
<span className="badge bg-secondary rounded-pill">
{group.links?.length ?? 0}
</span>
</span>
<div className="btn-group btn-group-sm">
<button className="btn btn-outline-primary">
<i className="bi bi-link-45deg"></i>
</button>
<button className="btn btn-outline-secondary">
<i className="bi bi-pencil"></i>
</button>
<button className="btn btn-outline-danger">
<i className="bi bi-trash"></i>
</button>
</div>
</div>
{expandedGroup === group.id && (
<div className="list-group-item bg-light">
<ul className="mb-0 ps-3">
{group.links?.map((link) => (
<li
key={link.id}
className="d-flex justify-content-between align-items-center mb-2"
>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex-grow-1"
>
{link.title}
</a>
<div className="btn-group btn-group-sm">
<button className="btn btn-outline-secondary">
<i className="bi bi-pencil-fill"></i>
</button>
<button className="btn btn-outline-danger">
<i className="bi bi-trash-fill"></i>
</button>
</div>
</li>
))}
</ul>
</div>
)}
</Fragment>
))}
</div>
<div className="card-footer">
<nav>
<ul className="pagination pagination-sm justify-content-center mb-0">
{[1, 2, 3].map((p) => (
<li key={p} className="page-item">
<Link href={`?page=${p}`} className="page-link">
{p}
</Link>
</li>
))}
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,173 @@
@import "tailwindcss";
/* src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
/* дальше — ваши кастом-стили */
@layer base {
h1 {
@apply text-3xl font-bold;
}
h2 {
@apply text-2xl font-bold;
}
h3 {
@apply text-xl font-bold;
}
p {
@apply text-base;
}
}
@layer components {
.btn {
@apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
.btn:hover {
@apply bg-blue-700;
}
}
@layer utilities {
.bg-custom {
@apply bg-blue-100;
}
.text-custom {
@apply text-blue-500;
}
}
@layer screens {
@screen sm {
.text-sm {
@apply text-xs;
}
}
@screen md {
.text-md {
@apply text-base;
}
}
@screen lg {
.text-lg {
@apply text-lg;
}
}
}
@layer forms {
.form-input {
@apply border-gray-300 rounded-md shadow-sm;
}
.form-input:focus {
@apply border-blue-500 ring-blue-500;
}
}
@layer typography {
.prose {
@apply prose-lg;
}
.prose h1 {
@apply text-3xl font-bold;
}
.prose h2 {
@apply text-2xl font-bold;
}
.prose h3 {
@apply text-xl font-bold;
}
}
@layer animations {
.fade-in {
@apply transition-opacity duration-500 ease-in;
}
.fade-out {
@apply transition-opacity duration-500 ease-out;
}
}
@layer transitions {
.transition-all {
@apply transition-all duration-300 ease-in-out;
}
.transition-colors {
@apply transition-colors duration-300 ease-in-out;
}
}
@layer shadows {
.shadow-custom {
@apply shadow-lg;
}
.shadow-none {
@apply shadow-none;
}
}
@layer borders {
.border-custom {
@apply border-2 border-blue-500;
}
.border-none {
@apply border-none;
}
}
@layer spacing {
.p-custom {
@apply p-4;
}
.m-custom {
@apply m-4;
}
}
@layer flex {
.flex-center {
@apply flex items-center justify-center;
}
.flex-column {
@apply flex flex-col;
}
}
@layer grid {
.grid-custom {
@apply grid grid-cols-3 gap-4;
}
.grid-center {
@apply grid place-items-center;
}
}
@layer typography {
.prose {
@apply prose-lg;
}
.prose h1 {
@apply text-3xl font-bold;
}
.prose h2 {
@apply text-2xl font-bold;
}
.prose h3 {
@apply text-xl font-bold;
}
}

View File

@@ -0,0 +1,173 @@
// src/app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ReactNode } from "react";
import Script from "next/script";
import Link from "next/link";
import Image from "next/image";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "CatLink",
description: "Ваши ссылки в одном месте",
};
export default function RootLayout({
children,
}: {
children: ReactNode;
}) {
return (
<html lang="ru">
<head>
<meta charSet="utf-8" />
<link
rel="apple-touch-icon"
type="image/png"
sizes="180x180"
href="/assets/img/apple-touch-icon.png?h=0f5e29c1169e75a7003e818478b67caa"
/>
<link
rel="icon"
type="image/png"
sizes="96x96"
href="/assets/img/favicon-96x96.png?h=c8792ce927e01a494c4af48bed5dc5e3"
/>
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/assets/img/web-app-manifest-192x192.png?h=c8792ce927e01a494c4af48bed5dc5e3"
/>
<link
rel="icon"
type="image/png"
sizes="512x512"
href="/assets/img/web-app-manifest-512x512.png?h=c8792ce927e01a494c4af48bed5dc5e3"
/>
<link
rel="stylesheet"
href="/assets/bootstrap/css/bootstrap.min.css?h=608a9825a1f76f674715160908e57785"
/>
<link
rel="stylesheet"
href="/assets/css/Lato.css?h=8253736d3a23b522f64b7e7d96d1d8ff"
/>
<link
rel="manifest"
href="/manifest.json?h=457941ffad3c027c946331c09a4d7d2f"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
/>
</head>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{/* Header (fixed to top) */}
<nav className="navbar navbar-expand bg-light fixed-top shadow-sm">
<div className="container">
<Link href="/" className="navbar-brand d-flex align-items-center">
<Image
src="/assets/img/CAT.png"
alt="CatLink"
width={89}
height={89}
/>
<span className="ms-2">CatLink</span>
</Link>
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navcol-1"
/>
<div className="collapse navbar-collapse" id="navcol-1">
<Link href="/auth/login" className="btn btn-primary ms-auto">
<i className="fa fa-user"></i>
<span className="d-none d-sm-inline"> Вход</span>
</Link>
</div>
</div>
</nav>
{/* Main content: добавляем дополнительный отступ сверху */}
<main
style={{
paddingTop: "100px", // <-- тут увеличиваем интервал
paddingBottom: "200px"
}}
// или через класс bootstrap: className="pt-5 pb-5"
>
{children}
</main>
{/* Footer (fixed to bottom) */}
<footer className="bg-light footer fixed-bottom border-top">
<div className="container py-2">
<div className="row">
<div className="col-lg-6 text-center text-lg-start mb-2 mb-lg-0">
<ul className="list-inline mb-1">
<li className="list-inline-item">
<Link href="#">About</Link>
</li>
<li className="list-inline-item"></li>
<li className="list-inline-item">
<Link href="#">Contact</Link>
</li>
<li className="list-inline-item"></li>
<li className="list-inline-item">
<Link href="#">Terms of Use</Link>
</li>
<li className="list-inline-item"></li>
<li className="list-inline-item">
<Link href="#">Privacy Policy</Link>
</li>
</ul>
<p className="text-muted small mb-0">© CatLink 2025</p>
</div>
<div className="col-lg-6 text-center text-lg-end">
<ul className="list-inline mb-0">
<li className="list-inline-item">
<Link href="#">
<i className="fa fa-facebook fa-lg fa-fw"></i>
</Link>
</li>
<li className="list-inline-item">
<Link href="#">
<i className="fa fa-twitter fa-lg fa-fw"></i>
</Link>
</li>
<li className="list-inline-item">
<Link href="#">
<i className="fa fa-instagram fa-lg fa-fw"></i>
</Link>
</li>
</ul>
</div>
</div>
</div>
</footer>
{/* Scripts */}
<Script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"
strategy="beforeInteractive"
/>
<Script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
strategy="beforeInteractive"
/>
<Script src="/assets/js/bs-init.js" strategy="lazyOnload" />
</body>
</html>
);
}

View File

@@ -0,0 +1,75 @@
// src/app/page.tsx
export default function HomePage() {
return (
<>
<header
className="text-center text-white masthead"
style={{
background: "url('/assets/img/bg-masthead.jpg') no-repeat center center",
backgroundSize: 'cover',
}}
>
<div className="overlay"></div>
<div className="container py-5">
<div className="row">
<div className="col-xl-9 mx-auto">
<h1 className="mb-5">Ваши ссылки. Ваш стиль. Ваш CatLink.</h1>
</div>
<div className="col-md-10 col-lg-8 col-xl-7 mx-auto">
<form className="d-flex">
<input
className="form-control form-control-lg me-2"
type="email"
placeholder="Введите электронную почту"
/>
<button className="btn btn-primary btn-lg" type="submit">
Начать
</button>
</form>
</div>
</div>
</div>
</header>
<section className="text-center bg-light features-icons py-5">
<div className="container">
<div className="row">
<div className="col-lg-4 mb-4">
<div className="features-icons-item">
<div className="features-icons-icon mb-3">
<img src="/assets/img/CAT.png" alt="CatLink" width={89} height={89} />
</div>
<h3>Публикация</h3>
<p className="lead mb-0">
Делитесь единой ссылкой catlinks.kr/ваше-имя в био, мессенджерах и письмах.
</p>
</div>
</div>
<div className="col-lg-4 mb-4">
<div className="features-icons-item">
<div className="features-icons-icon mb-3">
<img src="/assets/img/CAT.png" alt="CatLink" width={89} height={89} />
</div>
<h3>Почему CatLink?</h3>
<p className="lead mb-0">
Повяжите свои миры одной «хвостовой» ссылкой.
</p>
</div>
</div>
<div className="col-lg-4 mb-4">
<div className="features-icons-item">
<div className="features-icons-icon mb-3">
<img src="/assets/img/CAT.png" alt="CatLink" width={89} height={89} />
</div>
<h3>Разместите всё важное на одной ссылке</h3>
<p className="lead mb-0">
Идите дальше, как кошка: легко и грациозно.
</p>
</div>
</div>
</div>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,52 @@
// src/app/[username]/page.tsx
import Image from 'next/image';
import Link from 'next/link';
interface LinkItem {
id: string;
title: string;
url: string;
icon?: string;
}
export default async function UserLinksPage({ params }: { params: { username: string } }) {
const { username } = params;
// Серверный запрос к API. Замените API_URL на ваш бекенд
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/users/${username}/links`,
{ cache: 'no-store' }
);
if (!res.ok) {
return (
<main className="max-w-md mx-auto p-6">
<h1 className="text-2xl font-semibold mb-4">Пользователь не найден</h1>
<p>Попробуйте другой логин.</p>
</main>
);
}
const links: LinkItem[] = await res.json();
return (
<main className="max-w-md mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Каталог ссылок: {username}</h1>
{links.length === 0 ? (
<p>У пользователя пока нет ссылок.</p>
) : (
<ul className="space-y-4">
{links.map((link) => (
<li key={link.id} className="p-4 border rounded hover:shadow">
<Link href={link.url} target="_blank" className="flex items-center space-x-3">
{link.icon ? (
<Image src={link.icon} alt={link.title} width={24} height={24} />
) : (
<span className="w-6 h-6 bg-gray-300 rounded-full" />
)}
<span className="text-lg font-medium">{link.title}</span>
</Link>
</li>
))}
</ul>
)}
</main>
);
}

View File

@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/app/**/*.{js,ts,jsx,tsx}',
'./src/components/**/*.{js,ts,jsx,tsx}',
],
theme: { extend: {} },
plugins: [],
};

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}