This commit is contained in:
louiscklaw
2025-03-16 01:20:16 +08:00
parent a3fccbf5ea
commit f5ce9889c6
3112 changed files with 919640 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

212
quotation3_e-shop/NOTES.md Normal file
View File

@@ -0,0 +1,212 @@
# Notes
website template: https://mui.com/store/items/bazar-pro-react-ecommerce-template/
figma: https://www.figma.com/design/qj7rB4OOSaINllLPlUp0oZ/1683kids-tour?node-id=0-1&t=D2GYIT7gZJfbmHb3-1
- [Notes](#notes)
- [topics](#topics)
- [Thought](#thought)
- [coupon 對商戶的功用](#coupon-對商戶的功用)
- [IG shop](#ig-shop)
- [L: 我想講](#l-我想講)
- [另一個 topic, 唔續 shopage](#另一個-topic-唔續-shopage)
- [發掘可能可以賣嘅野 (賣白頭片??)](#發掘可能可以賣嘅野-賣白頭片)
## topics
### Thought
> 任何時候消費者都用緊一個去覺得同你溝通到嘅方式嚟去同你溝通
> 所以喺一個行緊嘅生意上面若果咁樣去改一個咁嘅溝通方式除非遇到一個我哋唔可以避免嘅問題啦咁否則呢就唔改好過改
> 因為改親呢就即係話有機會會流失部分嘅客人咁呢樣嘢係我哋需要避免做嘅事
> 我開頭都撈亂咗少少嘢就係話將佢同銀仔嗰樣嘢撈亂埋一齊
> 諗住用銀仔嚟去有到消費者經網頁落單
> 但既然係消費者本身比較喜歡用whatsapp嚟去落單的話呢樣嘢應該要尊重返佢
> 所以個問題就會變咗做點樣可以keep住現有個progress而又令到你可以做少啲又大又做快啲
> 我想搵個機會睇下有冇可能帶我去做一次你一個流程
> 就係話由你收到個message之後你之後嘅操作我想睇一睇
> 係處理呢個問題上面反而有D被動除非有信心班靚媽唔會因為咁而流走去第二度
> 否則會比較鼓勵唔好郁佢
> 反而會想知道點樣可以令事主做快D
> 用番我自己比錢方法做例子,我唔會限制消費者比錢方法,最緊要消費者比到錢,
> 基本上主流嘅比錢方法我都會開,直至而家都未遇過比唔到錢嘅問題
### coupon 對商戶的功用
- **商戶(事主 K) 想**
- 籍 coupon 刺激客人消費
- 客人用 coupon 兌換免運費,避免貨物屯積喺商戶度
- **要令到個客人覺得 coupon 唔用好可惜**
- coupon 設限期,過期 coupon 失效
- 要令客人隨時隨地可以睇到現存有幾多 coupon
- 提醒客人將有幾多 coupon 於幾時失效
- **實情(以免運費為例)**
- 客人買嘢時,要連埋 coupon 一齊買(送?)
- coupon = 運費分期付款
- coupon 有使用時限,過時失效 (令到客人定時購物)
---
> K: 我想係每個月個客人買夠 $500 就會有 1 次免運費
> K: 佢地係見我一賣野出黎 佢地就會落 可能同一個客 一日我會出 2-3 張單比佢
> K: 因為我 WhatsApp group 果堆客 係佢地儲夠一堆貨 由佢地決定幾時寄
> L: 咁的話 "每個月買夠 HKD500" 呢件事點體現到出黎啊 ?
> i.e.
> Feb wk1: 買左 HKD 300 貨 未寄
> Feb wk2: 買左 HKD 100 貨 未寄
> Feb wk3: 買左 HKD 200 貨 未寄
> Mar wk1: 買左 HKD 100 貨 ,一次過寄晒
Q: 咁你個運費點收啊?
A: 夠 $500 就 +1 幾時用都得
2 月 總數買左$600 有免運費 1 次
3 月 wk1 買$200 一次過寄曬(用左 2 月果次運費)
3 月 wk3 買$300
3 月 總數$500 又有一次免運費
3 月 尾想寄曬 就可以用 3 月果次
3 月 買野果條數 唔影響之前有免運費果次果到
## IG shop
https://www.instagram.com/1683_kids?igsh=MXhlMjBiYTI0NHkwZA%3D%3D&utm_source=qr
## L: 我想講
一般黎計有兩種做法 :
1. HKD500 即場免運費
1. HKD500 下一次消費免運費
點解咁長氣咁問,因為呢 D 野好多時背後有個系統去行
咁又點解要用系統去行呢? 唔用得唔得呢?
冇話唔得,但係會冇咁理想,點解?
有系統的話就會識得閘數,識得閘數就可以提個客原來佢有個免運費係你間 shop 度,可以刺激個客買野
有個系統可以容易 D 咁做,冇系統就人肉做
## 另一個 topic, 唔續 shopage
一般黎計支持嘅,但係唔應該冇左網頁去介紹貨品
**點解?**
客人有 ig shop, 可以用黎招生客
客人有 拍片, 應該要擺埋上 youtube
**但是**
客人 ig 冇一個直接方法去連番個 shop 度
- qr code ?
- link ? (text only still accepted)
目的就係要令客人用最小 click 就可以去到果頁度再慢慢 sell
```mermaid
graph TD;
A("A 客人見到 ig post")
A1("A 客人見到 youtube post")
A2("A 客人見到 whatsapp post")
B("B 客人感興趣")
D("D 客人入網頁度搵")
E("E 💀💀💀")
F("F 客人 scan qr code / copy link 去 shop 度搵")
G("搵到,繼續慢慢 sell")
H("客人落單 💰 <br /> 客人唔落單 💸")
A1-->B;
A2-->B;
A-->B;
B--(現時)-->D;
B-->F;
D--搵唔到-->E;
F-->G;
G-->H;
```
---
另一個優點
### 發掘可能可以賣嘅野 (賣白頭片??)
```mermaid
graph TD;
A("事主 K 見到有好野,<br/>但係唔知賣唔賣得去,<br/>所以未實際入貨住")
B("事主 K 放件野上網頁,<br/>落一個長 D 嘅發貨期")
C("試試喱個世界反應")
D("客人落單,<br/>事主 K 轉頭就向供應商落單")
E("客人唔落單,<br/>事主 K 冇事發生,<br/>由得果頁係度兜生意")
F("事主 K 發現太耐冇反應,<br/>乾脆收番埋件貨,<br/>當乜事都冇發生過")
G("長發貨期發生作用,<br/>到貨時就賣比客")
A-->B;
B-->C;
C-->D;
C-->E;
E-->F;
D-->G;
```
pros:
1. 好簡單,唔洗責貨,唔洗搵位放
1. 唔洗蝕紙水 (雖然暫時香港機會比較小)
1. 發掘更多可能賣得出嘅野,純大自然適者生存過程
cons:
1. 如果同一時間好多人落貨就仆街,因為唔知供應商會唔會有足夠貨物提供,不過都應該開心
---
![alt text](<WhatsApp Image 2025-02-08 at 2.47.31 PM.jpeg>)
---
[]()
---
首先,專重客戶意見
同埋,之前個 e-shop(shopage) 雖然我覺得設計唔合理,但係如果今次都仲係做番個網站的話,咁會變左吸收唔到之前 (shopage) 個網站。
咁所以,我諗我會提議
```mermaid
sequenceDiagram
事主 K-->>靚媽: 出 whatsapp message <br/>話某某某有得賣, <br/>附上連結
Note over 靚媽: click 連結
靚媽->>網頁: 靚媽選擇貨物<br/>連埋 variation, options <br/>和 quantity
靚媽->>網頁: 靚媽開始付款<br/>付上電話, <br/>稱謂<br/>送貨方式
網頁->>事主 K: 通知收到訂單<br/>扣貨一個鐘
靚媽->>payme/alipay/fps: payme/alipay/fps<br/>付款
payme/alipay/fps->>事主 K: 收左款
Note over 事主 K: 訂單確認(永久扣貨)
事主 K-->>靚媽: 出番張單, <br/>多謝幫襯<br/>thank you 😊
```
```mermaid
sequenceDiagram
事主 K-->>靚媽: 出 whatsapp message <br/>話某某某有得賣, <br/>附上連結
Note right of 靚媽: click 連結
靚媽->>網頁: 靚媽選擇貨物<br/>連埋 variation, options <br/>和 quantity
靚媽->>網頁: 靚媽開始付款<br/>付上電話, <br/>稱謂<br/>送貨方式
網頁->>事主 K: 通知收到訂單<br/>扣貨一個鐘
靚媽->>payme/alipay/fps: payme/alipay/fps<br/>付款
payme/alipay/fps->>事主 K: 收左款
Note over 靚媽: capture <br/>payment<br/> screenshot
靚媽->>網頁: 上傳 payment screenshot
網頁->>事主 K: 打包 訂單 同埋 payment screenshot
事主 K-->>靚媽: 訂單確認(永久扣貨)
事主 K-->>靚媽: 出番張單, <br/>多謝幫襯 thank you
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,6 @@
Chrome >=79
ChromeAndroid >=79
Firefox >=70
Edge >=79
Safari >=14
iOS >=14

View File

@@ -0,0 +1,31 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/dist
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
/.nx
/.nx/cache
/.vscode/*
!/.vscode/extensions.json
.idea
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Optional eslint cache
.eslintcache

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:5173",
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

View File

@@ -0,0 +1,6 @@
describe('My First Test', () => {
it('Visits the app root url', () => {
cy.visit('/')
cy.contains('ion-content', 'Tab 1 page')
})
})

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -0,0 +1,30 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist', 'cypress.config.ts'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
},
)

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ionic App</title>
<base href="/" />
<meta name="color-scheme" content="light dark" />
<meta
name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="manifest" href="/manifest.json" />
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
<!-- add to homescreen for ios -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Ionic App" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
{
"name": "ionic-instagram-clone",
"integrations": {},
"type": "react-vite"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
{
"name": "ionic-instagram-clone",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --cors --force --clearScreen",
"build": "tsc && vite build",
"preview": "vite preview",
"test.e2e": "cypress run",
"test.unit": "vitest",
"lint": "eslint"
},
"dependencies": {
"@capacitor/app": "7.0.0",
"@capacitor/core": "7.0.1",
"@capacitor/haptics": "7.0.0",
"@capacitor/keyboard": "7.0.0",
"@capacitor/status-bar": "7.0.0",
"@ionic/react": "^8.0.0",
"@ionic/react-router": "^8.0.0",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"ionicons": "^7.0.0",
"pullstate": "^1.25.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^5.3.4",
"react-router-dom": "^5.3.4",
"sass": "^1.85.1",
"web-vitals": "^4.2.4",
"workbox-background-sync": "^7.3.0",
"workbox-broadcast-update": "^7.3.0",
"workbox-cacheable-response": "^7.3.0",
"workbox-core": "^7.3.0",
"workbox-expiration": "^7.3.0",
"workbox-google-analytics": "^7.3.0",
"workbox-navigation-preload": "^7.3.0",
"workbox-precaching": "^7.3.0",
"workbox-range-requests": "^7.3.0",
"workbox-routing": "^7.3.0",
"workbox-strategies": "^7.3.0",
"workbox-streams": "^7.3.0"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"devDependencies": {
"@capacitor/cli": "7.0.1",
"@testing-library/dom": ">=7.21.4",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@vitejs/plugin-legacy": "^5.0.0",
"@vitejs/plugin-react": "^4.0.1",
"cypress": "^13.5.0",
"eslint": "^9.20.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"jsdom": "^22.1.0",
"terser": "^5.4.0",
"typescript": "^5.1.6",
"typescript-eslint": "^8.24.0",
"vite": "~5.2.0",
"vitest": "^0.34.6"
},
"description": "An Ionic project"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1 @@
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

View File

@@ -0,0 +1,21 @@
{
"short_name": "Ionic App",
"name": "My Ionic App",
"icons": [
{
"src": "assets/icon/favicon.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "assets/icon/icon.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#ffffff",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders without crashing', () => {
const { baseElement } = render(<App />);
expect(baseElement).toBeDefined();
});

View File

@@ -0,0 +1,169 @@
import { Redirect, Route } from "react-router-dom";
import {
IonApp,
IonIcon,
IonLabel,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonTabs,
setupIonicReact,
} from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import {
bagOutline,
ellipse,
home,
playCircleOutline,
searchOutline,
square,
triangle,
} from "ionicons/icons";
import Tab1 from "./pages/Tab1";
import Tab2 from "./pages/Tab2";
import Tab3 from "./pages/Tab3";
/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";
/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";
/* Optional CSS utils that can be commented out */
import "@ionic/react/css/padding.css";
import "@ionic/react/css/float-elements.css";
import "@ionic/react/css/text-alignment.css";
import "@ionic/react/css/text-transformation.css";
import "@ionic/react/css/flex-utils.css";
import "@ionic/react/css/display.css";
/**
* Ionic Dark Mode
* -----------------------------------------------------
* For more info, please see:
* https://ionicframework.com/docs/theming/dark-mode
*/
/* import '@ionic/react/css/palettes/dark.always.css'; */
/* import '@ionic/react/css/palettes/dark.class.css'; */
import "@ionic/react/css/palettes/dark.system.css";
/* Theme variables */
import "./theme/variables.css";
import Home from "./pages/Home";
import { ProfileStore } from "./pages/ProfileStore";
import MyProfile from "./pages/MyProfile";
import Profile from "./pages/Profile";
setupIonicReact();
function App() {
const profile = ProfileStore.useState((s) => s.profile);
return (
<>
<IonApp>
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<Route exact path="/home">
<Home />
</Route>
<Route exact path="/myprofile">
<MyProfile />
</Route>
<Route exact path="/profile/:id">
<Profile />
</Route>
<Route exact path="/tab1">
<Tab1 />
</Route>
<Route exact path="/tab2">
<Tab2 />
</Route>
<Route path="/tab3">
<Tab3 />
</Route>
<Route exact path="/">
<Redirect to="/home" />
</Route>
</IonRouterOutlet>
{/* */}
<IonTabBar slot="bottom">
<IonTabButton tab="home" href="/home">
<IonIcon icon={home} />
</IonTabButton>
<IonTabButton tab="tab2" href="/tab2">
<IonIcon icon={searchOutline} />
</IonTabButton>
<IonTabButton tab="tab3" href="/tab3">
<IonIcon icon={playCircleOutline} />
</IonTabButton>
<IonTabButton tab="tab4" href="/tab3">
<IonIcon icon={bagOutline} />
</IonTabButton>
<IonTabButton tab="tab5" href="/myprofile">
<img alt="tab avatar" src={profile.avatar} />
</IonTabButton>
</IonTabBar>
</IonTabs>
</IonReactRouter>
</IonApp>
</>
);
}
const App2: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<Route exact path="/home">
<Home />
</Route>
<Route exact path="/tab1">
<Tab1 />
</Route>
<Route exact path="/tab2">
<Tab2 />
</Route>
<Route path="/tab3">
<Tab3 />
</Route>
<Route exact path="/">
<Redirect to="/tab1" />
</Route>
</IonRouterOutlet>
{/* */}
<IonTabBar slot="bottom">
<IonTabButton tab="home" href="/home">
<IonIcon icon={home} />
</IonTabButton>
<IonTabButton tab="tab2" href="/tab2">
<IonIcon icon={searchOutline} />
</IonTabButton>
<IonTabButton tab="tab3" href="/tab3">
<IonIcon icon={playCircleOutline} />
</IonTabButton>
<IonTabButton tab="tab4" href="/tab3">
<IonIcon icon={bagOutline} />
</IonTabButton>
<IonTabButton tab="tab5" href="/myprofile">
<img alt="tab avatar" src={"profile.avatar"} />
</IonTabButton>
</IonTabBar>
</IonTabs>
</IonReactRouter>
</IonApp>
);
export default App;

View File

@@ -0,0 +1,24 @@
.container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.container strong {
font-size: 20px;
line-height: 26px;
}
.container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
.container a {
text-decoration: none;
}

View File

@@ -0,0 +1,16 @@
import './ExploreContainer.css';
interface ContainerProps {
name: string;
}
const ExploreContainer: React.FC<ContainerProps> = ({ name }) => {
return (
<div className="container">
<strong>{name}</strong>
<p>Explore <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
</div>
);
};
export default ExploreContainer;

View File

@@ -0,0 +1,221 @@
.postsContainer {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
.postContainer {
display: flex;
flex-direction: column;
margin-top: 1rem;
}
.postProfile {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
align-content: center;
padding-right: 0.75rem;
padding-left: 0.75rem;
}
.postProfile ion-router-link {
display: flex !important;
flex-direction: row !important;
}
.postProfileInfo ion-avatar {
height: 2.2rem;
width: 2.2rem;
}
.postProfileInfo p {
margin: 0;
padding: 0;
margin-left: 0.5rem;
font-weight: 500;
font-size: 0.9rem;
color: black;
}
.postProfileInfo {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
align-content: center;
}
.postImage {
border-top: 1px solid rgb(216, 216, 216);
margin-top: 0.5rem;
height: 20rem;
width: 100%;
}
.postImageLike {
font-size: 10rem;
color: rgb(231, 231, 231);
position: absolute;
left: 32vmin;
margin-top: 20vmin;
display: none;
}
.postActionsContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
align-content: center;
padding-right: 0.75rem;
padding-left: 0.75rem;
margin-top: 0.5rem;
}
.postActions {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.postActions ion-icon,
.postBookmark ion-icon {
font-size: 1.5rem;
}
.postActions ion-icon:not(:first-child) {
padding-left: 0.7rem;
}
.postLikesContainer {
padding-left: 0.75rem;
margin-top: 0.5rem;
}
.postLikesContainer p {
margin: 0;
padding: 0;
font-weight: 200 !important;
font-size: 0.8rem;
}
.postLikedName {
font-weight: 600;
}
.postCaption {
padding-left: 0.75rem;
padding-right: 0.75rem;
margin-top: 0.3rem;
}
.postCaption p {
margin: 0;
padding: 0;
font-weight: 200 !important;
font-size: 0.8rem;
}
.postName {
color: black !important;
font-weight: 600 !important;
}
.postName ion-router-link {
color: black;
}
.postComments {
padding-left: 0.75rem;
padding-right: 0.75rem;
margin-top: 0.5rem;
}
.postComments p {
margin: 0;
padding: 0;
color:rgb(175, 175, 175);
font-size: 0.8rem;
}
.postAddComment {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
align-content: center;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.postAddCommentProfile {
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
align-items: center;
margin-top: 0.5rem;
}
.postAddCommentProfile ion-avatar {
height: 1.9rem;
width: 1.9rem;
}
.postAddCommentProfile p {
padding-left: 0.75rem;
font-size: 0.8rem;
color: rgb(175, 175, 175);
}
.postAddCommentActions {
}
.postAddCommentActions ion-icon {
padding-left: 0.5rem;
}
.postTime {
padding-left: 0.75rem;
padding-right: 0.75rem;
margin-top: 0rem;
}
.postTime p {
margin: 0;
padding: 0;
color: rgb(175, 175, 175);
font-size: 0.6rem;
}

View File

@@ -0,0 +1,130 @@
import { IonAvatar, IonIcon, IonRouterLink } from "@ionic/react";
import {
addCircleOutline,
bookmarkOutline,
chatbubbleOutline,
ellipsisVertical,
heart,
heartOutline,
paperPlaneOutline,
} from "ionicons/icons";
import { likePost } from "../pages/PostStore";
import { ProfilesStore } from "../pages/ProfilesStore";
import { ProfileStore } from "../pages/ProfileStore";
import styles from "./Feed.module.scss";
const Feed = (props: any) => {
const { posts } = props;
const profile = ProfileStore.useState((s) => s.profile);
const profiles = ProfilesStore.useState((s) => s.profiles);
const addLike = (event: any, postID: any, liked: any) => {
likePost(event, postID, liked);
};
return (
<div className={styles.postsContainer}>
{posts.map((post: any, index: any) => {
const postProfile = profiles.filter((p) => p.id === post.profile_id)[0];
return (
<div key={index} className={styles.postContainer}>
<div className={styles.postProfile}>
<div className={styles.postProfileInfo}>
<IonRouterLink routerLink={`/profile/${postProfile.id}`}>
<IonAvatar>
<img alt="post avatar" src={postProfile.avatar} />
</IonAvatar>
</IonRouterLink>
<IonRouterLink routerLink={`/profile/${postProfile.id}`}>
<p>{postProfile.username}</p>
</IonRouterLink>
</div>
<div className={styles.postProfileMore}>
<IonIcon icon={ellipsisVertical} />
</div>
</div>
<div
className={styles.postImage}
style={{
backgroundImage: `url(${post.image})`,
backgroundPosition: "center, center",
backgroundSize: "cover",
}}
>
<IonIcon
id={`postLike_${post.id}`}
className={`animated__animated animate__heartBeat ${styles.postImageLike}`}
icon={heart}
color="light"
/>
</div>
<div className={styles.postActionsContainer}>
<div className={styles.postActions}>
<IonIcon
className="animate__animated"
color={post.liked ? "danger" : "dark"}
icon={post.liked ? heart : heartOutline}
onClick={(e) => addLike(e, post.id, post.liked)}
/>
<IonIcon icon={chatbubbleOutline} />
<IonIcon icon={paperPlaneOutline} />
</div>
<div className={styles.postBookmark}>
<IonIcon icon={bookmarkOutline} />
</div>
</div>
<div className={styles.postLikesContainer}>
<p>
Liked by{" "}
<span className={styles.postLikedName}>alanmontgomery</span> and{" "}
<span className={styles.postLikedName}>2 others</span>
</p>
</div>
<div className={styles.postCaption}>
<p>
<span className={styles.postName}>
<IonRouterLink routerLink={`/profile/${postProfile.id}`}>
{postProfile.username}
</IonRouterLink>
</span>{" "}
{post.caption}
</p>
</div>
<div className={styles.postComments}>
<p>View all {post.comments.length} comments</p>
</div>
<div className={styles.postAddComment}>
<div className={styles.postAddCommentProfile}>
<IonAvatar>
<img alt="add comment avatar" src={profile.avatar} />
</IonAvatar>
<p className="ion-margin-left">Add a comment...</p>
</div>
<div className={styles.postAddCommentActions}>
<IonIcon icon={heart} color="danger" />
<IonIcon icon={addCircleOutline} color="medium" />
</div>
</div>
<div className={styles.postTime}>
<p>{post.time}</p>
</div>
</div>
);
})}
</div>
);
};
export default Feed;

View File

@@ -0,0 +1,98 @@
$border: linear-gradient(to bottom, #d82b7e, #f57939);
.stories {
height: fit-content;
margin-top: -0.7rem;
}
.storiesContainer {
overflow-x: scroll;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
display: flex;
flex-direction: row;
width: 100%;
}
.storiesContainer::-webkit-scrollbar {
display: none;
}
.story,
.yourStory {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
align-content: center;
margin: 0 auto;
width: 4rem !important;
margin-left: 1rem;
}
.story:first-child,
.yourStory:first-child {
margin-left: 0.75rem;
}
.story p,
.yourStory p {
text-align: center;
margin: 0;
padding: 0;
margin-top: 0.2rem;
color: rgb(95, 95, 95);
font-size: 0.7rem;
font-weight: 400;
width: 120%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
margin-top: 5rem;
}
.story img,
.yourStory img {
height: 3.5rem !important;
width: 3.5rem !important;
position: absolute;
border-radius: 500px;
background: $border;
padding: 0.1rem;
}
.yourStory img {
background:rgb(214, 214, 214);
}
.storyAdd {
position: absolute;
color: white;
background-color: var(--ion-color-primary);
width: 1rem;
height: 1rem;
text-align: center;
margin: 0 auto;
display: flex;
flex-direction: row;
align-items: center;
align-content: center;
justify-content: center;
border-radius: 500px;
border: 2px solid white;
bottom: 20px;
right: 0;
padding: 0.5rem;
font-size: 0.9rem;
}

View File

@@ -0,0 +1,30 @@
import { IonCol, IonRouterLink, IonRow } from "@ionic/react";
import styles from "./Stories.module.scss";
const Stories = (props: any) => {
const { profiles } = props;
return (
<IonRow className={styles.stories}>
<div className={styles.storiesContainer}>
{profiles.map((story: any, index: any) => {
return (
<IonCol
key={index}
className={index === 0 ? styles.yourStory : styles.story}
>
<img alt="story avatar" src={story.avatar} />
{index === 0 && <div className={styles.storyAdd}>+</div>}
<IonRouterLink routerLink={`/profile/${story.id}`}>
<p>{index === 0 ? "Your story" : story.username}</p>
</IonRouterLink>
</IonCol>
);
})}
</div>
</IonRow>
);
};
export default Stories;

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,198 @@
.postsContainer {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
.postContainer {
display: flex;
flex-direction: column;
margin-top: 1rem;
}
.postProfile {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
align-content: center;
padding-right: 0.75rem;
padding-left: 0.75rem;
}
.postProfileInfo ion-avatar {
height: 2.2rem;
width: 2.2rem;
}
.postProfileInfo p {
margin: 0;
padding: 0;
margin-left: 0.5rem;
font-weight: 500;
font-size: 0.9rem;
}
.postProfileInfo {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
align-content: center;
}
.postImage {
border-top: 1px solid rgb(216, 216, 216);
margin-top: 0.5rem;
height: 20rem;
width: 100%;
}
.postActionsContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
align-content: center;
padding-right: 0.75rem;
padding-left: 0.75rem;
margin-top: 0.5rem;
}
.postActions {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.postActions ion-icon,
.postBookmark ion-icon {
font-size: 1.5rem;
}
.postActions ion-icon:not(:first-child) {
padding-left: 0.7rem;
}
.postLikesContainer {
padding-left: 0.75rem;
margin-top: 0.5rem;
}
.postLikesContainer p {
margin: 0;
padding: 0;
font-weight: 200 !important;
font-size: 0.8rem;
}
.postLikedName {
font-weight: 600;
}
.postCaption {
padding-left: 0.75rem;
padding-right: 0.75rem;
margin-top: 0.3rem;
}
.postCaption p {
margin: 0;
padding: 0;
font-weight: 200 !important;
font-size: 0.8rem;
}
.postName {
font-weight: 600 !important;
}
.postComments {
padding-left: 0.75rem;
padding-right: 0.75rem;
margin-top: 0.5rem;
}
.postComments p {
margin: 0;
padding: 0;
color:rgb(175, 175, 175);
font-size: 0.8rem;
}
.postAddComment {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
align-content: center;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.postAddCommentProfile {
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
align-items: center;
margin-top: 0.5rem;
}
.postAddCommentProfile ion-avatar {
height: 1.9rem;
width: 1.9rem;
}
.postAddCommentProfile p {
padding-left: 0.75rem;
font-size: 0.8rem;
color: rgb(175, 175, 175);
}
.postAddCommentActions {
}
.postAddCommentActions ion-icon {
padding-left: 0.5rem;
}
.postTime {
padding-left: 0.75rem;
padding-right: 0.75rem;
margin-top: 0rem;
}
.postTime p {
margin: 0;
padding: 0;
color: rgb(175, 175, 175);
font-size: 0.6rem;
}

View File

@@ -0,0 +1,60 @@
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonPage,
IonToolbar,
} from "@ionic/react";
import {
addCircleOutline,
heartOutline,
paperPlaneOutline,
} from "ionicons/icons";
import { PostStore } from "./PostStore";
import { ProfilesStore } from "./ProfilesStore";
import Stories from "../components/Stories";
import Feed from "../components/Feed";
const Home: React.FC = () => {
const profiles = ProfilesStore.useState((s) => s.profiles);
const posts = PostStore.useState((s) => s.posts);
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<img
alt="main logo"
src="/assets/logo.png"
style={{ width: "7rem" }}
/>
</IonButtons>
<IonButtons slot="end">
<IonButton color="dark">
<IonIcon icon={addCircleOutline} />
</IonButton>
<IonButton color="dark">
<IonIcon icon={heartOutline} />
</IonButton>
<IonButton color="dark">
<IonIcon icon={paperPlaneOutline} />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<Stories profiles={profiles} />
<Feed posts={posts} />
</IonContent>
</IonPage>
);
};
export default Home;

View File

@@ -0,0 +1,193 @@
import {
IonButton,
IonButtons,
IonCardSubtitle,
IonCardTitle,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonPage,
IonRow,
IonToolbar,
useIonViewWillEnter,
} from "@ionic/react";
import {
addCircleOutline,
bookmarksOutline,
chevronDown,
gridOutline,
menuOutline,
} from "ionicons/icons";
import { useState } from "react";
import styles from "./Profile.module.scss";
import { ProfilesStore } from "./ProfilesStore";
import { ProfileStore } from "./ProfileStore";
const MyProfile = () => {
const currentProfile = ProfileStore.useState((s) => s.profile);
const profiles = ProfilesStore.useState((s) => s.profiles);
const [profile, setProfile] = useState(false);
useIonViewWillEnter(() => {
const profileID = "123321";
const tempProfile = profiles.filter(
(p: any) => parseInt(p.id) === parseInt(profileID)
)[0];
setProfile(true);
});
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<p className={styles.username}>
{"profile.username"}
<IonIcon icon={chevronDown} />
</p>
</IonButtons>
<IonButtons slot="end">
<IonButton color="dark">
<IonIcon icon={addCircleOutline} />
</IonButton>
<IonButton color="dark">
<IonIcon icon={menuOutline} />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonGrid>
<IonRow className="ion-text-center ion-justify-content-between ion-align-self-center ion-align-items-center">
<IonCol size="4">
<img
src={
"https://random-image-pepebigotes.vercel.app/api/random-image"
}
alt="profile avatar"
className={styles.profileAvatar}
/>
</IonCol>
<IonCol>
<IonRow className="ion-text-center ion-justify-content-between ion-align-items-center ion-align-self-center ion-align">
<IonCol size="4" className="ion-text-center">
<IonCardTitle className={styles.value}>
{'"profile.posts" && "profile.posts.length"'}
</IonCardTitle>
<IonCardSubtitle className={styles.label}>
Posts
</IonCardSubtitle>
</IonCol>
<IonCol size="4" className="ion-text-center">
<IonCardTitle className={styles.value}>
{"profile.followers"}
</IonCardTitle>
<IonCardSubtitle className={styles.label}>
Followers
</IonCardSubtitle>
</IonCol>
<IonCol size="4" className="ion-text-center">
<IonCardTitle className={styles.value}>
{"profile.following"}
</IonCardTitle>
<IonCardSubtitle className={styles.label}>
Following
</IonCardSubtitle>
</IonCol>
</IonRow>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12" className={styles.profileInfo}>
<p className={styles.profileUsername}>
{"profile.firstname"} {"profile.surname"}
</p>
<p className={styles.profileTitle}>{"profile.title"}</p>
<p className={styles.profileBio}>{"profile.bio"}</p>
<a className={styles.profileLink} href={"profile.link"}>
{"profile.link"}
</a>
</IonCol>
</IonRow>
<IonRow className={styles.profileActions}>
<IonCol size="4">
<IonButton
className={styles.lightButton}
expand="block"
fill="outline"
>
Edit Profile
</IonButton>
</IonCol>
<IonCol size="4">
<IonButton
className={styles.lightButton}
fill="outline"
expand="block"
>
Promotions
</IonButton>
</IonCol>
<IonCol size="4">
<IonButton
className={styles.lightButton}
fill="outline"
expand="block"
>
Insights
</IonButton>
</IonCol>
</IonRow>
</IonGrid>
<IonRow className="ion-text-center ion-justify-content-center ion-align-items-center ion-align-self-center">
<IonCol
size="6"
className="ion-justify-content-center ion-align-items-center ion-align-self-center"
style={{ borderBottom: "2px solid black", marginBottom: "2px" }}
>
<IonIcon style={{ fontSize: "1.5rem" }} icon={gridOutline} />
</IonCol>
<IonCol
size="6"
className="ion-justify-content-center ion-align-items-center ion-align-self-center"
>
<IonIcon style={{ fontSize: "1.5rem" }} icon={bookmarksOutline} />
</IonCol>
</IonRow>
<IonRow className="ion-no-padding ion-no-margin">
{/* FIXME */}
{
// profile.posts
[0, 1, 2, 3, 4].map((post: any, index: any) => {
return (
<IonCol className={styles.postCol} key={index} size="4">
<img
alt="post"
src="https://random-image-pepebigotes.vercel.app/api/random-image"
/>
</IonCol>
);
})
}
</IonRow>
</IonContent>
</IonPage>
);
};
export default MyProfile;

View File

@@ -0,0 +1,141 @@
import { Store } from "pullstate";
export const PostStore = new Store({
posts: [
{
id: 1,
image: "https://random-image-pepebigotes.vercel.app/api/random-image",
caption: "Ioniconf 2021! Register Now!",
likes: 73,
liked: false,
profile_id: 6,
time: "1 hour ago",
comments: [
{
profile_id: 3,
comment: "Test",
},
],
},
{
id: 2,
image:
"https://creativetacos.com/wp-content/uploads/2019/08/Free-Chipper-Personal-Finance-App-Kit.jpg",
caption: "Ionic React Hub! UI Components, Templates, Clones and more!",
likes: 73,
liked: true,
profile_id: 1,
time: "1 hour ago",
comments: [
{
profile_id: 3,
comment: "Test",
},
{
profile_id: 3,
comment: "Test",
},
{
profile_id: 3,
comment: "Test",
},
],
},
{
id: 3,
image: "https://cdn.buttercms.com/AIcP6e8FRx6fgsKa7bvy",
caption: "Join the first ever Ionic Event!",
likes: 73,
liked: false,
profile_id: 4,
time: "2 hours ago",
comments: [
{
profile_id: 1,
comment: "Test",
},
{
profile_id: 2,
comment: "Test",
},
],
},
{
id: 4,
image: "https://ionicframework.com/img/meta/ionic-framework-og.png",
caption: "Build cross platform mobile apps with the Ionic Framework!",
likes: 73,
liked: false,
profile_id: 2,
time: "3 hours ago",
comments: [
{
profile_id: 1,
comment: "Test",
},
{
profile_id: 2,
comment: "Test",
},
],
},
],
});
export const likePost = (event, postID, liked) => {
event.target.classList.add("animate__heartBeat");
if (!liked) {
document.getElementById(`postLike_${postID}`).style.display = "inline";
}
setTimeout(() => {
event.target.classList.remove("animate__heartBeat");
document.getElementById(`postLike_${postID}`).style.display = "none";
}, 850);
PostStore.update((s) => {
s.posts.find((p, index) =>
parseInt(p.id) === parseInt(postID)
? (s.posts[index].liked = liked ? false : true)
: false
);
});
if (liked) {
PostStore.update((s) => {
s.posts.find((p, index) =>
parseInt(p.id) === parseInt(postID)
? (s.posts[index].likes = s.posts[index].likes++)
: false
);
});
} else {
PostStore.update((s) => {
s.posts.find((p, index) =>
parseInt(p.id) === parseInt(postID)
? (s.posts[index].likes = s.posts[index].likes--)
: false
);
});
}
};
export const addPost = (newPost) => {
PostStore.update((s) => {
s.posts = [...s.posts, newPost];
});
};
export const addCommentToPost = (newComment, postID) => {
PostStore.update((s) => {
s.posts.find((p, index) =>
parseInt(p.id) === parseInt(postID)
? (s.posts[index].comments = [...s.posts[index].comments, newComment])
: false
);
});
};

View File

@@ -0,0 +1,100 @@
.username {
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
align-items: center;
font-weight: 600;
}
.username ion-icon {
font-size: 0.8rem;
margin-left: 0.3rem;
}
.label {
color: black;
font-size: 0.7rem;
font-weight: 500;
font-family: Arial, Helvetica, sans-serif !important;
text-transform: lowercase;
}
.label::first-letter {
text-transform: uppercase;
}
.value {
font-size: 1rem;
}
.profileAvatar {
border-radius: 500px;
width: 5.5rem;
height: auto;
}
.profileInfo {
display: flex;
flex-direction: column;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.profileInfo p,
.profileInfo a {
// padding: 0.1rem 0 0.1rem 0;
margin: 0;
font-size: 0.9rem;
}
.profileUsername {
font-weight: 600;
}
.profileTitle {
color: rgb(136, 136, 136);
}
.profileLink {
color: rgb(22, 60, 131);
text-decoration: none;
}
.profileActions ion-button {
height: 2.3rem;
--border-radius: 5px;
font-weight: 600;
}
.lightButton {
--color: rgb(65, 65, 65);
--color-activated: rgb(65, 65, 65);
--background-hover: white;
--background-focused: white;
--background-activated: white;
--border-color: rgb(231, 231, 231);
--border-width: 2px;
font-size: 0.8rem;
}
.postCol {
--ion-grid-column-padding: 0rem;
padding: 0.1rem;
padding-bottom: 0.01rem !important;
padding-top: 0.01rem !important;
}

View File

@@ -0,0 +1,177 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonCardSubtitle,
IonCardTitle,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonPage,
IonRow,
IonTitle,
IonToolbar,
useIonViewWillEnter,
} from "@ionic/react";
import {
addCircleOutline,
arrowBackOutline,
bookmarksOutline,
chevronDown,
ellipsisVertical,
gridOutline,
menuOutline,
personOutline,
} from "ionicons/icons";
import ExploreContainer from "../components/ExploreContainer";
import "./Tab1.css";
import { useParams } from "react-router";
import { ProfilesStore } from "./ProfilesStore";
import { ProfileStore } from "./ProfileStore";
import { useState } from "react";
import styles from "./Profile.module.scss";
const Profile: React.FC = () => {
const params = useParams();
const profiles = ProfilesStore.useState((s) => s.profiles);
const currentProfile = ProfileStore.useState((s) => s.profile);
const [profile, setProfile] = useState<any>(false);
useIonViewWillEnter(() => {
// FIXME
// const profileID = params.id;
const profileID = "123321";
const tempProfile = profiles.filter(
(p: any) => parseInt(p.id) === parseInt(profileID)
)[0];
setProfile(tempProfile);
});
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Tab 1</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonGrid>
<IonRow className="ion-text-center ion-justify-content-between ion-align-self-center ion-align-items-center">
<IonCol size="4">
<img
src={profile.avatar}
alt="profile avatar"
className={styles.profileAvatar}
/>
</IonCol>
<IonCol>
<IonRow className="ion-text-center ion-justify-content-between ion-align-items-center ion-align-self-center ion-align">
<IonCol size="4" className="ion-text-center">
<IonCardTitle className={styles.value}>
{profile.posts && profile.posts.length}
</IonCardTitle>
<IonCardSubtitle className={styles.label}>
Posts
</IonCardSubtitle>
</IonCol>
<IonCol size="4" className="ion-text-center">
<IonCardTitle className={styles.value}>
{profile.followers}
</IonCardTitle>
<IonCardSubtitle className={styles.label}>
Followers
</IonCardSubtitle>
</IonCol>
<IonCol size="4" className="ion-text-center">
<IonCardTitle className={styles.value}>
{profile.following}
</IonCardTitle>
<IonCardSubtitle className={styles.label}>
Following
</IonCardSubtitle>
</IonCol>
</IonRow>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12" className={styles.profileInfo}>
<p className={styles.profileUsername}>
{profile.firstname} {profile.surname}
</p>
<p className={styles.profileTitle}>{profile.title}</p>
<p className={styles.profileBio}>{profile.bio}</p>
<a className={styles.profileLink} href={profile.link}>
{profile.link}
</a>
</IonCol>
</IonRow>
<IonRow className={styles.profileActions}>
<IonCol size="5">
<IonButton expand="block" color="primary">
Follow
</IonButton>
</IonCol>
<IonCol size="5">
<IonButton
className={styles.lightButton}
fill="outline"
expand="block"
>
Message
</IonButton>
</IonCol>
<IonCol size="2">
<IonButton className={styles.lightButton} fill="outline">
<IonIcon icon={chevronDown} />
</IonButton>
</IonCol>
</IonRow>
</IonGrid>
<IonRow className="ion-text-center ion-justify-content-center ion-align-items-center ion-align-self-center">
<IonCol
size="6"
className="ion-justify-content-center ion-align-items-center ion-align-self-center"
style={{ borderBottom: "2px solid black", marginBottom: "2px" }}
>
<IonIcon style={{ fontSize: "1.5rem" }} icon={gridOutline} />
</IonCol>
<IonCol
size="6"
className="ion-justify-content-center ion-align-items-center ion-align-self-center"
>
<IonIcon style={{ fontSize: "1.5rem" }} icon={bookmarksOutline} />
</IonCol>
</IonRow>
<IonRow className="ion-no-padding ion-no-margin">
FIXME
{/* {profile.posts &&
profile.posts.map((post, index) => {
return (
<IonCol className={styles.postCol} key={index} size="4">
<img
alt="post"
src="https://images.pexels.com/photos/699122/pexels-photo-699122.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260"
/>
</IonCol>
);
})} */}
</IonRow>
</IonContent>
</IonPage>
);
};
export default Profile;

View File

@@ -0,0 +1,21 @@
import { Store } from "pullstate";
export const ProfileStore = new Store({
profile: {
id: 1,
firstname: "Alan",
surname: "Montgomery",
avatar: "/assets/alan.jpg",
followers: 0,
following: 0,
},
posts: [],
feed: [],
});
export const addProfilePost = (newPost: any): void => {
// FIXME:
// ProfileStore.update((s) => {
// s.posts = [...s.posts, newPost];
// });
};

View File

@@ -0,0 +1,210 @@
import { Store } from "pullstate";
export const ProfilesStore = new Store({
profiles: [
{
id: 1,
firstname: "Alan",
surname: "Montgomery",
username: "alanmontgomery",
title: "Mobile Team Lead",
bio: "Full Stack 🤓 Mobile Team Lead/Senior React Dev",
link: "alanmontgomery.co.uk",
avatar: "/assets/alan.jpg",
followers: "1,470",
following: "230",
posts: [
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
],
},
{
id: 2,
firstname: "Max",
surname: "Lynch",
username: "maxlynch",
title: "CEO Ionic",
bio: "Co-founder/CEO @ionicframework. Created @capacitorjs. Gamer. @ManUtd fan.",
link: "maxlynch.com",
avatar: "https://random-image-pepebigotes.vercel.app/api/random-image",
followers: "21.1K",
following: "1,200",
posts: [{}, {}, {}, {}, {}, {}, {}, {}, {}],
},
{
id: 3,
firstname: "Ben",
surname: "Sperry",
username: "bensperry",
title: "CDO Ionic",
bio: "Co-founder / CDO @ionicframework. Creator of @ionicons. Product designer. Pixel junkie. Forest explorer.",
link: "bensperry.com",
avatar: "https://random-image-pepebigotes.vercel.app/api/random-image",
followers: "800",
following: "700",
posts: [{}, {}, {}, {}, {}, {}, {}, {}, {}],
},
{
id: 4,
firstname: "Matt",
surname: "Netkow",
username: "mattnetkow",
title: "Head of Product Marketing",
bio: "I help web developers build cross-platform Web Native apps. @IonicFramework: Head of Product Marketing",
link: "webnative.tech",
avatar: "https://random-image-pepebigotes.vercel.app/api/random-image",
followers: "1,200",
following: "900",
posts: [{}, {}, {}, {}, {}, {}, {}, {}, {}],
},
{
id: 5,
firstname: "Liam",
surname: "DeBeasi",
username: "liamdebeasi",
title: "Software Engineer",
bio: "Software Engineer at @ionicframework",
link: "liamdebeasi.com",
avatar: "https://random-image-pepebigotes.vercel.app/api/random-image",
followers: "871",
following: "510",
posts: [
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
],
},
{
id: 6,
firstname: "Mike",
surname: "Hartington",
username: "mikehartington",
title: "Senior Dev Rel",
bio: "Google Developer Expert. Mediocre at best. he/him. npx mhartington",
link: "mhartington.io",
avatar: "https://random-image-pepebigotes.vercel.app/api/random-image",
followers: "12.3K",
following: "2,200",
posts: [
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
],
},
{
id: 7,
firstname: "Adam",
surname: "Bradley",
username: "adambradley",
title: "Director of Technology",
bio: "Proud dad, husband, veteran & dogs best friend. Typos are my own",
link: "ionicframework.com",
avatar: "https://random-image-pepebigotes.vercel.app/api/random-image",
followers: "613",
following: "571",
posts: [
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
],
},
{
id: 8,
firstname: "Brody",
surname: "Kidd",
username: "brodykidd",
title: "Enterprise Account Manager",
bio: "Enterprise Account Manager | @ionicframework | @getcapacitor | @stenciljs",
link: "ionicframework.com",
avatar: "https://random-image-pepebigotes.vercel.app/api/random-image",
followers: "677",
following: "219",
posts: [{}, {}, {}, {}, {}, {}, {}],
},
],
});
export const addProfilePost = (newPost) => {
ProfilesStore.update((s) => {
s.posts = [...s.posts, newPost];
});
};

View File

@@ -0,0 +1,25 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import ExploreContainer from '../components/ExploreContainer';
import './Tab1.css';
const Tab1: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Tab 1</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Tab 1</IonTitle>
</IonToolbar>
</IonHeader>
<ExploreContainer name="Tab 1 page" />
</IonContent>
</IonPage>
);
};
export default Tab1;

View File

@@ -0,0 +1,25 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import ExploreContainer from '../components/ExploreContainer';
import './Tab2.css';
const Tab2: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Tab 2</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Tab 2</IonTitle>
</IonToolbar>
</IonHeader>
<ExploreContainer name="Tab 2 page" />
</IonContent>
</IonPage>
);
};
export default Tab2;

View File

@@ -0,0 +1,31 @@
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
} from "@ionic/react";
import ExploreContainer from "../components/ExploreContainer";
import "./Tab3.css";
const Tab3: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Tab 3</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Tab 3</IonTitle>
</IonToolbar>
</IonHeader>
<ExploreContainer name="Tab 3 page" />
</IonContent>
</IonPage>
);
};
export default Tab3;

View File

@@ -0,0 +1,14 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
// Mock matchmedia
window.matchMedia = window.matchMedia || function() {
return {
matches: false,
addListener: function() {},
removeListener: function() {}
};
};

View File

@@ -0,0 +1,124 @@
/* Ionic Variables and Theming. For more info, please see:
http://ionicframework.com/docs/theming/ */
* {
font-family: Arial, Helvetica, sans-serif !important;
scroll-behavior: smooth;
}
::-webkit-scrollbar,
::-webkit-scrollbar-thumb {
width: 0px;
}
/** Ionic CSS Variables **/
:root {
/** primary **/
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
/** secondary **/
--ion-color-secondary: #3dc2ff;
--ion-color-secondary-rgb: 61, 194, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #36abe0;
--ion-color-secondary-tint: #50c8ff;
/** tertiary **/
--ion-color-tertiary: #5260ff;
--ion-color-tertiary-rgb: 82, 96, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #4854e0;
--ion-color-tertiary-tint: #6370ff;
/** success **/
--ion-color-success: #2dd36f;
--ion-color-success-rgb: 45, 211, 111;
--ion-color-success-contrast: #ffffff;
--ion-color-success-contrast-rgb: 255, 255, 255;
--ion-color-success-shade: #28ba62;
--ion-color-success-tint: #42d77d;
/** warning **/
--ion-color-warning: #ffc409;
--ion-color-warning-rgb: 255, 196, 9;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0ac08;
--ion-color-warning-tint: #ffca22;
/** danger **/
--ion-color-danger: #eb445a;
--ion-color-danger-rgb: 235, 68, 90;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #cf3c4f;
--ion-color-danger-tint: #ed576b;
/** dark **/
--ion-color-dark: #222428;
--ion-color-dark-rgb: 34, 36, 40;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-contrast-rgb: 255, 255, 255;
--ion-color-dark-shade: #1e2023;
--ion-color-dark-tint: #383a3e;
/** medium **/
--ion-color-medium: #92949c;
--ion-color-medium-rgb: 146, 148, 156;
--ion-color-medium-contrast: #ffffff;
--ion-color-medium-contrast-rgb: 255, 255, 255;
--ion-color-medium-shade: #808289;
--ion-color-medium-tint: #9d9fa6;
/** light **/
--ion-color-light: #f4f5f8;
--ion-color-light-rgb: 244, 245, 248;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9;
}
:root {
--ion-toolbar-background: white;
--ion-tab-bar-color: black;
--ion-tab-bar-color-selected: black;
--ion-tab-bar-border-color: rgb(235, 235, 235);
}
ion-tab-button ion-icon {
font-size: 1.6rem;
}
ion-tab-button img {
border-radius: 500px;
height: 1.8rem;
border: 1px solid black;
}
ion-tab-bar {
height: 3rem;
}
ion-toolbar {
--border-style: none;
--padding-start: 1rem;
--padding-end: 1rem;
--padding-top: 0.5rem;
}
ion-toolbar ion-icon {
font-weight: 900 !important;
font-size: 1.6rem;
}
ion-toolbar ion-button:not(:last-child) {
padding-right: 0.3rem;
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,18 @@
/// <reference types="vitest" />
import legacy from '@vitejs/plugin-legacy'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
legacy()
],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/setupTests.ts',
}
})

View File

@@ -0,0 +1,9 @@
# NOTES
其實班靚媽幫襯你有可能因為你夠直接,一搵就搵到你,
## ionic-instgram-clone
i stopped at the middle of `ionic-instgram-clone`,
i found that the `ionic-instagram-clone` should be simple enough to re-structure/refactor to use ionic7
i like the layout and want to clone the layout from this source

Submodule quotation3_e-shop/_lab/ionic-instagram-clone-original added at 82f99df290

Binary file not shown.

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -ex
# ionic start --help
ionic start "ionic-instagram-clone" tabs --capacitor --type react --no-git
cd ionic-instagram-clone
npm i --save pullstate
npm i --save sass
npm i --save "web-vitals"
npm i --save "workbox-background-sync"
npm i --save "workbox-broadcast-update"
npm i --save "workbox-cacheable-response"
npm i --save "workbox-core"
npm i --save "workbox-expiration"
npm i --save "workbox-google-analytics"
npm i --save "workbox-navigation-preload"
npm i --save "workbox-precaching"
npm i --save "workbox-range-requests"
npm i --save "workbox-routing"
npm i --save "workbox-strategies"
npm i --save "workbox-streams"
cd ..
echo "done"

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -0,0 +1,16 @@
# PRODUCT1
![](./5792a24f-90ee-4642-98d9-a91f8268aa2f.jfif)
📣📣預訂📣📣 台灣製造 台灣直送
2-3 星期到‼️
$68@1盒/ $130@2
1 盒 6 包
4m+ bb 可以食‼️‼️‼️
★100%無農藥殘留、無添加香精、無添加鹽、無添加糖、無添加油、無麩質,全素可食。
★質地綿密、入口即化從4個月以上寶寶至牙口不便銀髮族皆可食用老少咸宜。
★創意心型與條狀米菓,小手抓握一次一顆剛剛好。
★內含6份貼心獨立小包裝次次都吃的到最新鮮的產品。

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

View File

@@ -0,0 +1,24 @@
# PRODUCT3
<!-- ![](./850ecd91-c627-4b2c-b0dc-f290f6347874.jfif) -->
<!-- ![](../product2/shot1.jpeg) -->
📣📣台灣好物分享📣📣
依個真係真係勁大推比易敏感既bb試試
妹妹係去台灣之前下巴突然紅曬
好似生濕疹甘 又r損曬
因為真係查極唔同既cream 都依然冇改善 我又唔想佢查藥膏
所以去左台灣藥房搵‼️‼️‼️
佢地推介左依款膏(英國製造)
❤️成份天然
冇任何藥性成份
👉🏻👉🏻👉🏻蜂膠萃取物、甜杏仁油、蜂蠟、維他命E
取自蜜蜂為保護蜂巢所分泌的蜂膠,含有多重來自花粉產生的微量元素與維生素,可適度緩解與修護肌膚不適問題。*不含刺激成分,眼睛與臉部也可塗抹。

View File

@@ -0,0 +1 @@
代碼,類型,貨號,"GTIN, UPC, EAN, or ISBN",名稱,已發佈,是特色商品?,目錄的可見度,簡短內容說明,描述,折扣價開始日期,折扣價結束日期,稅金狀態,稅率類別,有庫存?,庫存,低庫存量,允許無庫存下單嗎?,單獨銷售?,"重量 (公斤)","長 (公分)","寬 (公分)","高 (公分)",允許客戶評論嗎?,購買備註,折扣價,原價,分類,標籤,運送類別,圖片,下載限制,下載點過期天數,上層,組合商品,追加銷售,交叉銷售,外部網址,按鈕文字,位置,Brands
1 代碼 類型 貨號 GTIN, UPC, EAN, or ISBN 名稱 已發佈 是特色商品? 目錄的可見度 簡短內容說明 描述 折扣價開始日期 折扣價結束日期 稅金狀態 稅率類別 有庫存? 庫存 低庫存量 允許無庫存下單嗎? 單獨銷售? 重量 (公斤) 長 (公分) 寬 (公分) 高 (公分) 允許客戶評論嗎? 購買備註 折扣價 原價 分類 標籤 運送類別 圖片 下載限制 下載點過期天數 上層 組合商品 追加銷售 交叉銷售 外部網址 按鈕文字 位置 Brands

View File

@@ -0,0 +1 @@
github: nezhar

View File

@@ -0,0 +1 @@
.env

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 nezhar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,159 @@
# WPDC - WordPress Docker Compose
Easy WordPress development with Docker and Docker Compose.
With this project you can quickly run the following:
- [WordPress and WP CLI](https://hub.docker.com/_/wordpress/)
- [phpMyAdmin](https://hub.docker.com/r/phpmyadmin/phpmyadmin/)
- [MySQL](https://hub.docker.com/_/mysql/)
Contents:
- [Requirements](#requirements)
- [Configuration](#configuration)
- [Installation](#installation)
- [Usage](#usage)
## Requirements
Make sure you have the latest versions of **Docker** and **Docker Compose** installed on your machine.
Clone this repository or copy the files from this repository into a new folder. In the **docker-compose.yml** file you may change the IP address (in case you run multiple containers) or the database from MySQL to MariaDB.
Make sure to [add your user to the `docker` group](https://docs.docker.com/install/linux/linux-postinstall/#manage-docker-as-a-non-root-user) when using Linux.
## Configuration
Copy the example environment into `.env`
```
cp env.example .env
```
Edit the `.env` file to change the default IP address, MySQL root password and WordPress database name.
## Installation
Open a terminal and `cd` to the folder in which `docker-compose.yml` is saved and run:
```
docker-compose up
```
This creates two new folders next to your `docker-compose.yml` file.
* `wp-data` used to store and restore database dumps
* `wp-app` the location of your WordPress application
The containers are now built and running. You should be able to access the WordPress installation with the configured IP in the browser address. By default it is `http://127.0.0.1`.
For convenience you may add a new entry into your hosts file.
## Usage
### Starting containers
You can start the containers with the `up` command in daemon mode (by adding `-d` as an argument) or by using the `start` command:
```
docker-compose start
```
### Stopping containers
```
docker-compose stop
```
### Removing containers
To stop and remove all the containers use the`down` command:
```
docker-compose down
```
Use `-v` if you need to remove the database volume which is used to persist the database:
```
docker-compose down -v
```
### Project from existing source
Copy the `docker-compose.yml` file into a new directory. In the directory you create two folders:
* `wp-data` here you add the database dump
* `wp-app` here you copy your existing WordPress code
You can now use the `up` command:
```
docker-compose up
```
This will create the containers and populate the database with the given dump. You may set your host entry and change it in the database, or you simply overwrite it in `wp-config.php` by adding:
```
define('WP_HOME','http://wp-app.local');
define('WP_SITEURL','http://wp-app.local');
```
### Creating database dumps
```
./export.sh
```
### Developing a Theme
Configure the volume to load the theme in the container in the `docker-compose.yml`:
```
volumes:
- ./theme-name/trunk/:/var/www/html/wp-content/themes/theme-name
```
### Developing a Plugin
Configure the volume to load the plugin in the container in the `docker-compose.yml`:
```
volumes:
- ./plugin-name/trunk/:/var/www/html/wp-content/plugins/plugin-name
```
### WP CLI
The docker compose configuration also provides a service for using the [WordPress CLI](https://developer.wordpress.org/cli/commands/).
Sample command to install WordPress:
```
docker-compose run --rm wpcli core install --url=http://localhost --title=test --admin_user=admin --admin_email=test@example.com
```
Or to list installed plugins:
```
docker-compose run --rm wpcli plugin list
```
For an easier usage you may consider adding an alias for the CLI:
```
alias wp="docker-compose run --rm wpcli"
```
This way you can use the CLI command above as follows:
```
wp plugin list
```
### phpMyAdmin
You can also visit `http://127.0.0.1:8080` to access phpMyAdmin after starting the containers.
The default username is `root`, and the password is the same as supplied in the `.env` file.

View File

@@ -0,0 +1,69 @@
services:
wp:
image: wordpress:latest # https://hub.docker.com/_/wordpress/
ports:
- ${IP}:${PORT}:80 # change ip if required
volumes:
- ./volumes/config/wp_php.ini:/usr/local/etc/php/conf.d/conf.ini
- ./volumes/wp-app:/var/www/html # Full wordpress project
- ./src/plugin-helloworld/trunk/:/var/www/html/wp-content/plugins/plugin-helloworld # Plugin development
- ./src/theme-helloworld/trunk/:/var/www/html/wp-content/themes/theme-helloworld # Theme development
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: "${DB_NAME}"
WORDPRESS_DB_USER: root
WORDPRESS_DB_PASSWORD: "${DB_ROOT_PASSWORD}"
depends_on:
- db
links:
- db
wpcli:
image: wordpress:cli
volumes:
- ./volumes/config/wp_php.ini:/usr/local/etc/php/conf.d/conf.ini
- ./volumes/wp-app:/var/www/html
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: "${DB_NAME}"
WORDPRESS_DB_USER: root
WORDPRESS_DB_PASSWORD: "${DB_ROOT_PASSWORD}"
depends_on:
- db
- wp
pma:
image: phpmyadmin:latest # https://hub.docker.com/_/phpmyadmin
environment:
# https://docs.phpmyadmin.net/en/latest/setup.html#docker-environment-variables
PMA_HOST: db
PMA_PORT: 3306
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
UPLOAD_LIMIT: 50M
ports:
- ${IP}:8080:80
links:
- db:db
volumes:
- ./volumes/config/pma_php.ini:/usr/local/etc/php/conf.d/conf.ini
- ./volumes/config/pma_config.php:/etc/phpmyadmin/config.user.inc.php
db:
image: mysql:latest # https://hub.docker.com/_/mysql/ - or mariadb https://hub.docker.com/_/mariadb
# platform: linux/x86_64 # Uncomment if your machine is running on arm (ex: Apple Silicon processor)
ports:
- ${IP}:3306:3306 # change ip if required
command: [
'--character-set-server=utf8mb4',
'--collation-server=utf8mb4_unicode_ci'
]
volumes:
- ./volumes/wp-data:/docker-entrypoint-initdb.d
- db_data:/var/lib/mysql
environment:
MYSQL_DATABASE: "${DB_NAME}"
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
volumes:
db_data:

View File

@@ -0,0 +1,4 @@
IP=127.0.0.1
PORT=80
DB_ROOT_PASSWORD=password
DB_NAME=wordpress

View File

@@ -0,0 +1,14 @@
#!/bin/bash
_os="`uname`"
_now=$(date +"%m_%d_%Y")
_file="wp-data/data_$_now.sql"
# Export dump
EXPORT_COMMAND='exec mysqldump "$MYSQL_DATABASE" -uroot -p"$MYSQL_ROOT_PASSWORD"'
docker-compose exec db sh -c "$EXPORT_COMMAND" > $_file
if [[ $_os == "Darwin"* ]] ; then
sed -i '.bak' 1,1d $_file
else
sed -i 1,1d $_file # Removes the password warning from the file
fi

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -ex
docker compose kill
docker compose down
sleep 1
docker compose up -d
docker compose logs -f
echo "done"

Some files were not shown because too many files have changed in this diff Show More