first commmit

This commit is contained in:
2026-06-04 09:06:22 +03:00
commit f240b55dda
53 changed files with 6445 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
.env
.env.local
node_modules/
dist/
*.log
.DS_Store
+11
View File
@@ -0,0 +1,11 @@
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o main cmd/mrp/main.go
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/main .
COPY --from=builder /app/ui/dist ./ui/dist
EXPOSE 8080
CMD ["./main"]
+5
View File
@@ -0,0 +1,5 @@
vendor
bin
main
.git
.env
+23
View File
@@ -0,0 +1,23 @@
FROM golang:1.25-alpine AS builder
RUN apk add --no-cache git
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=0 GOOS=linux go build -o main .
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8090
CMD ["./main"]
+24
View File
@@ -0,0 +1,24 @@
package database
import (
"log"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"fishfish/models"
)
var DB *gorm.DB
func InitDB() {
dsn := os.Getenv("DB_DSN")
var err error
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
DB.AutoMigrate(&models.Item{}, &models.User{})
}
+49
View File
@@ -0,0 +1,49 @@
module fishfish
go 1.25.0
require (
github.com/gin-gonic/gin v1.12.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.48.0
gorm.io/driver/postgres v1.5.7
gorm.io/gorm v1.25.10
)
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
+110
View File
@@ -0,0 +1,110 @@
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+90
View File
@@ -0,0 +1,90 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"fishfish/database"
"fishfish/models"
)
func GetItem(c *gin.Context) {
id := c.Param("id")
var item models.Item
if err := database.DB.First(&item, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Товар не найден"})
return
}
c.JSON(http.StatusOK, item)
}
func GetAllItems(c *gin.Context) {
var items []models.Item
if err := database.DB.Find(&items).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось загрузить данные с сервера", "message": err.Error()})
return
}
c.JSON(http.StatusOK, items)
}
func UpdateItem(c *gin.Context) {
id := c.Param("id")
var item models.Item
if err := database.DB.First(&item, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Товар не найден"})
return
}
var input models.Item
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Неверно введеные данные"})
return
}
database.DB.Model(&item).Updates(models.Item{
Name: input.Name,
Unit: input.Unit,
City: input.City,
Price: input.Price,
})
c.JSON(http.StatusOK, item)
}
func AddItem(c *gin.Context) {
var input models.Item
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Неверные данные"})
return
}
if err := database.DB.Create(&input).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось создать товар"})
return
}
c.JSON(http.StatusCreated, input)
}
func DeleteItem(c *gin.Context) {
id := c.Param("id")
var item models.Item
if err := database.DB.First(&item, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Указанный товар не найден"})
return
}
database.DB.Delete(&item)
c.JSON(http.StatusOK, gin.H{"message": "Товар успешно удален"})
}
+125
View File
@@ -0,0 +1,125 @@
package handlers
import (
"fmt"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"fishfish/database"
"fishfish/models"
)
func Login(c *gin.Context) {
jwtKey := []byte(os.Getenv("JWT_SECRET"))
var input struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Неверный ввод"})
return
}
var user models.User
err := database.DB.Where("username = ?", input.Username).First(&user).Error
if err != nil {
fmt.Printf("[LOGIN ERROR] Пользователь %s не найден в БД: Ошибка")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Неверный логин или пароль"})
}
passErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password))
if passErr != nil {
fmt.Printf("[LOGIN ERROR] Пароль для пароля %s не совпал с хешем в базе данных")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Неверный логин или пароль"})
return
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.ID,
"role": user.Role,
"exp": time.Now().Add(time.Hour * 24).Unix(),
})
tokenString, _ := token.SignedString(jwtKey)
c.JSON(http.StatusOK, gin.H{"token": tokenString, "user": gin.H{
"id": user.ID,
"first_name": user.FirstName,
"last_name": user.LastName,
"phone": user.Phone,
"role": user.Role,
}})
}
func Register(c *gin.Context) {
var input struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,min=8"`
FirstName string `json:"first_name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
Phone string `json:"phone"`
Role string `json:"role" binding:"required"`
}
// 1. Проверяем входящие данные
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Ошибка валидации ", "details": err.Error()})
return
}
// 2. Хешируем пароль (чтобы не хранить его в открытом виде)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при шифровании пароля"})
return
}
// 3. Создаем объект пользователя
user := models.User{
Username: input.Username,
Password: string(hashedPassword),
FirstName: input.FirstName,
LastName: input.LastName,
Phone: input.Phone,
Role: input.Role, // по умолчанию
}
// 4. Сохраняем в базу через GORM
if err := database.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка при регистрации пользователя", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Пользователь успешно зарегистрирован"})
}
func UpdateProfile(c *gin.Context) {
var input models.UpdateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID := c.MustGet("user_id")
if err := database.DB.Model(&models.User{}).Where("id = ?", userID).Updates(models.User{
FirstName: input.FirstName,
LastName: input.LastName,
Phone: input.Phone,
Role: input.Role,
}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Не удалось обновить данные", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Данные успешно обновлены"})
}
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"log"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"fishfish/database"
"fishfish/handlers"
"fishfish/middleware"
)
func main() {
database.InitDB()
r := gin.Default()
err := godotenv.Load()
if err != nil {
log.Println("Файл .env не был найден в папке backend, использую системные переменные")
}
// CORS Middleware
r.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "https://fish.kkhome.ru")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
api := r.Group("/api")
{
api.POST("/login", handlers.Login)
protected := api.Group("/")
protected.Use(middleware.AuthRequired())
{
protected.GET("/items/:id", handlers.GetItem)
protected.GET("/items/", handlers.GetAllItems)
protected.DELETE("/items/:id", handlers.DeleteItem)
protected.PATCH("/items/:id", handlers.UpdateItem)
protected.POST("/items", handlers.AddItem)
}
admin := protected.Group("/")
admin.Use(middleware.AdminOnly())
{
admin.POST("/register", handlers.Register)
}
}
r.Run(":8081")
}
+64
View File
@@ -0,0 +1,64 @@
package middleware
import (
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
)
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == authHeader {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"})
c.Abort()
return
}
jwtKey := []byte(os.Getenv("JWT_SECRET"))
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
if id, ok := claims["user_id"].(float64); ok {
c.Set("user_id", uint(id))
}
c.Set("role", claims["role"])
}
c.Next()
}
}
func AdminOnly() gin.HandlerFunc {
return func(c *gin.Context) {
role, exists := c.Get("role")
if !exists || role != "Админ" {
c.JSON(http.StatusForbidden, gin.H{"error": "Доступ ограничен"})
c.Abort()
return
}
c.Next()
}
}
+10
View File
@@ -0,0 +1,10 @@
package models
type Item struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `json:"name"`
City string `json:"city"`
Unit string `json:"unit"`
Price int `json:"price"`
Category string `json:"category"`
}
+8
View File
@@ -0,0 +1,8 @@
package models
type UpdateUserInput struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Phone string `json:"phone"`
Role string `json:"role"`
}
+13
View File
@@ -0,0 +1,13 @@
package models
import "gorm.io/gorm"
type User struct {
gorm.Model `json:"-"`
Username string `gorm:"unique;not null" binding:"required"`
Password string `gorm:"not null" json:"-" binding:"required,min=8"`
Role string `gorm:"default:'Рабочий'" json:"role"`
FirstName string `json:"first_name" binding:"required"`
LastName string `gorm:"index" json:"last_name" binding:"required"`
Phone string `gorm:"index" json:"phone"`
}
+42
View File
@@ -0,0 +1,42 @@
services:
# База данных
db:
image: postgres:15-alpine
container_name: fishfish_db
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- pg_data:/var/lib/postgresql/data
# Бэкенд на Go
backend:
build: ./backend
container_name: fishfish_backend
restart: always
ports:
- "8081:8081"
environment:
- DB_DSN=host=${DB_HOST} user=${DB_USER} password=${DB_PASSWORD} dbname=${DB_NAME} port=${DB_PORT} sslmode=disable
depends_on:
- db
# Фронтенд на React
frontend:
build: ./frontend
container_name: fishfish_frontend
restart: always
ports:
- "80:80"
depends_on:
- backend
# Добавляем разрешение хоста для Vite прямо тут
environment:
- VITE_ALLOWED_HOSTS=fish.kkhome.ru
volumes:
pg_data:
+5
View File
@@ -0,0 +1,5 @@
node_modules
dist
build
.git
.env
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+35
View File
@@ -0,0 +1,35 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install --legacy-peer-deps
COPY . .
RUN npm run build
FROM nginx:stable-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY <<'EOF' /etc/nginx/conf.d/default.conf
server {
listen 80;
location /api {
proxy_pass http://backend:8081;
}
# Сама статика React
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}
EOF
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+30
View File
@@ -0,0 +1,30 @@
import js from "@eslint/js";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
export default [
js.configs.recommended,
{
files: ["**/*.jsx", "**/*.tsx"],
plugins: {
react: pluginReact,
},
languageOptions: {
globals: {
...globals.browser,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
// Подключаем рекомендуемые правила вручную, если импорт конфига не работает
...pluginReact.configs.recommended.rules,
// ОТКЛЮЧАЕМ те самые правила для React 17+
"react/react-in-jsx-scope": "off",
"react/jsx-uses-react": "off",
},
},
];
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MRP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+5120
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.2.4",
"axios": "^1.15.2",
"html5-qrcode": "^2.3.8",
"lucide-react": "^1.16.0",
"papaparse": "^5.5.3",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.2"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0",
"eslint": "^9.39.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"postcss": "^8.5.12",
"tailwindcss": "^4.2.4",
"vite": "^8.0.10"
}
}
View File
View File
+39
View File
@@ -0,0 +1,39 @@
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Main from "./pages/Main";
import AddItem from "./pages/AddItem";
import AuthForm from "./components/AuthForm";
const PrivateRoute = ({ children }) => {
const token = localStorage.getItem("token");
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
};
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<AuthForm />} />
<Route
path="/"
element={
<PrivateRoute>
<Main />
</PrivateRoute>
}
/>
<Route
path="/add_item"
element={
<PrivateRoute>
<AddItem />
</PrivateRoute>
}
/>
</Routes>
</BrowserRouter>
);
}
+38
View File
@@ -0,0 +1,38 @@
import axios from "axios";
const API_URL = "/api";
axios.interceptors.request.use(
(config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
localStorage.removeItem("token");
window.location.href = "/login";
}
return Promise.reject(error);
},
);
// Базовые CRUD операции
export const getItem = (id) => axios.get(`${API_URL}/items/${id}`);
export const getAllItems = () => axios.get(`${API_URL}/items`);
export const addItem = (itemData) => axios.post(`${API_URL}/items`, itemData);
export const updateItem = (id, itemData) =>
axios.patch(`${API_URL}/items/${id}`, itemData);
export const deleteItem = (id) => axios.delete(`${API_URL}/items/${id}`);
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+1
View File
@@ -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="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+83
View File
@@ -0,0 +1,83 @@
import { useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
const AuthForm = () => {
const [formData, setFormData] = useState({ username: "", password: "" });
const navigate = useNavigate();
const API_BASE = "/api";
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await axios.post(`${API_BASE}/login`, formData);
const data = response.data;
if (data.token) {
localStorage.setItem("token", data.token);
localStorage.setItem("user", JSON.stringify(data.user));
localStorage.removeItem("error");
navigate("/");
}
} catch (err) {
let errorMessage =
err.response?.data?.error || err.message || "Неверный логин или пароль";
const errorLog = {
message: errorMessage,
};
localStorage.setItem("error", JSON.stringify(errorLog));
}
};
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 px-4">
<div className="w-full max-w-md space-y-8 rounded-lg bg-white p-10 shadow-md">
<h2 className="text-center text-3xl font-bold text-gray-900">
Вход в FISHFISH
</h2>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm space-y-px">
<input
type="text"
required
className="relative block w-full rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 focus:border-blue-500 focus:outline-none sm:text-sm"
placeholder="Логин"
value={formData.username}
onChange={(e) =>
setFormData({ ...formData, username: e.target.value })
}
/>
<input
type="password"
required
className="relative block w-full rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 focus:border-blue-500 focus:outline-none sm:text-sm"
placeholder="Пароль"
value={formData.password}
onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
/>
</div>
{localStorage.getItem("error") && (
<p className={`text-sm text-red-500`}>
{JSON.parse(localStorage.getItem("error"))["message"] || ""}
</p>
)}
<button
type="submit"
className="group relative flex w-full justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Войти
</button>
</form>
</div>
</div>
);
};
export default AuthForm;
+60
View File
@@ -0,0 +1,60 @@
import { useNavigate } from "react-router-dom";
import siteLogo from "../assets/Исходники/logo.png";
export default function Header() {
const navigate = useNavigate();
return (
<footer className="bg-light-blue w-full h-27.5 px-16 flex items-center justify-between ">
<div className="flex flex-col items-center gap-2 text-lg tracking-widest text-gold">
<img src={siteLogo} alt="Лого" className="w-12 h-12 object-contain" />
<span>FISH FISH</span>
</div>
<div className="flex items-center gap-8 text-lg uppercase tracking-wider text-light-gray">
<button
className="hover:text-gold duration-200 transition uppercase"
onClick={() => navigate("/")}
>
fish fish
</button>
<svg
width="4"
height="4"
viewBox="0 0 4 4"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="2" cy="2" r="2" fill="white" />
</svg>
<button
className="hover:text-gold duration-200 transition uppercase"
onClick={() => navigate("/shop")}
>
ассортимент
</button>
<svg
width="4"
height="4"
viewBox="0 0 4 4"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="2" cy="2" r="2" fill="white" />
</svg>
<button
className="hover:text-gold duration-200 transition uppercase"
onClick={() => navigate("/about")}
>
контакты
</button>
</div>
<div className="flex items-center">
<button className="text-light-gray hover:text-gold transition-colors duration-200">
+7 (800) 900-20-20
</button>
</div>
</footer>
);
}
+100
View File
@@ -0,0 +1,100 @@
import { useNavigate } from "react-router-dom";
import siteLogo from "../assets/Исходники/logo.png";
export default function Header() {
const navigate = useNavigate();
return (
<header className="w-full h-27.5 px-16 flex items-center justify-between ">
<div className="flex flex-col items-center gap-2 text-lg tracking-widest text-gold">
<img src={siteLogo} alt="Лого" className="w-12 h-12 object-contain" />
<span>FISH FISH</span>
</div>
<div className="flex items-center gap-8 text-lg uppercase tracking-wider text-white">
<button
className="hover:text-gold duration-200 transition uppercase"
onClick={() => navigate("/")}
>
fish fish
</button>
<svg
width="4"
height="4"
viewBox="0 0 4 4"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="2" cy="2" r="2" fill="white" />
</svg>
<button
className="hover:text-gold duration-200 transition uppercase"
onClick={() => navigate("/shop")}
>
ассортимент
</button>
<svg
width="4"
height="4"
viewBox="0 0 4 4"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="2" cy="2" r="2" fill="white" />
</svg>
<button
className="hover:text-gold duration-200 transition uppercase"
onClick={() => navigate("/about")}
>
контакты
</button>
</div>
<div className="flex items-center">
<button className="text-slate-200 hover:text-gold transition-colors duration-200 pr-7">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="7.5"
r="4"
stroke="currentColor"
strokeLinecap="square"
strokeLinejoin="round"
/>
<path
d="M21 21H3C3 17.6863 5.68629 15 9 15H15C18.3137 15 21 17.6863 21 21Z"
stroke="currentColor"
strokeLinecap="square"
/>
</svg>
</button>
<button className="text-slate-200 hover:text-gold transition-colors duration-200">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.2654 8H7.73459C6.73929 8 5.89545 8.73186 5.75469 9.71716L4.32612 19.7172C4.154 20.922 5.08892 22 6.30602 22H17.694C18.9111 22 19.846 20.922 19.6739 19.7172L18.2453 9.71716C18.1046 8.73186 17.2607 8 16.2654 8Z"
stroke="currentColor"
/>
<path
d="M9 10V5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V10"
stroke="currentColor"
/>
<circle cx="9" cy="10" r="1" fill="currentColor" />
<circle cx="15" cy="10" r="1" fill="currentColor" />
</svg>
</button>
</div>
</header>
);
}
+30
View File
@@ -0,0 +1,30 @@
@import "tailwindcss";
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
@theme {
--color-gold: #b78a61;
--color-light-blue: #1c3751;
--color-light-gray: #d2cccc;
--font-sans: "Ubuntu", sans-serif;
}
@layer base {
*,
::after,
::before {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#root {
width: 100%;
min-height: 100dvh;
background-color: #051b26;
font-family: var(--font-sans);
}
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
+114
View File
@@ -0,0 +1,114 @@
import { useState } from "react";
import * as api from "../api"; // Импортируем все функции из api/index.js
import Header from "../components/Header";
export default function Dashboard() {
const [formData, setFormData] = useState({
name: "",
unit: "",
city: "",
price: "",
category: "",
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
const payload = {
...formData,
price: parseFloat(formData.price) || 0,
};
try {
const response = await api.addItem(payload);
console.log("Товар добавлен", response.data);
setFormData({ name: "", unit: "", city: "", price: "", category: "" });
} catch (err) {
console.error(err);
alert(err);
}
};
return (
<div>
<Header />
<div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md border">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-500 mb-1">
Название товара
</label>
<input
className="w-full border p-2 rounded focus:ring-2 focus:ring-blue-500 outline-none"
type="text"
name="unit"
required
value={formData.unit}
onChange={handleChange}
placeholder="кг, шт, упаковка"
></input>
<label className="block text-sm font-medium text-slate-500 mb-1">
Город
</label>
<input
className="w-full border p-2 rounded focus:ring-2 focus:ring-blue-500 outline-none"
type="text"
name="city"
required
value={formData.city}
onChange={handleChange}
placeholder="Город"
></input>
<label className="block text-sm font-medium text-slate-500 mb-1">
Название товара
</label>
<input
className="w-full border p-2 rounded focus:ring-2 focus:ring-blue-500 outline-none"
type="text"
name="name"
required
value={formData.name}
onChange={handleChange}
placeholder="Название товара"
></input>
<label className="block text-sm font-medium text-slate-500 mb-1">
Стоимость
</label>
<input
className="w-full border p-2 rounded focus:ring-2 focus:ring-blue-500 outline-none"
type="number"
name="price"
required
value={formData.price}
onChange={handleChange}
placeholder="Стоимость"
></input>
<label className="block text-sm font-medium text-slate-500 mb-1">
Категория
</label>
<input
className="w-full border p-2 rounded focus:ring-2 focus:ring-blue-500 outline-none"
type="text"
name="category"
required
value={formData.category}
onChange={handleChange}
placeholder="Рыба, крабы, гребешки"
></input>
<button type="submit">Добавить товар</button>
</div>
</form>
</div>
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
import { useEffect, useState } from "react";
import * as api from "../api"; // Импортируем все функции из api/index.js
import Header from "../components/Header";
import Footer from "../components/Footer";
export default function Dashboard() {
const [items, setItems] = useState([]);
useEffect(() => {
api
.getAllItems()
.then((res) => {
setItems(res.data);
})
.catch((err) => {
console.error("Ошибка при получении товара с сервера", err);
});
}, []);
return (
<div>
<Header />
<div className="p-4 flex flex-col w-full gap-4">
{Array.isArray(items) &&
items.map((item) => (
<div
key={item.id}
className="bg-white flex flex-col justify-center items-start p-4 border border-slate-500 rounded-lg shadow-sm"
>
<div className="text-gray-500 text-xs">ID: ${item.id}</div>
<div className="text-xl font-black text-slate-800">
{item.name}
</div>
<div className="text-xl font-black text-slate-800">
{item.price}
</div>
<div className="text-xl font-black text-slate-800">
{item.name}
</div>
<div className="text-xl font-black text-slate-800">
{item.category}
</div>
<div className="text-xl font-black text-slate-800">
{item.unit}
</div>
</div>
))}
</div>
<Footer />
</div>
);
}
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
server: {
allowedHosts: ['mrp.kkhome.ru'],
host: true,
port: 5173,
}
})