├── .env.example ├── .gitignore ├── .python-version ├── Procfile ├── README.md ├── app ├── __init__.py ├── config.py ├── library │ ├── __init__.py │ ├── helpers.py │ └── search.py ├── main.py ├── pages │ ├── about.md │ ├── contact.md │ ├── home.md │ ├── info.md │ └── portfolio.md └── routers │ ├── __init.py │ ├── accordion.py │ ├── copy_to_clipboard.py │ ├── flickr_search.py │ ├── twoforms.py │ ├── unsplash.py │ ├── unsplash_search.py │ └── upload.py ├── images ├── drag_drop.png └── image-1.png ├── requirements.txt ├── runtime.txt ├── static ├── css │ ├── mystyle.css │ ├── search_image.css │ └── style3.css ├── images │ ├── copy.gif │ ├── favicon.ico │ ├── image-1.png │ ├── logo.svg │ └── unsplash1.jpeg ├── js │ ├── dragdrop.js │ ├── flickr_search.js │ ├── imagehelper.js │ └── unsplash_search.js └── upload │ └── db0e85e8 │ ├── cecilieo_color_picker (5).png │ └── cecilieo_color_picker (5).thumbnail.png ├── templates ├── _unsplash_search.html ├── accordion.html ├── base.html ├── copy_to_clipboard.html ├── flickr_search.html ├── include │ ├── sidebar.html │ └── topnav.html ├── page.html ├── twoforms.html ├── unsplash.html ├── unsplash_search.html └── upload.html └── tests ├── __init__.py └── test_main.py /.env.example: -------------------------------------------------------------------------------- 1 | unsplash_key=your-key 2 | flickr_key=your-flickr-key 3 | flickr_secret=your-flickr-secret 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # FastAPI starter 2 | .DS_Store 3 | /data 4 | /tmp 5 | 6 | 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.6 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn app.main:app --host=0.0.0.0 --port=${PORT:-5000} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fastapi Web Starter 2 | 3 | Please read the Medium article at [https://shinichiokada.medium.com/](https://shinichiokada.medium.com/). 4 | 5 | ## Overview 6 | 7 | A simple website ready to deploy. 8 | This repo includes all the file and it is ready to deploy to Heroku. 9 | 10 | - .env 11 | - .gitignore 12 | - app 13 | - Procfile 14 | - README.md 15 | - requirements.txt 16 | - runtime.txt 17 | - static 18 | - templates 19 | 20 | ## Requirement 21 | 22 | - Python 3.9.6 23 | - See requirements.txt. 24 | 25 | ## Installation & Usage 26 | 27 | Get your Unsplash API and put it in the `.env` file. 28 | 29 | ```bash 30 | $ git clone git@github.com:shinokada/fastapi-webstarter-demo.git 31 | # change the directory 32 | $ cd fastapi-webstarter-demo 33 | # install packages 34 | $ pip install -r requirements.txt 35 | # start the server 36 | $ uvicorn app.main:app --reload --port 8000 37 | ``` 38 | 39 | Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/). 40 | 41 |  42 | 43 | ## Features 44 | 45 | - Flickr image search 46 | - Unsplash image search 47 | - Drag & Drop Form 48 | - Menu 49 | - Unsplash 50 | - Accordion 51 | - Markdown pages 52 | - Two Forms 53 | 54 | ## Test 55 | 56 | All tests are under `tests` directory. 57 | 58 | ```bash 59 | # Change the directory 60 | $ cd fastapi-webstarter-demo 61 | # Run tests 62 | $ pytest -v 63 | ``` 64 | 65 | ## Author 66 | 67 | [twitter](https://twitter.com/shinokada) 68 | 69 | ## Licence 70 | 71 | 【MIT License】 72 | 73 | Copyright 2021 Shinichi Okada 74 | 75 | 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: 76 | 77 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 78 | 79 | 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. 80 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinokada/fastapi-webstarter-demo/74d6363d7bbb4b257bafbb5106e229c105a06b53/app/__init__.py -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseSettings 2 | 3 | 4 | class Settings(BaseSettings): 5 | work_dir: str = 'static/upload/' 6 | thumb_width: int = 340 7 | thumb_height: int = 800 8 | thumb_size: tuple = (300, 500) 9 | max_imgWidth: int = 600 10 | max_imgHeight: int = 800 11 | 12 | 13 | settings = Settings() 14 | -------------------------------------------------------------------------------- /app/library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinokada/fastapi-webstarter-demo/74d6363d7bbb4b257bafbb5106e229c105a06b53/app/library/__init__.py -------------------------------------------------------------------------------- /app/library/helpers.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import uuid 3 | from pathlib import Path 4 | from PIL import Image 5 | import markdown 6 | from ..config import settings 7 | import functools 8 | 9 | 10 | def openfile(filename): 11 | filepath = os.path.join("app/pages/", filename) 12 | with open(filepath, "r", encoding="utf-8") as input_file: 13 | text = input_file.read() 14 | 15 | html = markdown.markdown(text) 16 | data = { 17 | "text": html 18 | } 19 | return data 20 | 21 | 22 | def setdimensions(winWidth, imgWidth, imgHeight): 23 | ratio = winWidth / imgWidth 24 | new_imgWidth = int(imgWidth * ratio * 0.8) 25 | max_imgWidth = settings.max_imgWidth 26 | max_imgHeight = settings.max_imgHeight 27 | if (new_imgWidth > max_imgWidth): 28 | new_imgWidth = max_imgWidth 29 | 30 | return (new_imgWidth, max_imgHeight) 31 | 32 | 33 | def create_workspace(): 34 | """ 35 | Return workspace path 36 | """ 37 | # base directory 38 | work_dir = Path(settings.work_dir) 39 | # UUID to prevent file overwrite 40 | request_id = Path(str(uuid.uuid4())[:8]) 41 | # path concat instead of work_dir + '/' + request_id 42 | workspace = work_dir / request_id 43 | if not os.path.exists(workspace): 44 | # recursively create workdir/unique_id 45 | os.makedirs(workspace) 46 | 47 | return workspace 48 | 49 | 50 | def thumb(myfile, winWidth, imgWidth, imgHeight): 51 | size = setdimensions(winWidth, imgWidth, imgHeight) 52 | # size = settings.thumb_width, settings.thumb_height 53 | filepath, ext = os.path.splitext(myfile) 54 | # print(ext) 55 | im = Image.open(myfile) 56 | im = image_transpose_exif(im) 57 | im.thumbnail(size) 58 | imgtype = "PNG" if ext == ".png" else "JPEG" 59 | # print(imgtype) 60 | im.save(filepath + ".thumbnail"+ext, imgtype) 61 | 62 | 63 | def image_transpose_exif(im): 64 | """ 65 | https://stackoverflow.com/questions/4228530/pil-thumbnail-is-rotating-my-image 66 | Apply Image.transpose to ensure 0th row of pixels is at the visual 67 | top of the image, and 0th column is the visual left-hand side. 68 | Return the original image if unable to determine the orientation. 69 | 70 | As per CIPA DC-008-2012, the orientation field contains an integer, 71 | 1 through 8. Other values are reserved. 72 | 73 | Parameters 74 | ---------- 75 | im: PIL.Image 76 | The image to be rotated. 77 | """ 78 | 79 | exif_orientation_tag = 0x0112 80 | exif_transpose_sequences = [ # Val 0th row 0th col 81 | [], # 0 (reserved) 82 | [], # 1 top left 83 | [Image.FLIP_LEFT_RIGHT], # 2 top right 84 | [Image.ROTATE_180], # 3 bottom right 85 | [Image.FLIP_TOP_BOTTOM], # 4 bottom left 86 | [Image.FLIP_LEFT_RIGHT, Image.ROTATE_90], # 5 left top 87 | [Image.ROTATE_270], # 6 right top 88 | [Image.FLIP_TOP_BOTTOM, Image.ROTATE_90], # 7 right bottom 89 | [Image.ROTATE_90], # 8 left bottom 90 | ] 91 | 92 | try: 93 | seq = exif_transpose_sequences[im._getexif()[exif_orientation_tag]] 94 | except Exception: 95 | return im 96 | else: 97 | return functools.reduce(type(im).transpose, seq, im) 98 | -------------------------------------------------------------------------------- /app/library/search.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import flickrapi 4 | 5 | 6 | def unsplashFetch(item, page_num): 7 | unsplash_key = os.getenv("unsplash_key") 8 | param = { 9 | "client_id": unsplash_key, 10 | "query": item, 11 | "per_page": 30, 12 | "page": page_num 13 | } 14 | url = "https://api.unsplash.com/search/photos/" 15 | response = requests.get(url, param) 16 | data = response.json() 17 | return data 18 | 19 | 20 | def flickr_photos_search(item, page_num): 21 | api_key = os.getenv("flickr_key").encode() 22 | api_secret = os.getenv("flickr_secret").encode() 23 | flickr = flickrapi.FlickrAPI(api_key, api_secret, format='parsed-json') 24 | # url_sq (75x75), url_q(150x150), url_t(w:100), url_s(w:240), url_m(w:500), url_n(w:320), url_z(w:640), url_c(w:800), url_l(w:1024), url_o(w:2400) 25 | extras = 'url_c,url_w,url_n,url_m,url_z,license,owner_name,' 26 | license = '1,2,3,4,5,6' 27 | 28 | photos = flickr.photos.search( 29 | text=item, 30 | sort='relevance', 31 | safe_search=1, 32 | per_page=30, 33 | license=license, 34 | page=page_num, 35 | tag_mode='all', 36 | extras=extras 37 | ) 38 | photo_items = photos["photos"]["photo"] 39 | print(len(photo_items)) 40 | # selected_photos = random.sample(list(photo_items), select) 41 | flickr_licenses: dict = { 42 | 1: "by-nc-sa", 43 | 2: "by-nc", 44 | 3: "by-nc-nd", 45 | 4: "by", 46 | 5: "by-sa", 47 | 6: "by-nd", 48 | } 49 | for item in photo_items: 50 | num = int(item["license"]) 51 | # print(type(item)) 52 | item["license_name"] = flickr_licenses[num] 53 | 54 | return photo_items 55 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | from fastapi import FastAPI, Request 3 | from fastapi.responses import HTMLResponse 4 | from fastapi.staticfiles import StaticFiles 5 | from fastapi.templating import Jinja2Templates 6 | 7 | from .library.helpers import * 8 | from app.routers import upload, twoforms, unsplash, unsplash_search, flickr_search, accordion, copy_to_clipboard 9 | from dotenv import load_dotenv 10 | load_dotenv() 11 | 12 | mimetypes.init() 13 | 14 | app = FastAPI() 15 | 16 | 17 | templates = Jinja2Templates(directory="templates") 18 | 19 | mimetypes.add_type('application/javascript', '.js') 20 | app.mount("/static", StaticFiles(directory="static"), name="static") 21 | 22 | app.include_router(copy_to_clipboard.router) 23 | app.include_router(upload.router) 24 | app.include_router(unsplash_search.router) 25 | app.include_router(flickr_search.router) 26 | app.include_router(unsplash.router) 27 | app.include_router(twoforms.router) 28 | app.include_router(accordion.router) 29 | 30 | 31 | @app.get("/", response_class=HTMLResponse) 32 | async def home(request: Request): 33 | data = openfile("home.md") 34 | return templates.TemplateResponse("page.html", {"request": request, "data": data}) 35 | 36 | 37 | @app.get("/page/{page_name}", response_class=HTMLResponse) 38 | async def show_page(request: Request, page_name: str): 39 | data = openfile(page_name+".md") 40 | return templates.TemplateResponse("page.html", {"request": request, "data": data}) 41 | -------------------------------------------------------------------------------- /app/pages/about.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | 4 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse velit nunc, tincidunt sed tempus eget, luctus venenatis ex. Morbi et felis pharetra, rhoncus erat et, feugiat ipsum. Nunc lorem elit, pellentesque ac magna et, luctus volutpat odio. Suspendisse potenti. Praesent interdum ligula nunc, non tempor mi congue et. Pellentesque tellus neque, lacinia vitae auctor ac, interdum ac mauris. Suspendisse et nunc in nibh iaculis semper vel ac sem. In vel eros eget ipsum semper dapibus vel in nibh. Nunc ac augue id nulla ultrices porta. Nullam consectetur sed purus eu congue. 5 | -------------------------------------------------------------------------------- /app/pages/contact.md: -------------------------------------------------------------------------------- 1 | # Contact 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse velit nunc, tincidunt sed tempus eget, luctus venenatis ex. Morbi et felis pharetra, rhoncus erat et, feugiat ipsum. Nunc lorem elit, pellentesque ac magna et, luctus volutpat odio. Suspendisse potenti. Praesent interdum ligula nunc, non tempor mi congue et. Pellentesque tellus neque, lacinia vitae auctor ac, interdum ac mauris. Suspendisse et nunc in nibh iaculis semper vel ac sem. In vel eros eget ipsum semper dapibus vel in nibh. Nunc ac augue id nulla ultrices porta. Nullam consectetur sed purus eu congue. 4 | -------------------------------------------------------------------------------- /app/pages/home.md: -------------------------------------------------------------------------------- 1 |
4 | This project uses FastAPI, Jinja2, and Bootstrap4 and 5 in Medium Unsplash Image page. 5 | 6 | A simple website ready to deploy. 7 | This repo includes all the file and it is ready to deploy to Heroku. 8 |
9 | 10 | 11 | Please read the Medium article at [https://shinichiokada.medium.com/](https://shinichiokada.medium.com/). 12 | 13 | [Please sign up for more free resources.](https://mailchi.mp/ae9891ba897a/codewithshin) 14 | 15 | ## Links 16 | 17 | - [Demo](https://fastapi-webstarter-demo.herokuapp.com/) 18 | - [Building a Website Starter with FastAPI](https://levelup.gitconnected.com/building-a-website-starter-with-fastapi-92d077092864) 19 | - [Link: How to Deploy a FastAPI App on Heroku for Free](https://towardsdatascience.com/how-to-deploy-your-fastapi-app-on-heroku-for-free-8d4271a4ab9) 20 | - [How to Build a Drag & Drop Form with FastAPI & JavaScript](https://towardsdatascience.com/how-to-build-a-drag-drop-form-with-python-javascript-f5e43433b005) 21 | 22 | ## Updated 23 | 24 | 2022-1-5 25 | 26 | ## Version 27 | 28 | 0.4.0 29 | 30 | ## Python environment 31 | 32 | 3.9.6 33 | 34 | ## Overview 35 | 36 | A simple website ready to deploy. 37 | This repo includes all the file and it is ready to deploy to Heroku. 38 | 39 | - .env 40 | - .gitignore 41 | - app 42 | - Procfile 43 | - README.md 44 | - requirements.txt 45 | - runtime.txt 46 | - static 47 | - templates 48 | 49 | ## Requirements 50 | 51 | - requests==2.26.0 52 | - fastapi==0.70.0 53 | - uvicorn==0.15.0 54 | - python-dotenv==0.19.1 55 | - aiofiles==0.7.0 56 | - python-multipart==0.0.5 57 | - jinja2==3.0.2 58 | - Markdown==3.3.4 59 | - pytest==6.2.5 60 | - Pillow==8.4.0 61 | 62 | ## Installation & Usage 63 | 64 | Get your Unsplash API and put it in the `.env` file. 65 | 66 |
67 | $ git clone git@github.com:shinokada/fastapi-web-starter.git
68 | # change the directory
69 | $ cd fastapi-web-starter
70 | # install packages
71 | $ pip install -r requirements.txt
72 | # start the server
73 | $ uvicorn app.main:app --reload --port 8000
74 |
75 |
76 | Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/).
77 |
78 |
95 | # Change the directory
96 | $ cd fastapi-web-starter
97 | # Run tests
98 | $ pytest -v
99 |
100 |
101 | ## Author
102 |
103 | [twitter](https://twitter.com/shinokada)
104 |
105 | ## Licence
106 |
107 | 【MIT License】
108 |
109 | Copyright 2021 Shinichi Okada
110 |
111 | 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:
112 |
113 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
114 |
115 | 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.
116 |
--------------------------------------------------------------------------------
/app/pages/info.md:
--------------------------------------------------------------------------------
1 | # Information
2 |
3 |
4 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse velit nunc, tincidunt sed tempus eget, luctus venenatis ex. Morbi et felis pharetra, rhoncus erat et, feugiat ipsum. Nunc lorem elit, pellentesque ac magna et, luctus volutpat odio. Suspendisse potenti. Praesent interdum ligula nunc, non tempor mi congue et. Pellentesque tellus neque, lacinia vitae auctor ac, interdum ac mauris. Suspendisse et nunc in nibh iaculis semper vel ac sem. In vel eros eget ipsum semper dapibus vel in nibh. Nunc ac augue id nulla ultrices porta. Nullam consectetur sed purus eu congue.
5 |
--------------------------------------------------------------------------------
/app/pages/portfolio.md:
--------------------------------------------------------------------------------
1 | # Portfolio
2 |
3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse velit nunc, tincidunt sed tempus eget, luctus venenatis ex. Morbi et felis pharetra, rhoncus erat et, feugiat ipsum. Nunc lorem elit, pellentesque ac magna et, luctus volutpat odio. Suspendisse potenti. Praesent interdum ligula nunc, non tempor mi congue et. Pellentesque tellus neque, lacinia vitae auctor ac, interdum ac mauris. Suspendisse et nunc in nibh iaculis semper vel ac sem. In vel eros eget ipsum semper dapibus vel in nibh. Nunc ac augue id nulla ultrices porta. Nullam consectetur sed purus eu congue.
4 |
--------------------------------------------------------------------------------
/app/routers/__init.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shinokada/fastapi-webstarter-demo/74d6363d7bbb4b257bafbb5106e229c105a06b53/app/routers/__init.py
--------------------------------------------------------------------------------
/app/routers/accordion.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI, Request, Form, APIRouter
2 | from fastapi.responses import HTMLResponse
3 | from fastapi.templating import Jinja2Templates
4 |
5 | router = APIRouter()
6 | templates = Jinja2Templates(directory="templates/")
7 |
8 |
9 | @router.get("/accordion", response_class=HTMLResponse)
10 | def get_accordion(request: Request):
11 | tag = "flower"
12 | result = "Type a number"
13 | return templates.TemplateResponse('accordion.html', context={'request': request, 'result': result, 'tag': tag})
14 |
15 |
16 | @router.post("/accordion", response_class=HTMLResponse)
17 | def post_accordion(request: Request, tag: str = Form(...)):
18 |
19 | return templates.TemplateResponse('accordion.html', context={'request': request, 'tag': tag})
20 |
--------------------------------------------------------------------------------
/app/routers/copy_to_clipboard.py:
--------------------------------------------------------------------------------
1 | from fastapi import Request, Form, APIRouter
2 | from fastapi.responses import HTMLResponse
3 | from fastapi.templating import Jinja2Templates
4 |
5 | templates = Jinja2Templates(directory="templates")
6 | router = APIRouter()
7 |
8 |
9 | @router.get("/copy-to-clipboard", response_class=HTMLResponse)
10 | async def get_flickr(request: Request):
11 |
12 | data = {
13 | "request": request,
14 | "title": "Copy to clipboard"
15 | }
16 |
17 | return templates.TemplateResponse("copy_to_clipboard.html", data)
18 |
--------------------------------------------------------------------------------
/app/routers/flickr_search.py:
--------------------------------------------------------------------------------
1 | from fastapi import Request, Form, APIRouter
2 | from fastapi.responses import HTMLResponse
3 | from fastapi.templating import Jinja2Templates
4 | from .. library.search import flickr_photos_search
5 |
6 | templates = Jinja2Templates(directory="templates")
7 | router = APIRouter()
8 |
9 |
10 | @router.get("/flickr-search", response_class=HTMLResponse)
11 | async def get_flickr(request: Request):
12 |
13 | data = {
14 | "request": request,
15 | "title": "Flickr Search"
16 | }
17 |
18 | return templates.TemplateResponse("flickr_search.html", data)
19 |
20 |
21 | @router.get("/flickr_photos/{tag}/{page_num}")
22 | async def photos(request: Request, tag: str, page_num: int):
23 |
24 | data = flickr_photos_search(tag, page_num)
25 | # data = unsplashFetch(tag, page_num)
26 | return(data)
27 |
--------------------------------------------------------------------------------
/app/routers/twoforms.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI, Request, Form, APIRouter
2 | from fastapi.responses import HTMLResponse
3 | from fastapi.templating import Jinja2Templates
4 | import os
5 |
6 | router = APIRouter()
7 | templates = Jinja2Templates(directory="templates/")
8 |
9 |
10 | @router.get("/twoforms", response_class=HTMLResponse)
11 | def form_get(request: Request):
12 | key = os.getenv("unsplash_key")
13 | print(key)
14 | result = "Type a number"
15 | return templates.TemplateResponse('twoforms.html', context={'request': request, 'result': result})
16 |
17 |
18 | @router.post("/form1", response_class=HTMLResponse)
19 | def form_post1(request: Request, number: int = Form(...)):
20 | result = number + 2
21 | return templates.TemplateResponse('twoforms.html', context={'request': request, 'result': result, 'yournum': number})
22 |
23 |
24 | @router.post("/form2", response_class=HTMLResponse)
25 | def form_post2(request: Request, number: int = Form(...)):
26 | result = number + 100
27 | return templates.TemplateResponse('twoforms.html', context={'request': request, 'result': result, 'yournum': number})
28 |
--------------------------------------------------------------------------------
/app/routers/unsplash.py:
--------------------------------------------------------------------------------
1 | import os
2 | from fastapi import Request, APIRouter, Depends
3 | from fastapi.responses import HTMLResponse
4 | from fastapi.templating import Jinja2Templates
5 |
6 |
7 | templates = Jinja2Templates(directory="templates")
8 |
9 | router = APIRouter()
10 |
11 |
12 | @router.get("/unsplash", response_class=HTMLResponse)
13 | async def unsplash_home(request: Request):
14 | key = os.getenv("unsplash_key")
15 | print(key)
16 | return templates.TemplateResponse("unsplash.html", {"request": request})
17 |
18 |
19 | @router.get("/unsplash_search", response_class=HTMLResponse)
20 | async def get_unsplash_search(request: Request):
21 | tag = "flower"
22 | return templates.TemplateResponse("unsplash_search.html", {"request": request, "tag": tag})
23 |
24 |
25 | @router.post("/unsplash_search", response_class=HTMLResponse)
26 | async def post_unsplash_search(request: Request):
27 | tag = "flower"
28 | return templates.TemplateResponse("unsplash_search.html", {"request": request, "tag": tag})
29 |
--------------------------------------------------------------------------------
/app/routers/unsplash_search.py:
--------------------------------------------------------------------------------
1 | from fastapi import Request, APIRouter
2 | from fastapi.responses import HTMLResponse
3 | from fastapi.templating import Jinja2Templates
4 | from ..library.search import unsplashFetch
5 |
6 | templates = Jinja2Templates(directory="templates")
7 | router = APIRouter()
8 |
9 |
10 | @router.get("/unsplash-search", response_class=HTMLResponse)
11 | async def get_index(request: Request,):
12 | title = "Unsplash Image Search"
13 | data = {
14 | "request": request,
15 | "title": title,
16 | }
17 |
18 | return templates.TemplateResponse("unsplash_search.html", data)
19 |
20 |
21 | @router.get("/unsplash-search/photos/{tag}/{page_num}")
22 | async def photos(request: Request, tag: str, page_num: int):
23 | # tag = tag
24 | data = unsplashFetch(tag, page_num)
25 | return(data)
26 |
27 |
28 | @router.get("/trackdownload/{id}/{ixid}")
29 | async def unsplash_track_download(request: Request, id: str, ixid: str):
30 | url = "https://api.unsplash.com/photos/"+id+"/download?ixid="+ixid
31 | # print('url', url)
32 | unsplash_key = os.getenv("unsplash_key")
33 | param = {
34 | "client_id": unsplash_key,
35 | }
36 | response = requests.get(url, param)
37 | data = response.json()
38 | return data
39 |
--------------------------------------------------------------------------------
/app/routers/upload.py:
--------------------------------------------------------------------------------
1 | from fastapi import Request, Form, APIRouter, File, UploadFile
2 | from fastapi.responses import HTMLResponse
3 | from fastapi.templating import Jinja2Templates
4 | from ..library.helpers import *
5 |
6 | router = APIRouter()
7 | templates = Jinja2Templates(directory="templates/")
8 |
9 |
10 | @router.get("/upload", response_class=HTMLResponse)
11 | def get_upload(request: Request):
12 | return templates.TemplateResponse('upload.html', context={'request': request})
13 |
14 |
15 | @router.post("/upload/new/")
16 | async def post_upload(imgdata: tuple, file: UploadFile = File(...)):
17 | print(imgdata)
18 | data_dict = eval(imgdata[0])
19 | winWidth, imgWidth, imgHeight = data_dict["winWidth"], data_dict["imgWidth"], data_dict["imgHeight"]
20 |
21 | # create the directory path
22 | workspace = create_workspace()
23 | # filename
24 | file_name = Path(file.filename)
25 | print(file.filename)
26 | print(type(file.filename))
27 | print(file_name)
28 | print(type(file_name))
29 | # image full path
30 | img_full_path = workspace / file_name
31 | with open(str(img_full_path), 'wb') as myfile:
32 | contents = await file.read()
33 | myfile.write(contents)
34 | # create a thumb image and save it
35 | thumb(img_full_path, winWidth, imgWidth, imgHeight)
36 | # create the thumb path
37 | # ext is like .png or .jpg
38 | filepath, ext = os.path.splitext(img_full_path)
39 | thumb_path = filepath + ".thumbnail"+ext
40 |
41 | data = {
42 | "img_path": img_full_path,
43 | "thumb_path": thumb_path
44 | }
45 | return data
46 |
--------------------------------------------------------------------------------
/images/drag_drop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shinokada/fastapi-webstarter-demo/74d6363d7bbb4b257bafbb5106e229c105a06b53/images/drag_drop.png
--------------------------------------------------------------------------------
/images/image-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shinokada/fastapi-webstarter-demo/74d6363d7bbb4b257bafbb5106e229c105a06b53/images/image-1.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests==2.27.1
2 | fastapi==0.72.0
3 | flickrapi==2.4.0
4 | uvicorn==0.17.0
5 | python-dotenv==0.19.2
6 | aiofiles==0.8.0
7 | python-multipart==0.0.5
8 | jinja2==3.0.3
9 | Markdown==3.3.6
10 | pytest==6.2.5
11 | Pillow==9.3.0
--------------------------------------------------------------------------------
/runtime.txt:
--------------------------------------------------------------------------------
1 | python-3.9.6
--------------------------------------------------------------------------------
/static/css/mystyle.css:
--------------------------------------------------------------------------------
1 | body {
2 | min-height: 100vh;
3 | position: relative;
4 | margin: 0;
5 | padding-bottom: 100px;
6 | box-sizing: border-box;
7 | }
8 |
9 | .wrapper{
10 | max-width: 960px;
11 | }
12 |
13 | footer {
14 | position: absolute;
15 | bottom: 0;
16 | height: 100px;
17 | padding-top:50px;
18 | }
19 |
20 | .responsive{
21 | max-width: 100%;
22 | height: auto;
23 | margin-left: auto;
24 | margin-right: auto;
25 | }
26 |
27 | /* upload home */
28 | .up-area{
29 | margin:10px auto 30px;
30 | }
31 |
32 | /* color finder */
33 | #file{
34 | display: none;
35 | }
36 |
37 | .upload{
38 | padding: 20px 0;
39 | }
40 |
41 |
42 | .upload-area{
43 | height: 250px;
44 | border: 5px dashed lightgray;
45 | border-radius: 3px;
46 | margin: 0 auto;
47 | margin-top: 20px;
48 | text-align: center;
49 | overflow: auto;
50 | position: relative;
51 | }
52 |
53 | .upload-area:hover{
54 | cursor: pointer;
55 | }
56 |
57 | .upload-area h2{
58 | font-size: 22px;
59 | padding:0;
60 | margin:0;
61 | text-align: center;
62 | font-weight: normal;
63 | font-family: sans-serif;
64 | line-height: 40px;
65 | color: darkslategray;
66 | position: relative;
67 | top: 40%;
68 | left: 50%;
69 | -ms-transform: translate(-50%, -50%);
70 | transform: translate(-50%, -50%);
71 | width:300px;
72 | }
73 |
74 | .box_button{
75 | font-weight: 700;
76 | background-color: #cacbcc;
77 | color: #515353;
78 | padding: 8px 16px;
79 | display: block;
80 | margin: 3px auto 0;
81 | cursor: pointer;
82 | margin: 0;
83 | position: absolute;
84 | top: 80%;
85 | left: 50%;
86 | -ms-transform: translate(-50%, -50%);
87 | transform: translate(-50%, -50%);
88 | }
89 |
90 | @media(max-width: 768px) {
91 | .upload-area h2{
92 | font-size: 20px;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/static/css/search_image.css:
--------------------------------------------------------------------------------
1 | /* unsplash photo search */
2 | .unsplash_search, .photo_search {
3 | margin:20px 0;
4 | }
5 |
6 |
7 | .unsplash_search .search-input, .photo_search .search-input {
8 | width: 400px;
9 | height: 38px;
10 | padding: 5px;
11 | border-radius: 4px;
12 | }
13 |
14 | .unsplash_search .search-photo, .unsplash_search .remove-photo, .photo_search .search-photo, .photo_search .remove-photo{
15 | margin: 20px 0;
16 | width: 250px;
17 | margin-right: 10px;
18 | }
19 |
20 |
21 | .unsplash_search button, .photo_search button {
22 | cursor: pointer;
23 | border-radius: 4px;
24 | /* font-family: 'Lato', sans-serif; */
25 | }
26 |
27 |
28 | .unsplash_search .hidden, .photo_search .hidden {
29 | display: none;
30 | }
31 |
32 | .unsplash_search .result-stats, .photo_search .result-stats {
33 | color: gray;
34 | padding-top: 15px;
35 | font-size: 1.5em;
36 | }
37 |
38 | .unsplash_search .search-results, .photo_search .search-results {
39 | padding: 10px;
40 | }
41 |
42 | .unsplash_search .result-item, .photo_search .result-item{
43 | width: 200px;
44 | height: auto;
45 | padding: 5px;
46 |
47 | border-radius: 4px;
48 | }
49 |
50 | .unsplash_search .photographer-name, .photo_search .photographer-name{
51 | padding: 7px;
52 | font-size: 0.7em;
53 | letter-spacing: 0.7px;
54 | }
55 |
56 | .unsplash_search .photographer-name:hover, .photo_search .photographer-name:hover{
57 | text-decoration: none;
58 | }
59 |
60 | .unsplash_search .prev-btn, .unsplash_search .next-btn, .photo_search .prev-btn, .photo_search .next-btn {
61 | color: white;
62 | text-decoration: none;
63 | }
64 |
65 | .unsplash_search button a, .photo_search button a{
66 | color: #fff;
67 | text-decoration: none;
68 | }
69 |
70 | .unsplash_search .next-btn, .photo_search .next-btn {
71 | margin-left: 10px;
72 | }
73 |
74 | .unsplash_search a:hover, .photo_search a:hover {
75 | text-decoration: none;
76 | }
77 |
78 | .unsplash_search .spinner, .photo_search .spinner {
79 | width: 40px;
80 | height: 40px;
81 | position: relative;
82 | margin: 100px auto;
83 | }
84 |
85 | .unsplash_search .double-bounce1, .unsplash_search .double-bounce2, .photo_search .double-bounce1, .photo_search .double-bounce2 {
86 | width: 100%;
87 | height: 100%;
88 | border-radius: 50%;
89 | background-color: #333;
90 | opacity: 0.6;
91 | position: absolute;
92 | top: 0;
93 | left: 0;
94 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out;
95 | animation: sk-bounce 2.0s infinite ease-in-out;
96 | }
97 |
98 | .unsplash_search .double-bounce2, .photo_search .double-bounce2 {
99 | -webkit-animation-delay: -1.0s;
100 | animation-delay: -1.0s;
101 | }
102 |
103 | @-webkit-keyframes sk-bounce {
104 | 0%, 100% { -webkit-transform: scale(0.0) }
105 | 50% { -webkit-transform: scale(1.0) }
106 | }
107 |
108 | @keyframes sk-bounce {
109 | 0%, 100% {
110 | transform: scale(0.0);
111 | -webkit-transform: scale(0.0);
112 | } 50% {
113 | transform: scale(1.0);
114 | -webkit-transform: scale(1.0);
115 | }
116 | }
117 |
118 | .unsplash_thumb{
119 | text-align: center;
120 | display: block;
121 | }
122 |
123 | #unsplash-img, .selected-img{
124 | display: none;
125 | }
126 |
127 | #unsplash-img, #flickrPhoto {
128 | text-align: center;
129 | }
130 |
131 | .desc a{
132 | text-decoration: underline;
133 | }
--------------------------------------------------------------------------------
/static/css/style3.css:
--------------------------------------------------------------------------------
1 | /*
2 | DEMO STYLE
3 | https://bootstrapious.com/p/bootstrap-sidebar
4 | */
5 |
6 | @import "https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700";
7 | body {
8 | font-family: 'Poppins', sans-serif;
9 | background: #fafafa;
10 | }
11 |
12 | p {
13 | font-family: 'Poppins', sans-serif;
14 | font-size: 1.1em;
15 | font-weight: 300;
16 | line-height: 1.7em;
17 | color: #000;
18 | }
19 |
20 | a,
21 | a:hover,
22 | a:focus {
23 | color: inherit;
24 | text-decoration: none;
25 | transition: all 0.3s;
26 | }
27 |
28 | .navbar {
29 | padding: 15px 10px;
30 | background: #fff;
31 | border: none;
32 | border-radius: 0;
33 | margin-bottom: 40px;
34 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
35 | }
36 |
37 | .navbar-btn {
38 | box-shadow: none;
39 | outline: none !important;
40 | border: none;
41 | }
42 |
43 | .line {
44 | width: 100%;
45 | height: 1px;
46 | border-bottom: 1px dashed #ddd;
47 | margin: 40px 0;
48 | }
49 |
50 | /* ---------------------------------------------------
51 | SIDEBAR STYLE
52 | ----------------------------------------------------- */
53 |
54 | #sidebar {
55 | width: 250px;
56 | position: fixed;
57 | top: 0;
58 | left: -250px;
59 | height: 100vh;
60 | z-index: 999;
61 | background: #7386D5;
62 | color: #fff;
63 | transition: all 0.3s;
64 | overflow-y: scroll;
65 | box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.2);
66 | }
67 |
68 | #sidebar.active {
69 | left: 0;
70 | }
71 |
72 | #dismiss {
73 | width: 35px;
74 | height: 35px;
75 | line-height: 35px;
76 | text-align: center;
77 | background: #7386D5;
78 | position: absolute;
79 | top: 10px;
80 | right: 10px;
81 | cursor: pointer;
82 | -webkit-transition: all 0.3s;
83 | -o-transition: all 0.3s;
84 | transition: all 0.3s;
85 | }
86 |
87 | #dismiss:hover {
88 | background: #fff;
89 | color: #7386D5;
90 | }
91 |
92 | .overlay {
93 | display: none;
94 | position: fixed;
95 | width: 100vw;
96 | height: 100vh;
97 | background: rgba(0, 0, 0, 0.7);
98 | z-index: 998;
99 | opacity: 0;
100 | transition: all 0.5s ease-in-out;
101 | }
102 | .overlay.active {
103 | display: block;
104 | opacity: 1;
105 | }
106 |
107 | #sidebar .sidebar-header {
108 | padding: 20px;
109 | background: #6d7fcc;
110 | }
111 |
112 | #sidebar ul.components {
113 | padding: 20px 0;
114 | border-bottom: 1px solid #47748b;
115 | }
116 |
117 | #sidebar ul p {
118 | color: #fff;
119 | padding: 10px;
120 | }
121 |
122 | #sidebar ul li a {
123 | padding: 10px;
124 | font-size: 1.1em;
125 | display: block;
126 |
127 | }
128 |
129 | #sidebar ul li a:hover {
130 | color: #7386D5;
131 | background: #fff;
132 | }
133 |
134 | #sidebar .dropdown-menu{
135 | width: 95%;
136 | }
137 |
138 | a[data-toggle="collapse"] {
139 | position: relative;
140 | }
141 |
142 | ul.CTAs {
143 | padding: 20px;
144 | }
145 |
146 | ul.CTAs a {
147 | text-align: center;
148 | font-size: 0.9em !important;
149 | display: block;
150 | border-radius: 5px;
151 | margin-bottom: 5px;
152 | }
153 |
154 | a.article,
155 | a.article:hover {
156 | background: #6d7fcc !important;
157 | color: #fff !important;
158 | }
159 |
160 | /* ---------------------------------------------------
161 | CONTENT STYLE
162 | ----------------------------------------------------- */
163 |
164 | #content {
165 | width: 100%;
166 | padding: 20px;
167 | min-height: 100vh;
168 | transition: all 0.3s;
169 | position: absolute;
170 | top: 0;
171 | right: 0;
172 | }
173 |
174 | #sidebarCollapse span{
175 | color: #fff;
176 | }
177 |
178 | #sidebarCollapse i{
179 | color: #fff;
180 | }
181 |
182 | /* Nav */
183 | .navbar-nav {
184 | justify-content: end;
185 | width:100%;
186 | }
187 |
188 | /* copy to clipboard */
189 | .mycontainer{
190 | width: 400px;
191 | margin: 0 auto;
192 | text-align: center;
193 | }
194 |
195 | .mycontainer a{
196 | text-decoration: underline;
197 | color:rgb(11, 1, 77);
198 | }
199 |
200 | .gif-img{
201 | width:662px;
202 | margin:20px auto;
203 | }
204 |
205 | .gif-img img{
206 | margin-top: 20px;
207 | }
208 |
--------------------------------------------------------------------------------
/static/images/copy.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shinokada/fastapi-webstarter-demo/74d6363d7bbb4b257bafbb5106e229c105a06b53/static/images/copy.gif
--------------------------------------------------------------------------------
/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shinokada/fastapi-webstarter-demo/74d6363d7bbb4b257bafbb5106e229c105a06b53/static/images/favicon.ico
--------------------------------------------------------------------------------
/static/images/image-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shinokada/fastapi-webstarter-demo/74d6363d7bbb4b257bafbb5106e229c105a06b53/static/images/image-1.png
--------------------------------------------------------------------------------
/static/images/logo.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/static/images/unsplash1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shinokada/fastapi-webstarter-demo/74d6363d7bbb4b257bafbb5106e229c105a06b53/static/images/unsplash1.jpeg
--------------------------------------------------------------------------------
/static/js/dragdrop.js:
--------------------------------------------------------------------------------
1 | import { detect, dragdrop } from './imagehelper.js'
2 |
3 | $(function () {
4 | dragdrop();
5 |
6 | function preparedata (file) {
7 | console.log("Preparing ...")
8 | const img = document.createElement("img");
9 | img.src = URL.createObjectURL(file);
10 | detect(img.src, function (result) {
11 | let winWidth = $(window).width();
12 | let imgWidth = result.imgWidth;
13 | let imgHeight = result.imgHeight;
14 | let data = { 'winWidth': winWidth, 'imgWidth': imgWidth, 'imgHeight': imgHeight };
15 | let jdata = JSON.stringify(data);
16 | let fd = new FormData();
17 | fd.append('imgdata', jdata);
18 | fd.append('file', file);
19 | console.log("fd: ", fd);
20 | uploadData(fd);
21 | });
22 | }
23 |
24 | // Drop
25 | $('.upload-area').on('drop', function (e) {
26 | e.stopPropagation();
27 | e.preventDefault();
28 | $("#howto").text("We are uploading your file.");
29 | let file = e.originalEvent.dataTransfer.files;
30 | console.log("File uploaded: ", file);
31 | let imageType = /image.*/;
32 | let winWidth = $("#window_width").val();
33 | let dropped = file[0];
34 | if (dropped.type.match(imageType)) {
35 | preparedata(dropped);
36 | } else {
37 | console.log("not image");
38 | $("#howto").text("Please use an image file. Try one more time.");
39 | }
40 | console.log("done drop.")
41 | });
42 |
43 | // Open file selector on div click
44 | $("#uploadfile").click(function () {
45 | $("#file").click();
46 | });
47 |
48 | // file selected
49 | $("#file").change(function () {
50 | let imageType = /image.*/;
51 | let file = $('#file')[0].files[0];
52 | console.log("file.size: ", file.size);
53 | $("#howto").text("Uploading your file.");
54 | if (file.type.match(imageType)) {
55 | console.log("file: ", file);
56 | preparedata(file);
57 | } else {
58 | console.log("not image");
59 | $("#howto").text("Please use an image file. Try one more time.");
60 | }
61 | });
62 | });
63 |
64 |
65 |
66 | // Sending AJAX request and upload file
67 | function uploadData (formdata) {
68 |
69 | $.ajax({
70 | url: '/upload/new/',
71 | type: 'post',
72 | data: formdata,
73 | contentType: false,
74 | processData: false,
75 | success: function (data) {
76 | updatetags(data);
77 | }
78 | });
79 | }
80 |
81 | function updatetags (data) {
82 | let original = `Photo by 149 | ${photographer} on Flickr 150 |
151 |Photo by 130 | ${photographer} on Unsplash 131 |
132 |16 | Photo by Pietro De Grandi on Unsplash 17 |
18 | 19 |Source
17 | 18 |Source
20 | 21 | {% endblock %} 22 | 23 | {% block scripts %} 24 | {{ super() }} 25 | 26 | 27 | {% endblock %} -------------------------------------------------------------------------------- /templates/unsplash_search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% set active_page = "unsplash-search" %} 3 | 4 | {% block title %}Unsplash Search{% endblock %} 5 | 6 | {% block head %} 7 | {{ super() }} 8 | 9 | {% endblock %} 10 | 11 | {% block page_content %} 12 |